Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion @codexteam/ui/dev/index.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<head>
<title>Codex UI</title>
<link rel="stylesheet" href="/styles/fonts.css">
</head>
<body
color-scheme="dark"
Expand All @@ -10,7 +11,6 @@
<script type="module">
import { createApp } from 'vue';
import { createRouter, createWebHashHistory } from 'vue-router';
import "../src/styles/fonts.pcss";
import "../src/styles/index.pcss";
import "../src/styles/themes/index.pcss";

Expand Down
2 changes: 1 addition & 1 deletion @codexteam/ui/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@codexteam/ui",
"version": "0.2.7",
"version": "0.2.8",
"type": "module",
"sideEffects": [
"*.css",
Expand Down
45 changes: 42 additions & 3 deletions @codexteam/ui/src/vue/components/avatar/Avatar.vue
Original file line number Diff line number Diff line change
@@ -1,24 +1,33 @@
<template>
<img
v-if="src && !hasLoadingError"
:src="src"
:alt="`Avatar of ${username}`"
:class="$style[`avatar--${size}`]"
referrerpolicy="no-referrer"
@error="hasLoadingError = true"
>
<span
v-else
:class="[$style[`avatar--${size}`], $style['avatar--placeholder']]"
:title="username"
>
{{ initials }}
</span>
</template>

<script setup lang="ts">
import { defineProps } from 'vue';
import { computed, ref, watch } from 'vue';

withDefaults(defineProps<{
const props = withDefaults(defineProps<{
/**
* Path to the image
*/
src: string;

/**
* Name of the user
* In future, we can use this to generate initials
* Its initials are displayed when there is no image
*/
username: string;

Expand All @@ -34,6 +43,18 @@ withDefaults(defineProps<{
size: 'medium',
});

const hasLoadingError = ref(false);

watch(() => props.src, () => {
hasLoadingError.value = false;
});

const initials = computed(() => (props.username ?? '')
.split(/\s+/)
.filter(Boolean)
.slice(0, 2)
.map(word => word[0].toUpperCase())
.join('') || '?');
</script>

<style module>
Expand All @@ -42,12 +63,30 @@ withDefaults(defineProps<{
width: var(--size-icon);
height: var(--size-icon);
border-radius: var(--radius-s);
font-size: 9px;
}

&--medium {
width: var(--size-avatar);
height: var(--size-avatar);
border-radius: var(--radius-m);
font-size: 12px;
}

&--small,
&--medium {
flex-shrink: 0;
object-fit: cover;
}

&--placeholder {
display: inline-flex;
align-items: center;
justify-content: center;
font-weight: 600;
background-color: var(--base--solid);
color: var(--base--text-solid-foreground);
user-select: none;
}
}
</style>
5 changes: 5 additions & 0 deletions @codexteam/ui/src/vue/components/button/Button.vue
Original file line number Diff line number Diff line change
Expand Up @@ -194,5 +194,10 @@ const iconType = computed<'none' | 'leading' | 'trailing' | 'leadingTrailing' |
&:hover {
background-color: var(--bg-hover);
}

&:focus-visible {
outline: 2px solid var(--accent--solid);
outline-offset: 2px;
}
}
</style>
26 changes: 15 additions & 11 deletions @codexteam/ui/src/vue/components/confirm/Confirm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,13 @@
secondary
@click="onCancel"
>
<div :class="$style['confirm__button-inner']">
{{ cancelText }}
</div>
{{ cancelText }}
</Button>
<Button
primary
:destructive="destructive"
@click="onConfirm"
>
<div :class="$style['confirm__button-inner']">
{{ confirmText }}
</div>
{{ confirmText }}
</Button>
</div>
</div>
Expand Down Expand Up @@ -56,6 +52,11 @@ const props = withDefaults(
*/
cancelText?: string;

/**
* Style the confirm button as a negative action
*/
destructive?: boolean;

/**
* Function that is executed after pressing the Cancel button
*/
Expand All @@ -69,6 +70,7 @@ const props = withDefaults(
{
confirmText: 'Confirm',
cancelText: 'Cancel',
destructive: false,
}
);

Expand Down Expand Up @@ -114,21 +116,23 @@ onUnmounted(() => {
flex-direction: column;
gap: var(--v-padding);
text-align: center;
width: min-content;
width: 340px;

&__body {
padding: 0 var(--spacing-ml) 0 var(--spacing-ml);
word-wrap: break-word;
color: var(--base--text-secondary);
}

&__controls {
display: flex;
padding: var(--v-padding) 0 0 0;
gap: var(--spacing-m);
}

&__button-inner {
width: 84px;
& > button {
flex: 1;
justify-content: center;
}
}
}

Expand Down
37 changes: 33 additions & 4 deletions @codexteam/ui/src/vue/components/confirm/useConfirm.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,57 @@
import { createSharedComposable } from '@vueuse/core';
import { watch } from 'vue';
import { usePopup } from '../popup';
import { Confirm } from '.';

/**
* Optional texts and style of the confirm window
*/
export interface ConfirmOptions {
/**
* Text of the confirm button
*/
confirmText?: string;

/**
* Text of the cancel button
*/
cancelText?: string;

/**
* Style the confirm button as a negative action
*/
destructive?: boolean;
}

export const useConfirm = createSharedComposable(() => {
/**
* Used to create a Popup component that will display the current Confirm
*/
const { showPopup, hidePopup } = usePopup();
const { showPopup, hidePopup, isOpen } = usePopup();

/**
* @param title - title of the confirm window
* @param text - message to be displayed in confirm window
* @returns user selection result
* @param options - button texts and style
* @returns user selection result, false if the popup was closed without a choice
*/
async function confirm(title: string, text: string): Promise<boolean> {
async function confirm(title: string, text: string, options: ConfirmOptions = {}): Promise<boolean> {
return new Promise<boolean>((resolve) => {
const stop = watch(isOpen, (open) => {
if (!open) {
stop();
resolve(false);
}
});

showPopup({
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
component: Confirm,
props: {
...options,
title: title,
text: text,
onCancel: () => {
resolve(false);
hidePopup();
},
onConfirm: () => {
Expand Down
7 changes: 6 additions & 1 deletion @codexteam/ui/src/vue/components/editor/Editor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ const editorConfig = computed<EditorConfig>(() => {
};
});

const { isEmpty } = useEditor(editorConfig, {
const { isEmpty, save } = useEditor(editorConfig, {
onChange: data => emit('change', data),
});

Expand All @@ -85,6 +85,11 @@ defineExpose({
return isEmpty.value;
},

/**
* Returns the current content, including changes not yet reported by the change event
*/
save,

/**
* Returns the editor holder
*/
Expand Down
28 changes: 24 additions & 4 deletions @codexteam/ui/src/vue/components/editor/useEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ interface UseEditorComposableState {
* Attribute containing is-empty state.
*/
isEmpty: Ref<boolean>;

/**
* Returns the current content, including changes not yet reported by onChange
*/
save: () => Promise<OutputData | undefined>;
}

/**
Expand Down Expand Up @@ -67,10 +72,18 @@ export function useEditor(editorConfig: MaybeRefOrGetter<EditorConfig>, options:
/**
* Function called on every change of the editor
* @param api - EditorJS API
* @param instance - editor that reported the change
*/
async function handleChange(api: API): Promise<void> {
async function handleChange(api: API, instance: Editor): Promise<void> {
const data = await api.saver.save();

/**
* Editor.js reports changes with a delay, drop the ones from an editor that has been destroyed meanwhile
*/
if (instance !== editor) {
return;
}

/**
* Update the isEmpty attribute
*/
Expand Down Expand Up @@ -102,14 +115,18 @@ export function useEditor(editorConfig: MaybeRefOrGetter<EditorConfig>, options:
const config = toValue(editorConfig);

try {
editor = new Editor({
const instance: Editor = new Editor({
...config,
onChange(api: API) {
void handleChange(api);
if (instance === editor) {
void handleChange(api, instance);
}
},
});

await editor?.isReady;
editor = instance;

await instance.isReady;
} catch (e) {
console.error(e);
}
Expand All @@ -131,5 +148,8 @@ export function useEditor(editorConfig: MaybeRefOrGetter<EditorConfig>, options:

return {
isEmpty,
async save() {
return await editor?.save();
},
};
}
6 changes: 5 additions & 1 deletion @codexteam/ui/src/vue/components/row/Row.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

<div
v-if="subtitle"
class="text-ui-subtle"
:class="[$style['row__subtitle'], 'text-ui-subtle']"
>
{{ subtitle }}
</div>
Expand Down Expand Up @@ -108,6 +108,10 @@ defineProps<{
gap: var(--spacing-very-x);
}

&__subtitle {
color: var(--base--text-secondary);
}

&__right {
display: flex;
align-items: center;
Expand Down
10 changes: 2 additions & 8 deletions @codexteam/ui/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ export default defineConfig({
* With cssInjectedByJsPlugin enabled:
* - Component CSS is injected into vue.js bundle (no separate CSS files for components)
* - Base styles are emitted as dist/style.css
* - Fonts are emitted as dist/styles/fonts.css (optional import)
* - Fonts are copied from public/ as dist/styles/fonts.css and dist/fonts/* (optional import),
* lib mode would inline them as base64 otherwise
* - Theme CSS files are emitted as dist/styles/themes/*.css
* This allows users to import only the themes they need:
* - import '@codexteam/ui/styles' (base styles)
Expand All @@ -44,13 +45,6 @@ export default defineConfig({
*/
style: resolve(__dirname, 'src/styles/index.pcss'),

/**
* Optional fonts (Inter + JetBrains Mono)
* Exported as: @codexteam/ui/styles/fonts
* Vite will automatically copy font files to dist/fonts/ when processing @import url()
*/
'styles/fonts': resolve(__dirname, 'src/styles/fonts.pcss'),

/**
* Individual themes for tree-shaking
*/
Expand Down
Loading
Loading