sv5ui 1.8.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Accordion/Accordion.svelte +11 -0
- package/dist/Alert/alert.types.d.ts +1 -1
- package/dist/AvatarGroup/AvatarGroup.svelte +5 -3
- package/dist/Badge/badge.types.d.ts +1 -1
- package/dist/Button/Button.svelte +7 -6
- package/dist/Button/button.types.d.ts +3 -3
- package/dist/Calendar/Calendar.svelte +14 -1
- package/dist/Collapsible/collapsible.types.d.ts +4 -2
- package/dist/Command/command.types.d.ts +4 -2
- package/dist/Command/index.d.ts +1 -1
- package/dist/ContextMenu/ContextMenu.svelte +1 -1
- package/dist/Drawer/Drawer.svelte +7 -3
- package/dist/Drawer/DrawerTriggerTestWrapper.svelte +10 -0
- package/dist/Drawer/DrawerTriggerTestWrapper.svelte.d.ts +18 -0
- package/dist/Drawer/drawer.types.d.ts +13 -2
- package/dist/DropdownMenu/DropdownMenu.svelte +1 -3
- package/dist/DropdownMenu/DropdownMenuTriggerTestWrapper.svelte +12 -0
- package/dist/DropdownMenu/DropdownMenuTriggerTestWrapper.svelte.d.ts +7 -0
- package/dist/DropdownMenu/dropdown-menu.types.d.ts +17 -9
- package/dist/Editor/Editor.svelte +85 -61
- package/dist/Editor/SlashPopup.svelte +8 -1
- package/dist/Editor/SlashPopup.svelte.d.ts +2 -0
- package/dist/Editor/editor.extensions.d.ts +1 -1
- package/dist/Editor/editor.extensions.js +25 -16
- package/dist/Editor/editor.schemas.d.ts +1 -0
- package/dist/Editor/editor.schemas.js +24 -0
- package/dist/Editor/editor.slash.svelte.d.ts +0 -9
- package/dist/Editor/editor.slash.svelte.js +33 -7
- package/dist/Editor/editor.suggestion.js +23 -0
- package/dist/Editor/editor.toolbar.js +0 -8
- package/dist/Editor/editor.types.d.ts +20 -0
- package/dist/Editor/editor.variants.d.ts +0 -5
- package/dist/Editor/editor.variants.js +0 -15
- package/dist/Editor/index.d.ts +6 -4
- package/dist/Editor/index.js +6 -4
- package/dist/FileUpload/FileUpload.svelte +7 -0
- package/dist/Icon/icon.types.d.ts +4 -1
- package/dist/Input/Input.svelte +22 -16
- package/dist/Input/index.d.ts +1 -1
- package/dist/Input/input.variants.d.ts +0 -15
- package/dist/Input/input.variants.js +1 -20
- package/dist/Link/Link.svelte +4 -3
- package/dist/Link/link.types.d.ts +2 -2
- package/dist/Modal/Modal.svelte +4 -2
- package/dist/Modal/ModalTriggerTestWrapper.svelte +10 -0
- package/dist/Modal/ModalTriggerTestWrapper.svelte.d.ts +18 -0
- package/dist/Modal/modal.types.d.ts +13 -3
- package/dist/Pagination/Pagination.svelte +7 -1
- package/dist/Pagination/pagination.types.d.ts +4 -1
- package/dist/Pagination/pagination.variants.d.ts +0 -72
- package/dist/Pagination/pagination.variants.js +6 -30
- package/dist/Popover/Popover.svelte +1 -1
- package/dist/Popover/popover.types.d.ts +2 -0
- package/dist/Progress/Progress.svelte +14 -6
- package/dist/RadioGroup/RadioGroup.svelte +3 -1
- package/dist/Select/Select.svelte +3 -1
- package/dist/Select/select.types.d.ts +5 -9
- package/dist/SelectMenu/SelectMenu.svelte +27 -10
- package/dist/SelectMenu/SelectMenuFormFieldTestWrapper.svelte +11 -0
- package/dist/SelectMenu/SelectMenuFormFieldTestWrapper.svelte.d.ts +7 -0
- package/dist/SelectMenu/select-menu.types.d.ts +5 -2
- package/dist/SelectMenu/select-menu.variants.d.ts +12 -2
- package/dist/SelectMenu/select-menu.variants.js +10 -1
- package/dist/Separator/Separator.svelte +9 -2
- package/dist/Separator/separator.types.d.ts +6 -1
- package/dist/Separator/separator.variants.d.ts +25 -0
- package/dist/Separator/separator.variants.js +7 -1
- package/dist/Skeleton/Skeleton.svelte +3 -5
- package/dist/Slideover/Slideover.svelte +4 -2
- package/dist/Slideover/SlideoverTriggerTestWrapper.svelte +10 -0
- package/dist/Slideover/SlideoverTriggerTestWrapper.svelte.d.ts +18 -0
- package/dist/Slideover/slideover.types.d.ts +13 -3
- package/dist/Stepper/Stepper.svelte +1 -3
- package/dist/Switch/Switch.svelte +12 -17
- package/dist/Table/table.utils.d.ts +7 -4
- package/dist/Table/table.utils.js +26 -25
- package/dist/Tabs/Tabs.svelte +4 -2
- package/dist/Tabs/tabs.types.d.ts +4 -6
- package/dist/ThemeModeButton/ThemeModeButton.svelte +4 -3
- package/dist/Tooltip/Tooltip.svelte +1 -1
- package/dist/Tooltip/tooltip.types.d.ts +2 -0
- package/dist/hooks/HookContextProbe.svelte +7 -0
- package/dist/hooks/HookContextProbe.svelte.d.ts +18 -0
- package/dist/hooks/HookContextProvider.svelte +9 -0
- package/dist/hooks/HookContextProvider.svelte.d.ts +18 -0
- package/dist/hooks/HookEmitProbe.svelte +14 -0
- package/dist/hooks/HookEmitProbe.svelte.d.ts +18 -0
- package/dist/hooks/index.d.ts +1 -1
- package/dist/hooks/index.js +1 -1
- package/dist/hooks/useFormField.svelte.d.ts +0 -31
- package/dist/hooks/useFormField.svelte.js +0 -21
- package/package.json +1 -1
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
items: SlashCommand[]
|
|
7
7
|
selectedIndex: number
|
|
8
8
|
onPick: (index: number) => void
|
|
9
|
+
listboxId: string
|
|
10
|
+
optionIdPrefix: string
|
|
9
11
|
}
|
|
10
12
|
|
|
11
|
-
let { items, selectedIndex, onPick }: Props = $props()
|
|
13
|
+
let { items, selectedIndex, onPick, listboxId, optionIdPrefix }: Props = $props()
|
|
12
14
|
|
|
13
15
|
let listEl: HTMLDivElement | null = $state(null)
|
|
14
16
|
|
|
@@ -24,7 +26,9 @@
|
|
|
24
26
|
|
|
25
27
|
<div
|
|
26
28
|
bind:this={listEl}
|
|
29
|
+
id={listboxId}
|
|
27
30
|
role="listbox"
|
|
31
|
+
aria-label="Slash commands"
|
|
28
32
|
data-editor-slash-popup
|
|
29
33
|
class="max-h-72 max-w-80 min-w-64 overflow-y-auto rounded-lg border border-outline-variant bg-surface py-1 shadow-lg"
|
|
30
34
|
>
|
|
@@ -35,6 +39,9 @@
|
|
|
35
39
|
{@const active = i === selectedIndex}
|
|
36
40
|
<button
|
|
37
41
|
type="button"
|
|
42
|
+
role="option"
|
|
43
|
+
id={`${optionIdPrefix}${i}`}
|
|
44
|
+
aria-selected={active}
|
|
38
45
|
data-slash-item
|
|
39
46
|
data-id={cmd.id}
|
|
40
47
|
data-index={i}
|
|
@@ -3,6 +3,8 @@ interface Props {
|
|
|
3
3
|
items: SlashCommand[];
|
|
4
4
|
selectedIndex: number;
|
|
5
5
|
onPick: (index: number) => void;
|
|
6
|
+
listboxId: string;
|
|
7
|
+
optionIdPrefix: string;
|
|
6
8
|
}
|
|
7
9
|
declare const SlashPopup: import("svelte").Component<Props, {}, "">;
|
|
8
10
|
type SlashPopup = ReturnType<typeof SlashPopup>;
|
|
@@ -19,5 +19,5 @@ interface BuildExtensionsOptions {
|
|
|
19
19
|
dragHandle?: boolean;
|
|
20
20
|
extra?: AnyExtension[];
|
|
21
21
|
}
|
|
22
|
-
export declare function buildExtensions(options?: BuildExtensionsOptions): AnyExtension[]
|
|
22
|
+
export declare function buildExtensions(options?: BuildExtensionsOptions): AnyExtension[] | Promise<AnyExtension[]>;
|
|
23
23
|
export {};
|
|
@@ -5,11 +5,6 @@ import Typography from '@tiptap/extension-typography';
|
|
|
5
5
|
import CharacterCount from '@tiptap/extension-character-count';
|
|
6
6
|
import Image from '@tiptap/extension-image';
|
|
7
7
|
import Mention from '@tiptap/extension-mention';
|
|
8
|
-
import { Table } from '@tiptap/extension-table';
|
|
9
|
-
import { TableRow } from '@tiptap/extension-table-row';
|
|
10
|
-
import { TableCell } from '@tiptap/extension-table-cell';
|
|
11
|
-
import { TableHeader } from '@tiptap/extension-table-header';
|
|
12
|
-
import { Markdown } from 'tiptap-markdown';
|
|
13
8
|
import Youtube from '@tiptap/extension-youtube';
|
|
14
9
|
import { DragHandle } from '@tiptap/extension-drag-handle';
|
|
15
10
|
import { buildSlashExtension } from './editor.slash.svelte.js';
|
|
@@ -38,7 +33,13 @@ function buildImageExt() {
|
|
|
38
33
|
HTMLAttributes: { class: 'sv5ui-editor-image' }
|
|
39
34
|
});
|
|
40
35
|
}
|
|
41
|
-
function buildTableExts() {
|
|
36
|
+
async function buildTableExts() {
|
|
37
|
+
const [{ Table }, { TableRow }, { TableCell }, { TableHeader }] = await Promise.all([
|
|
38
|
+
import('@tiptap/extension-table'),
|
|
39
|
+
import('@tiptap/extension-table-row'),
|
|
40
|
+
import('@tiptap/extension-table-cell'),
|
|
41
|
+
import('@tiptap/extension-table-header')
|
|
42
|
+
]);
|
|
42
43
|
return [
|
|
43
44
|
Table.configure({
|
|
44
45
|
resizable: true,
|
|
@@ -49,7 +50,8 @@ function buildTableExts() {
|
|
|
49
50
|
TableCell
|
|
50
51
|
];
|
|
51
52
|
}
|
|
52
|
-
function buildMarkdownExt(allowHtml) {
|
|
53
|
+
async function buildMarkdownExt(allowHtml) {
|
|
54
|
+
const { Markdown } = await import('tiptap-markdown');
|
|
53
55
|
return Markdown.configure({
|
|
54
56
|
html: allowHtml,
|
|
55
57
|
tightLists: true,
|
|
@@ -73,8 +75,6 @@ function buildYoutubeExt() {
|
|
|
73
75
|
HTMLAttributes: { class: 'sv5ui-editor-youtube' }
|
|
74
76
|
});
|
|
75
77
|
}
|
|
76
|
-
// lucide:grip-vertical inline SVG. Used directly because @iconify/svelte
|
|
77
|
-
// component would need to be mounted via Svelte; this is a vanilla DOM helper.
|
|
78
78
|
const GRIP_VERTICAL_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="9" cy="5" r="1"/><circle cx="9" cy="12" r="1"/><circle cx="9" cy="19" r="1"/><circle cx="15" cy="5" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="15" cy="19" r="1"/></svg>';
|
|
79
79
|
function buildDragHandleExt() {
|
|
80
80
|
return DragHandle.configure({
|
|
@@ -106,18 +106,27 @@ const OPTIONAL_BUILDERS = [
|
|
|
106
106
|
(o) => (o.dragHandle ? buildDragHandleExt() : null)
|
|
107
107
|
];
|
|
108
108
|
function collectOptionalExts(options) {
|
|
109
|
-
const
|
|
109
|
+
const sync = [];
|
|
110
|
+
const lazy = [];
|
|
110
111
|
for (const build of OPTIONAL_BUILDERS) {
|
|
111
112
|
const result = build(options);
|
|
112
|
-
if (result === null)
|
|
113
|
+
if (result === null || result === undefined)
|
|
113
114
|
continue;
|
|
114
|
-
if (
|
|
115
|
-
|
|
115
|
+
if (result instanceof Promise)
|
|
116
|
+
lazy.push(result);
|
|
117
|
+
else if (Array.isArray(result))
|
|
118
|
+
sync.push(...result);
|
|
116
119
|
else
|
|
117
|
-
|
|
120
|
+
sync.push(result);
|
|
118
121
|
}
|
|
119
|
-
|
|
122
|
+
if (lazy.length === 0)
|
|
123
|
+
return sync;
|
|
124
|
+
return Promise.all(lazy).then((resolved) => [...sync, ...resolved.flat()]);
|
|
120
125
|
}
|
|
121
126
|
export function buildExtensions(options = {}) {
|
|
122
|
-
|
|
127
|
+
const optional = collectOptionalExts(options);
|
|
128
|
+
if (optional instanceof Promise) {
|
|
129
|
+
return optional.then((opt) => [...buildCore(options), ...opt, ...(options.extra ?? [])]);
|
|
130
|
+
}
|
|
131
|
+
return [...buildCore(options), ...optional, ...(options.extra ?? [])];
|
|
123
132
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
|
2
2
|
export type UrlSchema = StandardSchemaV1<string, string>;
|
|
3
3
|
export declare const httpUrlSchema: UrlSchema;
|
|
4
|
+
export declare function isSafeImageSrc(src: string): boolean;
|
|
4
5
|
export declare const youtubeUrlSchema: UrlSchema;
|
|
@@ -1,3 +1,27 @@
|
|
|
1
1
|
import * as v from 'valibot';
|
|
2
2
|
export const httpUrlSchema = v.pipe(v.string(), v.trim(), v.nonEmpty('URL is required'), v.url('Please enter a valid URL'), v.regex(/^https?:\/\//i, 'URL must start with http:// or https://'));
|
|
3
|
+
function normalizeUrl(src) {
|
|
4
|
+
const s = src.replace(/[\t\n\r]/g, '');
|
|
5
|
+
let start = 0;
|
|
6
|
+
let end = s.length;
|
|
7
|
+
while (start < end && s.charCodeAt(start) <= 0x20)
|
|
8
|
+
start++;
|
|
9
|
+
while (end > start && s.charCodeAt(end - 1) <= 0x20)
|
|
10
|
+
end--;
|
|
11
|
+
return s.slice(start, end);
|
|
12
|
+
}
|
|
13
|
+
export function isSafeImageSrc(src) {
|
|
14
|
+
const s = normalizeUrl(src);
|
|
15
|
+
if (!s)
|
|
16
|
+
return false;
|
|
17
|
+
const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(s)?.[1]?.toLowerCase();
|
|
18
|
+
if (!scheme)
|
|
19
|
+
return true;
|
|
20
|
+
if (scheme === 'http' || scheme === 'https')
|
|
21
|
+
return true;
|
|
22
|
+
if (scheme === 'data') {
|
|
23
|
+
return /^data:image\/(?:png|jpe?g|gif|webp|avif|bmp|x-icon|vnd\.microsoft\.icon)[;,]/i.test(s);
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
3
27
|
export const youtubeUrlSchema = v.pipe(v.string(), v.trim(), v.nonEmpty('URL is required'), v.url('Please enter a valid URL'), v.regex(/^https?:\/\/(?:www\.|m\.)?(?:youtube\.com|youtu\.be|youtube-nocookie\.com)\//i, 'Must be a YouTube URL (youtube.com or youtu.be)'));
|
|
@@ -14,17 +14,8 @@ interface SlashCommandsContext {
|
|
|
14
14
|
image?: boolean;
|
|
15
15
|
tables?: boolean;
|
|
16
16
|
youtube?: boolean;
|
|
17
|
-
/**
|
|
18
|
-
* Override for the URL prompt used by image/youtube commands. Pass an
|
|
19
|
-
* async function that resolves to a URL string, or `null`/empty string
|
|
20
|
-
* to cancel. When omitted, falls back to `window.prompt`.
|
|
21
|
-
*/
|
|
22
17
|
promptUrl?: (opts: UrlPromptOptions) => Promise<string | null>;
|
|
23
18
|
}
|
|
24
|
-
/**
|
|
25
|
-
* Returns the built-in slash command list, optionally including media
|
|
26
|
-
* commands based on which features are enabled in the host Editor.
|
|
27
|
-
*/
|
|
28
19
|
export declare function buildDefaultSlashCommands(ctx?: SlashCommandsContext): SlashCommand[];
|
|
29
20
|
interface SlashExtensionOptions {
|
|
30
21
|
suggestion: Partial<SuggestionOptions>;
|
|
@@ -9,10 +9,6 @@ function defaultPromptUrl(opts) {
|
|
|
9
9
|
return Promise.resolve(null);
|
|
10
10
|
return Promise.resolve(window.prompt(opts.title, opts.initialValue ?? opts.placeholder ?? ''));
|
|
11
11
|
}
|
|
12
|
-
/**
|
|
13
|
-
* Returns the built-in slash command list, optionally including media
|
|
14
|
-
* commands based on which features are enabled in the host Editor.
|
|
15
|
-
*/
|
|
16
12
|
export function buildDefaultSlashCommands(ctx = {}) {
|
|
17
13
|
const commands = [
|
|
18
14
|
{
|
|
@@ -143,6 +139,7 @@ export function buildDefaultSlashCommands(ctx = {}) {
|
|
|
143
139
|
}
|
|
144
140
|
return commands;
|
|
145
141
|
}
|
|
142
|
+
let slashSeq = 0;
|
|
146
143
|
function substringFilter(commands, query) {
|
|
147
144
|
const q = query.trim().toLowerCase();
|
|
148
145
|
if (!q)
|
|
@@ -163,6 +160,10 @@ function buildSuggestionRender() {
|
|
|
163
160
|
onStart: (props) => {
|
|
164
161
|
if (typeof document === 'undefined')
|
|
165
162
|
return;
|
|
163
|
+
const seq = ++slashSeq;
|
|
164
|
+
const listboxId = `sv5ui-slash-listbox-${seq}`;
|
|
165
|
+
const optionIdPrefix = `sv5ui-slash-${seq}-`;
|
|
166
|
+
const editorDom = props.editor.view.dom;
|
|
166
167
|
const container = document.createElement('div');
|
|
167
168
|
container.setAttribute('data-editor-slash-container', '');
|
|
168
169
|
container.style.cssText = 'position:absolute;top:0;left:0;z-index:50;';
|
|
@@ -170,6 +171,8 @@ function buildSuggestionRender() {
|
|
|
170
171
|
const state = $state({
|
|
171
172
|
items: props.items,
|
|
172
173
|
selectedIndex: 0,
|
|
174
|
+
listboxId,
|
|
175
|
+
optionIdPrefix,
|
|
173
176
|
onPick: (i) => {
|
|
174
177
|
const cmd = state.items[i];
|
|
175
178
|
if (!cmd)
|
|
@@ -181,7 +184,26 @@ function buildSuggestionRender() {
|
|
|
181
184
|
target: container,
|
|
182
185
|
props: state
|
|
183
186
|
});
|
|
184
|
-
|
|
187
|
+
editorDom.setAttribute('aria-controls', listboxId);
|
|
188
|
+
editorDom.setAttribute('aria-expanded', 'true');
|
|
189
|
+
const stopActiveDescendant = $effect.root(() => {
|
|
190
|
+
$effect(() => {
|
|
191
|
+
if (state.items.length > 0) {
|
|
192
|
+
editorDom.setAttribute('aria-activedescendant', `${optionIdPrefix}${state.selectedIndex}`);
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
editorDom.removeAttribute('aria-activedescendant');
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
handle = {
|
|
200
|
+
container,
|
|
201
|
+
component,
|
|
202
|
+
state,
|
|
203
|
+
cleanup: null,
|
|
204
|
+
editorDom,
|
|
205
|
+
stopActiveDescendant
|
|
206
|
+
};
|
|
185
207
|
const rect = props.clientRect?.();
|
|
186
208
|
if (rect) {
|
|
187
209
|
const virtualEl = { getBoundingClientRect: () => rect };
|
|
@@ -224,6 +246,12 @@ function buildSuggestionRender() {
|
|
|
224
246
|
if (!handle)
|
|
225
247
|
return;
|
|
226
248
|
handle.cleanup?.();
|
|
249
|
+
handle.stopActiveDescendant?.();
|
|
250
|
+
if (handle.editorDom) {
|
|
251
|
+
handle.editorDom.removeAttribute('aria-controls');
|
|
252
|
+
handle.editorDom.removeAttribute('aria-expanded');
|
|
253
|
+
handle.editorDom.removeAttribute('aria-activedescendant');
|
|
254
|
+
}
|
|
227
255
|
unmount(handle.component);
|
|
228
256
|
handle.container.remove();
|
|
229
257
|
handle = null;
|
|
@@ -262,8 +290,6 @@ export function buildSlashExtension(commands, trigger = '/') {
|
|
|
262
290
|
allowSpaces: false,
|
|
263
291
|
items: ({ query }) => substringFilter(commands, query),
|
|
264
292
|
render: buildSuggestionRender,
|
|
265
|
-
// Tiptap merges `suggestion` shallowly when an extension is .configure()'d,
|
|
266
|
-
// so the command default in addOptions gets overwritten. Include it here.
|
|
267
293
|
command: ({ editor, range, props }) => {
|
|
268
294
|
editor.chain().focus().deleteRange(range).run();
|
|
269
295
|
props.run({ editor });
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { computePosition, flip, shift, offset, autoUpdate } from '@floating-ui/dom';
|
|
2
|
+
let mentionSeq = 0;
|
|
2
3
|
function createPopup() {
|
|
3
4
|
const popup = document.createElement('div');
|
|
4
5
|
popup.setAttribute('data-editor-mention-popup', '');
|
|
@@ -25,6 +26,9 @@ function renderItems(state) {
|
|
|
25
26
|
row.type = 'button';
|
|
26
27
|
row.setAttribute('data-mention-item', '');
|
|
27
28
|
row.setAttribute('data-index', String(i));
|
|
29
|
+
row.setAttribute('role', 'option');
|
|
30
|
+
row.id = `${state.optionIdPrefix}${i}`;
|
|
31
|
+
row.setAttribute('aria-selected', i === state.selectedIndex ? 'true' : 'false');
|
|
28
32
|
row.className = [
|
|
29
33
|
'flex w-full items-center gap-2 px-3 py-1.5 text-sm text-start',
|
|
30
34
|
'hover:bg-surface-container-high',
|
|
@@ -49,6 +53,9 @@ function renderItems(state) {
|
|
|
49
53
|
});
|
|
50
54
|
state.listEl?.appendChild(row);
|
|
51
55
|
});
|
|
56
|
+
if (state.editorDom && state.items.length > 0) {
|
|
57
|
+
state.editorDom.setAttribute('aria-activedescendant', `${state.optionIdPrefix}${state.selectedIndex}`);
|
|
58
|
+
}
|
|
52
59
|
}
|
|
53
60
|
export function buildMentionSuggestion(options) {
|
|
54
61
|
return {
|
|
@@ -66,17 +73,28 @@ export function buildMentionSuggestion(options) {
|
|
|
66
73
|
onStart: (props) => {
|
|
67
74
|
if (typeof document === 'undefined')
|
|
68
75
|
return;
|
|
76
|
+
const seq = ++mentionSeq;
|
|
77
|
+
const listboxId = `sv5ui-mention-listbox-${seq}`;
|
|
78
|
+
const optionIdPrefix = `sv5ui-mention-${seq}-`;
|
|
79
|
+
const editorDom = props.editor.view.dom;
|
|
69
80
|
const popupEl = createPopup();
|
|
70
81
|
const listEl = document.createElement('div');
|
|
71
82
|
listEl.setAttribute('role', 'listbox');
|
|
83
|
+
listEl.id = listboxId;
|
|
84
|
+
listEl.setAttribute('aria-label', 'Mentions');
|
|
72
85
|
popupEl.appendChild(listEl);
|
|
73
86
|
document.body.appendChild(popupEl);
|
|
87
|
+
editorDom.setAttribute('aria-controls', listboxId);
|
|
88
|
+
editorDom.setAttribute('aria-expanded', 'true');
|
|
74
89
|
state = {
|
|
75
90
|
items: props.items,
|
|
76
91
|
selectedIndex: 0,
|
|
77
92
|
popupEl,
|
|
78
93
|
listEl,
|
|
79
94
|
cleanup: null,
|
|
95
|
+
editorDom,
|
|
96
|
+
listboxId,
|
|
97
|
+
optionIdPrefix,
|
|
80
98
|
pickItem: (i) => {
|
|
81
99
|
const it = state?.items[i];
|
|
82
100
|
if (!it)
|
|
@@ -134,6 +152,11 @@ export function buildMentionSuggestion(options) {
|
|
|
134
152
|
onExit: () => {
|
|
135
153
|
state?.cleanup?.();
|
|
136
154
|
state?.popupEl?.remove();
|
|
155
|
+
if (state?.editorDom) {
|
|
156
|
+
state.editorDom.removeAttribute('aria-controls');
|
|
157
|
+
state.editorDom.removeAttribute('aria-expanded');
|
|
158
|
+
state.editorDom.removeAttribute('aria-activedescendant');
|
|
159
|
+
}
|
|
137
160
|
state = null;
|
|
138
161
|
}
|
|
139
162
|
};
|
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// Fallback `run` handlers below (link/image/youtube) use window.prompt because
|
|
3
|
-
// they need to gather a URL. Editor.svelte intercepts these actions and routes
|
|
4
|
-
// them through the EditorUrlPrompt modal instead. The fallbacks only execute
|
|
5
|
-
// when a consumer calls `api.run('link'|'image'|'youtube')` directly — at
|
|
6
|
-
// which point we have no modal mount point, so window.prompt is the simplest
|
|
7
|
-
// non-UI fallback.
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
1
|
function promptForLink(editor) {
|
|
10
2
|
const previous = editor.getAttributes('link').href ?? 'https://';
|
|
11
3
|
const url = typeof window === 'undefined' ? null : window.prompt('URL', previous);
|
|
@@ -195,10 +195,20 @@ export interface EditorProps extends Omit<HTMLAttributes<HTMLElement>, 'class' |
|
|
|
195
195
|
* Bindable content. The runtime type depends on `output`:
|
|
196
196
|
* - `output: 'html'` (default) → `string` of HTML
|
|
197
197
|
* - `output: 'json'` → `EditorJSON` document
|
|
198
|
+
*
|
|
199
|
+
* Security: the serialized value is structurally validated by the editor
|
|
200
|
+
* schema (unknown tags, attributes, and event handlers are dropped), but it
|
|
201
|
+
* is NOT sanitizer-clean. It can still contain arbitrary image `src` values
|
|
202
|
+
* (including `data:` URIs from pasted content). Sanitize the HTML output
|
|
203
|
+
* (e.g. with DOMPurify) before rendering it as raw markup elsewhere.
|
|
198
204
|
*/
|
|
199
205
|
value?: string | EditorJSON;
|
|
200
206
|
/**
|
|
201
207
|
* Serialization format used for `value` and `onValueChange`.
|
|
208
|
+
*
|
|
209
|
+
* Read once when the editor mounts (it also decides whether the Markdown
|
|
210
|
+
* extension is loaded). Changing it on an existing editor has no effect —
|
|
211
|
+
* re-key the component (e.g. `{#key output}`) to switch formats.
|
|
202
212
|
* @default 'html'
|
|
203
213
|
*/
|
|
204
214
|
output?: EditorOutput;
|
|
@@ -257,8 +267,18 @@ export interface EditorProps extends Omit<HTMLAttributes<HTMLElement>, 'class' |
|
|
|
257
267
|
* must return the resolved URL to insert as `<img src=...>`. When
|
|
258
268
|
* omitted and `image` is `true`, images can only be inserted via URL
|
|
259
269
|
* prompt (toolbar).
|
|
270
|
+
*
|
|
271
|
+
* The returned URL is validated before insertion: relative URLs,
|
|
272
|
+
* `http(s)`, and raster `data:image/*` URIs are allowed; `javascript:`,
|
|
273
|
+
* `data:text/*`, and `data:image/svg+xml` are rejected (the image is not
|
|
274
|
+
* inserted and a warning is logged).
|
|
260
275
|
*/
|
|
261
276
|
onImageUpload?: (file: File) => Promise<string>;
|
|
277
|
+
/**
|
|
278
|
+
* Called when `onImageUpload` rejects. When omitted, the error is logged to
|
|
279
|
+
* the console. Use this to surface upload failures to the user.
|
|
280
|
+
*/
|
|
281
|
+
onImageUploadError?: (error: unknown) => void;
|
|
262
282
|
/**
|
|
263
283
|
* Enable tables. Adds the `table` toolbar action which opens a
|
|
264
284
|
* dimension picker (rows × columns) and inserts a new table.
|
|
@@ -52,7 +52,6 @@ export declare const editorVariants: import("tailwind-variants").TVReturnType<{
|
|
|
52
52
|
}, {
|
|
53
53
|
root: string[];
|
|
54
54
|
toolbar: string[];
|
|
55
|
-
toolbarGroup: string;
|
|
56
55
|
toolbarButton: string[];
|
|
57
56
|
toolbarSeparator: string;
|
|
58
57
|
content: string[];
|
|
@@ -112,7 +111,6 @@ export declare const editorVariants: import("tailwind-variants").TVReturnType<{
|
|
|
112
111
|
}, {
|
|
113
112
|
root: string[];
|
|
114
113
|
toolbar: string[];
|
|
115
|
-
toolbarGroup: string;
|
|
116
114
|
toolbarButton: string[];
|
|
117
115
|
toolbarSeparator: string;
|
|
118
116
|
content: string[];
|
|
@@ -172,7 +170,6 @@ export declare const editorVariants: import("tailwind-variants").TVReturnType<{
|
|
|
172
170
|
}, {
|
|
173
171
|
root: string[];
|
|
174
172
|
toolbar: string[];
|
|
175
|
-
toolbarGroup: string;
|
|
176
173
|
toolbarButton: string[];
|
|
177
174
|
toolbarSeparator: string;
|
|
178
175
|
content: string[];
|
|
@@ -236,7 +233,6 @@ export declare const editorDefaults: {
|
|
|
236
233
|
}, {
|
|
237
234
|
root: string[];
|
|
238
235
|
toolbar: string[];
|
|
239
|
-
toolbarGroup: string;
|
|
240
236
|
toolbarButton: string[];
|
|
241
237
|
toolbarSeparator: string;
|
|
242
238
|
content: string[];
|
|
@@ -296,7 +292,6 @@ export declare const editorDefaults: {
|
|
|
296
292
|
}, {
|
|
297
293
|
root: string[];
|
|
298
294
|
toolbar: string[];
|
|
299
|
-
toolbarGroup: string;
|
|
300
295
|
toolbarButton: string[];
|
|
301
296
|
toolbarSeparator: string;
|
|
302
297
|
content: string[];
|
|
@@ -13,7 +13,6 @@ export const editorVariants = tv({
|
|
|
13
13
|
'bg-surface-container-low',
|
|
14
14
|
'rounded-t-lg p-1.5'
|
|
15
15
|
],
|
|
16
|
-
toolbarGroup: 'flex items-center gap-0.5',
|
|
17
16
|
toolbarButton: [
|
|
18
17
|
'inline-flex items-center justify-center rounded text-on-surface-variant',
|
|
19
18
|
'hover:bg-surface-container-high hover:text-on-surface',
|
|
@@ -25,7 +24,6 @@ export const editorVariants = tv({
|
|
|
25
24
|
toolbarSeparator: 'mx-1 h-5 w-px shrink-0 bg-outline-variant',
|
|
26
25
|
content: [
|
|
27
26
|
'text-on-surface',
|
|
28
|
-
// ----- Inner ProseMirror element (the actual contenteditable) -----
|
|
29
27
|
'[&_.ProseMirror]:outline-none',
|
|
30
28
|
'[&_.ProseMirror]:focus:outline-none',
|
|
31
29
|
'[&_.ProseMirror]:focus-visible:outline-none',
|
|
@@ -33,13 +31,11 @@ export const editorVariants = tv({
|
|
|
33
31
|
'[&_.ProseMirror]:min-h-32',
|
|
34
32
|
'[&_.ProseMirror]:whitespace-pre-wrap',
|
|
35
33
|
'[&_.ProseMirror]:break-words',
|
|
36
|
-
// ----- Placeholder (when editor is empty) -----
|
|
37
34
|
'[&_.is-editor-empty]:before:content-[attr(data-placeholder)]',
|
|
38
35
|
'[&_.is-editor-empty]:before:text-on-surface-variant/60',
|
|
39
36
|
'[&_.is-editor-empty]:before:pointer-events-none',
|
|
40
37
|
'[&_.is-editor-empty]:before:float-left',
|
|
41
38
|
'[&_.is-editor-empty]:before:h-0',
|
|
42
|
-
// ----- Content typography (lightweight in-house prose) -----
|
|
43
39
|
'[&_p]:my-2 [&_p:first-child]:mt-0 [&_p:last-child]:mb-0',
|
|
44
40
|
'[&_h1]:text-2xl [&_h1]:font-bold [&_h1]:my-3',
|
|
45
41
|
'[&_h2]:text-xl [&_h2]:font-bold [&_h2]:my-3',
|
|
@@ -56,17 +52,13 @@ export const editorVariants = tv({
|
|
|
56
52
|
'[&_hr]:border-outline-variant [&_hr]:my-4',
|
|
57
53
|
'[&_strong]:font-semibold',
|
|
58
54
|
'[&_em]:italic',
|
|
59
|
-
// ----- Image -----
|
|
60
55
|
'[&_img]:max-w-full [&_img]:h-auto [&_img]:rounded-md [&_img]:my-2',
|
|
61
|
-
// ----- YouTube embed (responsive 16:9) -----
|
|
62
56
|
'[&_iframe[src*="youtube"]]:aspect-video [&_iframe[src*="youtube"]]:w-full [&_iframe[src*="youtube"]]:rounded-md [&_iframe[src*="youtube"]]:my-3 [&_iframe[src*="youtube"]]:border-0',
|
|
63
|
-
// ----- Mention chip -----
|
|
64
57
|
'[&_.sv5ui-editor-mention]:inline-flex [&_.sv5ui-editor-mention]:items-center',
|
|
65
58
|
'[&_.sv5ui-editor-mention]:rounded [&_.sv5ui-editor-mention]:bg-primary-container/60',
|
|
66
59
|
'[&_.sv5ui-editor-mention]:text-on-primary-container',
|
|
67
60
|
'[&_.sv5ui-editor-mention]:px-1.5 [&_.sv5ui-editor-mention]:py-0.5',
|
|
68
61
|
'[&_.sv5ui-editor-mention]:text-sm [&_.sv5ui-editor-mention]:font-medium',
|
|
69
|
-
// ----- Table -----
|
|
70
62
|
'[&_.tableWrapper]:my-3 [&_.tableWrapper]:overflow-x-auto',
|
|
71
63
|
'[&_table]:w-full [&_table]:border-collapse [&_table]:table-fixed',
|
|
72
64
|
'[&_table]:border [&_table]:border-outline-variant [&_table]:rounded-md',
|
|
@@ -77,12 +69,10 @@ export const editorVariants = tv({
|
|
|
77
69
|
'[&_td]:border [&_td]:border-outline-variant',
|
|
78
70
|
'[&_td]:px-2 [&_td]:py-1.5 [&_td]:align-top',
|
|
79
71
|
'[&_td]:relative [&_td]:min-w-16',
|
|
80
|
-
// Tiptap selectedCell highlight + column resize handle
|
|
81
72
|
'[&_.selectedCell]:bg-primary/10',
|
|
82
73
|
'[&_.column-resize-handle]:absolute [&_.column-resize-handle]:top-0 [&_.column-resize-handle]:bottom-[-2px]',
|
|
83
74
|
'[&_.column-resize-handle]:-right-0.5 [&_.column-resize-handle]:w-1',
|
|
84
75
|
'[&_.column-resize-handle]:bg-primary/40 [&_.column-resize-handle]:pointer-events-none',
|
|
85
|
-
// ----- Selection styling -----
|
|
86
76
|
'[&_::selection]:bg-primary/20'
|
|
87
77
|
],
|
|
88
78
|
footer: [
|
|
@@ -94,11 +84,6 @@ export const editorVariants = tv({
|
|
|
94
84
|
],
|
|
95
85
|
countLabel: 'tabular-nums',
|
|
96
86
|
bubbleMenu: [
|
|
97
|
-
// ⚠️ Tiptap's BubbleMenu only sets `visibility:hidden` initially,
|
|
98
|
-
// leaving the element in normal flow (taking up space below the editor)
|
|
99
|
-
// until the user first selects text. Setting `position:absolute` upfront
|
|
100
|
-
// keeps it out of flow from mount — extension's Floating UI positioning
|
|
101
|
-
// then overrides left/top inline on selection.
|
|
102
87
|
'absolute top-0 left-0 z-50 invisible',
|
|
103
88
|
'flex items-center gap-0.5 p-1',
|
|
104
89
|
'rounded-lg border border-outline-variant bg-surface',
|
package/dist/Editor/index.d.ts
CHANGED
|
@@ -29,10 +29,12 @@
|
|
|
29
29
|
*
|
|
30
30
|
* (`npm add` / `yarn add` work equally well.)
|
|
31
31
|
*
|
|
32
|
-
* All 18 packages
|
|
33
|
-
* features
|
|
34
|
-
*
|
|
35
|
-
*
|
|
32
|
+
* All 18 packages must be installed even if you only enable a subset of
|
|
33
|
+
* features — missing peers fail at runtime with module-resolution errors.
|
|
34
|
+
* Most extensions are imported eagerly, but the heaviest optional ones load
|
|
35
|
+
* on demand: `tiptap-markdown` (only with `output="markdown"`) and the table
|
|
36
|
+
* packages (only with `tables`) are pulled in via dynamic `import()`, so your
|
|
37
|
+
* bundler code-splits them out of the base editor chunk when unused.
|
|
36
38
|
*
|
|
37
39
|
* ## Usage
|
|
38
40
|
*
|
package/dist/Editor/index.js
CHANGED
|
@@ -29,10 +29,12 @@
|
|
|
29
29
|
*
|
|
30
30
|
* (`npm add` / `yarn add` work equally well.)
|
|
31
31
|
*
|
|
32
|
-
* All 18 packages
|
|
33
|
-
* features
|
|
34
|
-
*
|
|
35
|
-
*
|
|
32
|
+
* All 18 packages must be installed even if you only enable a subset of
|
|
33
|
+
* features — missing peers fail at runtime with module-resolution errors.
|
|
34
|
+
* Most extensions are imported eagerly, but the heaviest optional ones load
|
|
35
|
+
* on demand: `tiptap-markdown` (only with `output="markdown"`) and the table
|
|
36
|
+
* packages (only with `tables`) are pulled in via dynamic `import()`, so your
|
|
37
|
+
* bundler code-splits them out of the base editor chunk when unused.
|
|
36
38
|
*
|
|
37
39
|
* ## Usage
|
|
38
40
|
*
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { IconProps as IconifyProps } from '@iconify/svelte';
|
|
2
|
+
import type { SVGAttributes } from 'svelte/elements';
|
|
2
3
|
import type { ClassNameValue } from 'tailwind-merge';
|
|
3
|
-
export interface IconProps extends Omit<IconifyProps, 'icon' | 'width' | 'height' | 'rotate' | 'flip' | 'class'> {
|
|
4
|
+
export interface IconProps extends Omit<IconifyProps, 'icon' | 'width' | 'height' | 'rotate' | 'flip' | 'class'>, Pick<SVGAttributes<SVGSVGElement>, 'role' | 'tabindex' | 'aria-label' | 'aria-labelledby' | 'aria-describedby' | 'aria-hidden' | 'onclick' | 'onkeydown' | 'onmouseenter' | 'onmouseleave' | 'onfocus' | 'onblur'> {
|
|
5
|
+
/** Custom data attributes are forwarded to the rendered `<svg>`. */
|
|
6
|
+
[key: `data-${string}`]: string | number | boolean | null | undefined;
|
|
4
7
|
/**
|
|
5
8
|
* Icon name in Iconify format: "collection:icon-name"
|
|
6
9
|
* @example "lucide:home", "mdi:account", "heroicons:star"
|
package/dist/Input/Input.svelte
CHANGED
|
@@ -94,19 +94,14 @@
|
|
|
94
94
|
const resolvedId = $derived(id ?? formFieldContext?.ariaId)
|
|
95
95
|
const resolvedName = $derived(name ?? formFieldContext?.name)
|
|
96
96
|
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
)
|
|
100
|
-
const isTrailing = $derived((!!icon && trailing) || (loading && trailing) || !!trailingIcon)
|
|
97
|
+
const loadingLeading = $derived(loading && !trailing)
|
|
98
|
+
const loadingTrailing = $derived(loading && trailing)
|
|
101
99
|
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
)
|
|
107
|
-
const trailingIconName = $derived(
|
|
108
|
-
loading && isTrailing ? loadingIcon : trailingIcon || (trailing ? icon : undefined)
|
|
109
|
-
)
|
|
100
|
+
const isLeading = $derived((!!icon && !trailing) || !!leadingIcon || !!avatar || loadingLeading)
|
|
101
|
+
const isTrailing = $derived((!!icon && trailing) || !!trailingIcon || loadingTrailing)
|
|
102
|
+
|
|
103
|
+
const leadingIconName = $derived(leadingIcon || (!!icon && !trailing ? icon : undefined))
|
|
104
|
+
const trailingIconName = $derived(trailingIcon || (!!icon && trailing ? icon : undefined))
|
|
110
105
|
|
|
111
106
|
const ariaDescribedBy = $derived(
|
|
112
107
|
!formFieldContext
|
|
@@ -123,7 +118,6 @@
|
|
|
123
118
|
size: resolvedSize,
|
|
124
119
|
leading: isLeading,
|
|
125
120
|
trailing: isTrailing,
|
|
126
|
-
loading,
|
|
127
121
|
highlight: resolvedHighlight
|
|
128
122
|
})
|
|
129
123
|
)
|
|
@@ -154,14 +148,20 @@
|
|
|
154
148
|
<span class={classes.leading}>
|
|
155
149
|
{@render leadingSlot()}
|
|
156
150
|
</span>
|
|
157
|
-
{:else if
|
|
151
|
+
{:else if loadingLeading}
|
|
158
152
|
<span class={classes.leading}>
|
|
159
|
-
<
|
|
153
|
+
<span class="inline-flex animate-spin">
|
|
154
|
+
<Icon name={loadingIcon} class={classes.leadingIcon} />
|
|
155
|
+
</span>
|
|
160
156
|
</span>
|
|
161
157
|
{:else if avatar}
|
|
162
158
|
<span class={classes.leading}>
|
|
163
159
|
<Avatar {...avatar} size={classes.leadingAvatarSize} class={classes.leadingAvatar} />
|
|
164
160
|
</span>
|
|
161
|
+
{:else if leadingIconName}
|
|
162
|
+
<span class={classes.leading}>
|
|
163
|
+
<Icon name={leadingIconName} class={classes.leadingIcon} />
|
|
164
|
+
</span>
|
|
165
165
|
{/if}
|
|
166
166
|
|
|
167
167
|
<input
|
|
@@ -185,7 +185,13 @@
|
|
|
185
185
|
<span class={classes.trailing}>
|
|
186
186
|
{@render trailingSlot()}
|
|
187
187
|
</span>
|
|
188
|
-
{:else if
|
|
188
|
+
{:else if loadingTrailing}
|
|
189
|
+
<span class={classes.trailing}>
|
|
190
|
+
<span class="inline-flex animate-spin">
|
|
191
|
+
<Icon name={loadingIcon} class={classes.trailingIcon} />
|
|
192
|
+
</span>
|
|
193
|
+
</span>
|
|
194
|
+
{:else if trailingIconName}
|
|
189
195
|
<span class={classes.trailing}>
|
|
190
196
|
<Icon name={trailingIconName} class={classes.trailingIcon} />
|
|
191
197
|
</span>
|
package/dist/Input/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { default as Input } from './Input.svelte';
|
|
2
|
-
export type { InputProps } from './input.types.js';
|
|
2
|
+
export type { InputProps, InputValue } from './input.types.js';
|