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
|
@@ -53,7 +53,18 @@
|
|
|
53
53
|
|
|
54
54
|
type SlotName = (typeof slotNames)[number]
|
|
55
55
|
|
|
56
|
+
const itemDefaults = $derived.by(() => {
|
|
57
|
+
const result = {} as Record<SlotName, string>
|
|
58
|
+
for (const slot of slotNames) {
|
|
59
|
+
result[slot] = variantSlots[slot]({ class: [config.slots[slot], ui?.[slot]] })
|
|
60
|
+
}
|
|
61
|
+
return result
|
|
62
|
+
})
|
|
63
|
+
|
|
56
64
|
function getItemClasses(item: AccordionItem) {
|
|
65
|
+
if (!item.ui && item.class === undefined && item.disabled === undefined) {
|
|
66
|
+
return itemDefaults
|
|
67
|
+
}
|
|
57
68
|
const result = {} as Record<SlotName, string>
|
|
58
69
|
for (const slot of slotNames) {
|
|
59
70
|
const baseClass = [
|
|
@@ -43,7 +43,7 @@ export type AlertProps = Omit<HTMLAttributes<HTMLDivElement>, 'class' | 'title'>
|
|
|
43
43
|
color?: NonNullable<AlertVariantProps['color']>;
|
|
44
44
|
/**
|
|
45
45
|
* The visual style variant.
|
|
46
|
-
* @default '
|
|
46
|
+
* @default 'solid'
|
|
47
47
|
*/
|
|
48
48
|
variant?: NonNullable<AlertVariantProps['variant']>;
|
|
49
49
|
/**
|
|
@@ -46,9 +46,11 @@
|
|
|
46
46
|
}
|
|
47
47
|
})
|
|
48
48
|
|
|
49
|
-
const visibleAvatars = $derived(
|
|
50
|
-
!avatars
|
|
51
|
-
|
|
49
|
+
const visibleAvatars = $derived.by(() => {
|
|
50
|
+
if (!avatars) return []
|
|
51
|
+
const shown = max && max > 0 ? avatars.slice(0, max) : avatars
|
|
52
|
+
return [...shown].reverse()
|
|
53
|
+
})
|
|
52
54
|
|
|
53
55
|
const overflowCount = $derived(
|
|
54
56
|
avatars && max && max > 0 ? Math.max(0, avatars.length - max) : 0
|
|
@@ -17,7 +17,7 @@ export type BadgeProps = Omit<HTMLAttributes<HTMLSpanElement>, 'class'> & {
|
|
|
17
17
|
* Override styles for specific badge slots.
|
|
18
18
|
* Available slots: base, label, leadingIcon, trailingIcon, leadingAvatar.
|
|
19
19
|
*/
|
|
20
|
-
ui?: Partial<Record<BadgeSlots, ClassNameValue>>;
|
|
20
|
+
ui?: Partial<Record<Exclude<BadgeSlots, 'leadingAvatarSize'>, ClassNameValue>>;
|
|
21
21
|
/**
|
|
22
22
|
* Sets the text content displayed inside the badge.
|
|
23
23
|
*/
|
|
@@ -76,13 +76,14 @@
|
|
|
76
76
|
const isLeading = $derived((!!icon && !trailing) || (isLoading && !trailing) || !!leadingIcon)
|
|
77
77
|
const isTrailing = $derived((!!icon && trailing) || (isLoading && trailing) || !!trailingIcon)
|
|
78
78
|
|
|
79
|
+
const spinLeading = $derived(isLoading && !trailing)
|
|
80
|
+
const spinTrailing = $derived(isLoading && trailing)
|
|
81
|
+
|
|
79
82
|
const leadingIconName = $derived(
|
|
80
|
-
|
|
81
|
-
? loadingIcon
|
|
82
|
-
: leadingIcon || (isLeading && !trailing ? icon : undefined)
|
|
83
|
+
spinLeading ? loadingIcon : leadingIcon || (!trailing ? icon : undefined)
|
|
83
84
|
)
|
|
84
85
|
const trailingIconName = $derived(
|
|
85
|
-
|
|
86
|
+
spinTrailing ? loadingIcon : trailingIcon || (trailing ? icon : undefined)
|
|
86
87
|
)
|
|
87
88
|
|
|
88
89
|
const resolvedColor = $derived(active && activeColor ? activeColor : color)
|
|
@@ -96,8 +97,8 @@
|
|
|
96
97
|
block,
|
|
97
98
|
square: isIconOnly,
|
|
98
99
|
loading: isLoading,
|
|
99
|
-
leading:
|
|
100
|
-
trailing:
|
|
100
|
+
leading: spinLeading,
|
|
101
|
+
trailing: spinTrailing
|
|
101
102
|
})
|
|
102
103
|
return {
|
|
103
104
|
base: slots.base({ class: [config.slots.base, fieldGroupClass, className, ui?.base] }),
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
|
-
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
|
+
import type { HTMLAttributes, HTMLButtonAttributes, HTMLAnchorAttributes } from 'svelte/elements';
|
|
3
3
|
import type { ClassNameValue } from 'tailwind-merge';
|
|
4
4
|
import type { ButtonVariantProps, ButtonSlots } from './button.variants.js';
|
|
5
5
|
import type { AvatarProps } from '../Avatar/avatar.types.js';
|
|
6
|
-
export type ButtonProps = Omit<HTMLAttributes<HTMLElement>, 'class' | 'color'> & {
|
|
6
|
+
export type ButtonProps = Omit<HTMLAttributes<HTMLElement>, 'class' | 'color'> & Pick<HTMLButtonAttributes, 'name' | 'value' | 'form' | 'formaction' | 'formenctype' | 'formmethod' | 'formnovalidate' | 'formtarget' | 'popovertarget' | 'popovertargetaction'> & Pick<HTMLAnchorAttributes, 'download' | 'hreflang' | 'ping' | 'media' | 'referrerpolicy'> & {
|
|
7
7
|
/**
|
|
8
8
|
* Bindable reference to the root DOM element.
|
|
9
9
|
*/
|
|
@@ -147,5 +147,5 @@ export type ButtonProps = Omit<HTMLAttributes<HTMLElement>, 'class' | 'color'> &
|
|
|
147
147
|
/**
|
|
148
148
|
* Override styles for specific button slots.
|
|
149
149
|
*/
|
|
150
|
-
ui?: Partial<Record<ButtonSlots, ClassNameValue>>;
|
|
150
|
+
ui?: Partial<Record<Exclude<ButtonSlots, 'leadingAvatarSize'>, ClassNameValue>>;
|
|
151
151
|
};
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import { calendarVariants, calendarDefaults } from './calendar.variants.js'
|
|
15
15
|
import { getComponentConfig } from '../config.js'
|
|
16
16
|
import Icon from '../Icon/Icon.svelte'
|
|
17
|
-
import type
|
|
17
|
+
import { type DateValue, today, getLocalTimeZone } from '@internationalized/date'
|
|
18
18
|
import type { Month } from 'bits-ui'
|
|
19
19
|
import { useFormField, useFormFieldEmit } from '../hooks/useFormField.svelte.js'
|
|
20
20
|
|
|
@@ -63,6 +63,19 @@
|
|
|
63
63
|
...restProps
|
|
64
64
|
}: Props = $props()
|
|
65
65
|
|
|
66
|
+
function firstDateOf(val: unknown): DateValue | undefined {
|
|
67
|
+
if (!val) return undefined
|
|
68
|
+
if (Array.isArray(val)) return val[0] as DateValue | undefined
|
|
69
|
+
if (typeof val === 'object' && 'start' in val) {
|
|
70
|
+
return (val as { start?: DateValue }).start
|
|
71
|
+
}
|
|
72
|
+
return val as DateValue
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (placeholder === undefined) {
|
|
76
|
+
placeholder = firstDateOf(value) ?? today(getLocalTimeZone())
|
|
77
|
+
}
|
|
78
|
+
|
|
66
79
|
const formFieldContext = useFormField()
|
|
67
80
|
const emit = useFormFieldEmit()
|
|
68
81
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
2
|
import type { ClassNameValue } from 'tailwind-merge';
|
|
3
|
-
import type { CollapsibleRootPropsWithoutHTML } from 'bits-ui';
|
|
3
|
+
import type { CollapsibleRootProps, CollapsibleRootPropsWithoutHTML } from 'bits-ui';
|
|
4
4
|
import type { CollapsibleSlots } from './collapsible.variants.js';
|
|
5
5
|
/**
|
|
6
6
|
* Props for the Collapsible component.
|
|
@@ -21,7 +21,9 @@ import type { CollapsibleSlots } from './collapsible.variants.js';
|
|
|
21
21
|
*
|
|
22
22
|
* @see https://bits-ui.com/docs/components/collapsible
|
|
23
23
|
*/
|
|
24
|
-
export interface CollapsibleProps extends Pick<CollapsibleRootPropsWithoutHTML, 'open' | 'onOpenChange' | 'onOpenChangeComplete' | 'disabled'> {
|
|
24
|
+
export interface CollapsibleProps extends Pick<CollapsibleRootPropsWithoutHTML, 'open' | 'onOpenChange' | 'onOpenChangeComplete' | 'disabled'>, Pick<CollapsibleRootProps, 'id' | 'style' | 'title' | 'role' | 'tabindex' | 'aria-label' | 'aria-labelledby' | 'aria-describedby' | 'onclick' | 'onkeydown' | 'onmouseenter' | 'onmouseleave' | 'onfocus' | 'onblur'> {
|
|
25
|
+
/** Custom data attributes are forwarded to the root element. */
|
|
26
|
+
[key: `data-${string}`]: string | number | boolean | null | undefined;
|
|
25
27
|
/**
|
|
26
28
|
* Bindable reference to the root DOM element.
|
|
27
29
|
*/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
2
|
import type { ClassNameValue } from 'tailwind-merge';
|
|
3
|
-
import type { CommandRootPropsWithoutHTML } from 'bits-ui';
|
|
3
|
+
import type { CommandRootProps, CommandRootPropsWithoutHTML } from 'bits-ui';
|
|
4
4
|
import type { CommandSlots, CommandVariantProps } from './command.variants.js';
|
|
5
5
|
/**
|
|
6
6
|
* Configuration for an individual command item.
|
|
@@ -57,7 +57,9 @@ export interface CommandItemSlotProps {
|
|
|
57
57
|
*
|
|
58
58
|
* @see https://bits-ui.com/docs/components/command
|
|
59
59
|
*/
|
|
60
|
-
export interface CommandProps extends Pick<CommandRootPropsWithoutHTML, 'value' | 'onValueChange' | 'filter' | 'shouldFilter' | 'loop' | 'vimBindings' | 'label'> {
|
|
60
|
+
export interface CommandProps extends Pick<CommandRootPropsWithoutHTML, 'value' | 'onValueChange' | 'filter' | 'shouldFilter' | 'loop' | 'vimBindings' | 'label'>, Pick<CommandRootProps, 'id'> {
|
|
61
|
+
/** Custom data attributes are forwarded to the root element. */
|
|
62
|
+
[key: `data-${string}`]: string | number | boolean | null | undefined;
|
|
61
63
|
/** Bindable reference to the root DOM element. */
|
|
62
64
|
ref?: HTMLElement | null;
|
|
63
65
|
/** Array of grouped command items. */
|
package/dist/Command/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { default as Command } from './Command.svelte';
|
|
2
|
-
export type { CommandProps } from './command.types.js';
|
|
2
|
+
export type { CommandProps, CommandGroup, CommandItem } from './command.types.js';
|
|
@@ -44,7 +44,8 @@
|
|
|
44
44
|
titleSlot,
|
|
45
45
|
descriptionSlot,
|
|
46
46
|
body: bodySlot,
|
|
47
|
-
footer: footerSlot
|
|
47
|
+
footer: footerSlot,
|
|
48
|
+
...rest
|
|
48
49
|
}: Props = $props()
|
|
49
50
|
|
|
50
51
|
const hasTitle = $derived(!!title || !!titleSlot)
|
|
@@ -81,6 +82,7 @@
|
|
|
81
82
|
|
|
82
83
|
const rootProps = $derived.by(() => {
|
|
83
84
|
const base = {
|
|
85
|
+
...rest,
|
|
84
86
|
open,
|
|
85
87
|
onOpenChange: handleOpenChange,
|
|
86
88
|
direction,
|
|
@@ -175,8 +177,10 @@
|
|
|
175
177
|
|
|
176
178
|
{#snippet drawerBody()}
|
|
177
179
|
{#if children}
|
|
178
|
-
<Drawer.Trigger
|
|
179
|
-
{
|
|
180
|
+
<Drawer.Trigger>
|
|
181
|
+
{#snippet child({ props })}
|
|
182
|
+
{@render children({ props })}
|
|
183
|
+
{/snippet}
|
|
180
184
|
</Drawer.Trigger>
|
|
181
185
|
{/if}
|
|
182
186
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
2
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
3
|
+
$$bindings?: Bindings;
|
|
4
|
+
} & Exports;
|
|
5
|
+
(internal: unknown, props: {
|
|
6
|
+
$$events?: Events;
|
|
7
|
+
$$slots?: Slots;
|
|
8
|
+
}): Exports & {
|
|
9
|
+
$set?: any;
|
|
10
|
+
$on?: any;
|
|
11
|
+
};
|
|
12
|
+
z_$$bindings?: Bindings;
|
|
13
|
+
}
|
|
14
|
+
declare const DrawerTriggerTestWrapper: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
|
15
|
+
[evt: string]: CustomEvent<any>;
|
|
16
|
+
}, {}, {}, string>;
|
|
17
|
+
type DrawerTriggerTestWrapper = InstanceType<typeof DrawerTriggerTestWrapper>;
|
|
18
|
+
export default DrawerTriggerTestWrapper;
|
|
@@ -45,9 +45,20 @@ export type DrawerProps = VaulRootProps & {
|
|
|
45
45
|
*/
|
|
46
46
|
class?: ClassNameValue;
|
|
47
47
|
/**
|
|
48
|
-
*
|
|
48
|
+
* Trigger content. Spread the provided `props` onto your own focusable
|
|
49
|
+
* element (e.g. a `<Button>`) so the drawer's trigger ARIA and event
|
|
50
|
+
* handlers land on the real control instead of a nested wrapper button.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```svelte
|
|
54
|
+
* {#snippet children({ props })}
|
|
55
|
+
* <Button {...props}>Open</Button>
|
|
56
|
+
* {/snippet}
|
|
57
|
+
* ```
|
|
49
58
|
*/
|
|
50
|
-
children?: Snippet
|
|
59
|
+
children?: Snippet<[{
|
|
60
|
+
props: Record<string, unknown>;
|
|
61
|
+
}]>;
|
|
51
62
|
/**
|
|
52
63
|
* Custom content slot (replaces default layout with header/body/footer).
|
|
53
64
|
*/
|
|
@@ -348,9 +348,7 @@
|
|
|
348
348
|
{#if children}
|
|
349
349
|
<DropdownMenu.Trigger>
|
|
350
350
|
{#snippet child({ props })}
|
|
351
|
-
|
|
352
|
-
{@render children({ open })}
|
|
353
|
-
</span>
|
|
351
|
+
{@render children({ open, props })}
|
|
354
352
|
{/snippet}
|
|
355
353
|
</DropdownMenu.Trigger>
|
|
356
354
|
{/if}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import DropdownMenu from './DropdownMenu.svelte'
|
|
3
|
+
import type { DropdownMenuItem } from './dropdown-menu.types.js'
|
|
4
|
+
|
|
5
|
+
let { items = [] }: { items?: DropdownMenuItem[] } = $props()
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<DropdownMenu {items}>
|
|
9
|
+
{#snippet children({ open, props })}
|
|
10
|
+
<button data-testid="trigger" data-open={open} {...props}>Open</button>
|
|
11
|
+
{/snippet}
|
|
12
|
+
</DropdownMenu>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { DropdownMenuItem } from './dropdown-menu.types.js';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
items?: DropdownMenuItem[];
|
|
4
|
+
};
|
|
5
|
+
declare const DropdownMenuTriggerTestWrapper: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
6
|
+
type DropdownMenuTriggerTestWrapper = ReturnType<typeof DropdownMenuTriggerTestWrapper>;
|
|
7
|
+
export default DropdownMenuTriggerTestWrapper;
|
|
@@ -129,10 +129,6 @@ export type DropdownMenuItem = DropdownMenuItemAction | DropdownMenuItemCheckbox
|
|
|
129
129
|
* Configuration for a radio group within the dropdown menu.
|
|
130
130
|
*/
|
|
131
131
|
export interface DropdownMenuRadioGroup {
|
|
132
|
-
/**
|
|
133
|
-
* Unique identifier for the radio group.
|
|
134
|
-
*/
|
|
135
|
-
name: string;
|
|
136
132
|
/**
|
|
137
133
|
* Currently selected value in the group.
|
|
138
134
|
*/
|
|
@@ -178,8 +174,9 @@ export interface DropdownMenuProps extends RootProps, ContentProps {
|
|
|
178
174
|
*/
|
|
179
175
|
items?: DropdownMenuItem[];
|
|
180
176
|
/**
|
|
181
|
-
* Radio
|
|
182
|
-
*
|
|
177
|
+
* Radio group configuration for managing radio item selections.
|
|
178
|
+
* Only the first entry is used — all `type: 'radio'` items belong to it.
|
|
179
|
+
* @example [{ value: 'dark', onValueChange: (v) => theme = v }]
|
|
183
180
|
*/
|
|
184
181
|
radioGroups?: DropdownMenuRadioGroup[];
|
|
185
182
|
/**
|
|
@@ -214,7 +211,9 @@ export interface DropdownMenuProps extends RootProps, ContentProps {
|
|
|
214
211
|
*/
|
|
215
212
|
size?: NonNullable<DropdownMenuVariantProps['size']>;
|
|
216
213
|
/**
|
|
217
|
-
* Additional CSS class for the
|
|
214
|
+
* Additional CSS class for the dropdown content.
|
|
215
|
+
* Applied only when no trigger `children` are provided (style your own
|
|
216
|
+
* trigger element directly otherwise).
|
|
218
217
|
*/
|
|
219
218
|
class?: ClassNameValue;
|
|
220
219
|
/**
|
|
@@ -222,11 +221,20 @@ export interface DropdownMenuProps extends RootProps, ContentProps {
|
|
|
222
221
|
*/
|
|
223
222
|
ui?: Partial<Record<DropdownMenuSlots, ClassNameValue>>;
|
|
224
223
|
/**
|
|
225
|
-
*
|
|
226
|
-
*
|
|
224
|
+
* Trigger content. Spread the provided `props` onto your own focusable
|
|
225
|
+
* element (e.g. a `<Button>`) so the trigger's ARIA and event handlers land
|
|
226
|
+
* on the real control.
|
|
227
|
+
*
|
|
228
|
+
* @example
|
|
229
|
+
* ```svelte
|
|
230
|
+
* {#snippet children({ open, props })}
|
|
231
|
+
* <Button {...props}>{open ? 'Close' : 'Open'}</Button>
|
|
232
|
+
* {/snippet}
|
|
233
|
+
* ```
|
|
227
234
|
*/
|
|
228
235
|
children?: Snippet<[{
|
|
229
236
|
open: boolean;
|
|
237
|
+
props: Record<string, unknown>;
|
|
230
238
|
}]>;
|
|
231
239
|
/**
|
|
232
240
|
* Custom content to render at the top of the dropdown menu.
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
</script>
|
|
6
6
|
|
|
7
7
|
<script lang="ts">
|
|
8
|
-
import { Editor } from '@tiptap/core'
|
|
8
|
+
import { Editor, type AnyExtension } from '@tiptap/core'
|
|
9
9
|
import BubbleMenuExt from '@tiptap/extension-bubble-menu'
|
|
10
10
|
import { untrack } from 'svelte'
|
|
11
11
|
import { editorVariants, editorDefaults } from './editor.variants.js'
|
|
@@ -24,7 +24,12 @@
|
|
|
24
24
|
import Icon from '../Icon/Icon.svelte'
|
|
25
25
|
import Tooltip from '../Tooltip/Tooltip.svelte'
|
|
26
26
|
import EditorUrlPrompt from './EditorUrlPrompt.svelte'
|
|
27
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
httpUrlSchema,
|
|
29
|
+
youtubeUrlSchema,
|
|
30
|
+
isSafeImageSrc,
|
|
31
|
+
type UrlSchema
|
|
32
|
+
} from './editor.schemas.js'
|
|
28
33
|
|
|
29
34
|
const config = getComponentConfig('editor', editorDefaults)
|
|
30
35
|
|
|
@@ -53,6 +58,7 @@
|
|
|
53
58
|
markdownAllowHtml = false,
|
|
54
59
|
image = false,
|
|
55
60
|
onImageUpload,
|
|
61
|
+
onImageUploadError,
|
|
56
62
|
tables = false,
|
|
57
63
|
onMention,
|
|
58
64
|
mentionTrigger = '@',
|
|
@@ -77,6 +83,14 @@
|
|
|
77
83
|
const formFieldContext = useFormField()
|
|
78
84
|
const emit = useFormFieldEmit()
|
|
79
85
|
|
|
86
|
+
const resolvedOutput = untrack(() => output)
|
|
87
|
+
|
|
88
|
+
function getMarkdownStorage(ed: Editor): { getMarkdown?: () => string } | undefined {
|
|
89
|
+
return (ed.storage as unknown as Record<string, unknown>).markdown as
|
|
90
|
+
| { getMarkdown?: () => string }
|
|
91
|
+
| undefined
|
|
92
|
+
}
|
|
93
|
+
|
|
80
94
|
const hasError = $derived(
|
|
81
95
|
formFieldContext?.error !== undefined && formFieldContext?.error !== false
|
|
82
96
|
)
|
|
@@ -143,11 +157,9 @@
|
|
|
143
157
|
}
|
|
144
158
|
|
|
145
159
|
function serialize(ed: Editor): string | EditorJSON {
|
|
146
|
-
if (
|
|
147
|
-
if (
|
|
148
|
-
const md = (ed
|
|
149
|
-
| { getMarkdown?: () => string }
|
|
150
|
-
| undefined
|
|
160
|
+
if (resolvedOutput === 'json') return ed.getJSON() as EditorJSON
|
|
161
|
+
if (resolvedOutput === 'markdown') {
|
|
162
|
+
const md = getMarkdownStorage(ed)
|
|
151
163
|
if (md && typeof md.getMarkdown === 'function') {
|
|
152
164
|
return md.getMarkdown()
|
|
153
165
|
}
|
|
@@ -191,7 +203,7 @@
|
|
|
191
203
|
})
|
|
192
204
|
}
|
|
193
205
|
|
|
194
|
-
function resolveExtensions() {
|
|
206
|
+
function resolveExtensions(): AnyExtension[] | Promise<AnyExtension[]> {
|
|
195
207
|
if (extensionsOverride) return extensionsOverride
|
|
196
208
|
return buildExtensions({
|
|
197
209
|
headingLevels,
|
|
@@ -203,7 +215,7 @@
|
|
|
203
215
|
tables,
|
|
204
216
|
youtube,
|
|
205
217
|
dragHandle,
|
|
206
|
-
markdown:
|
|
218
|
+
markdown: resolvedOutput === 'markdown',
|
|
207
219
|
markdownAllowHtml,
|
|
208
220
|
mentionTrigger,
|
|
209
221
|
mentionSuggestion: onMention
|
|
@@ -228,14 +240,11 @@
|
|
|
228
240
|
}
|
|
229
241
|
|
|
230
242
|
let suppressUpdate = false
|
|
243
|
+
let lastEmitted: string | EditorJSON | undefined
|
|
231
244
|
|
|
232
245
|
$effect(() => {
|
|
233
246
|
if (!contentElement) return
|
|
234
247
|
|
|
235
|
-
// Untrack: these props would otherwise cause editor recreation on every
|
|
236
|
-
// keystroke (value changes via onUpdate → effect re-runs → destroy + rebuild
|
|
237
|
-
// → cursor lost → can only type 1 char). value sync is handled by a
|
|
238
|
-
// dedicated effect below; disabled/readonly toggle via setEditable.
|
|
239
248
|
const initialContent = untrack(() => value ?? '')
|
|
240
249
|
const initialEditable = untrack(() => !disabled && !readonly)
|
|
241
250
|
const initialAutofocus = untrack(() => autofocus)
|
|
@@ -245,43 +254,57 @@
|
|
|
245
254
|
...(ariaDescribedBy ? { 'aria-describedby': ariaDescribedBy } : {}),
|
|
246
255
|
...(hasError ? { 'aria-invalid': 'true' } : {})
|
|
247
256
|
}))
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
syncState(e)
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
257
|
+
const el = contentElement
|
|
258
|
+
const result = untrack(() => resolveExtensions())
|
|
259
|
+
|
|
260
|
+
let ed: Editor | null = null
|
|
261
|
+
let cancelled = false
|
|
262
|
+
|
|
263
|
+
const create = (exts: AnyExtension[]) => {
|
|
264
|
+
if (cancelled) return
|
|
265
|
+
ed = new Editor({
|
|
266
|
+
element: el,
|
|
267
|
+
extensions: exts,
|
|
268
|
+
content: initialContent,
|
|
269
|
+
editable: initialEditable,
|
|
270
|
+
autofocus: initialAutofocus,
|
|
271
|
+
editorProps: {
|
|
272
|
+
attributes: initialAttrs as Record<string, string>
|
|
273
|
+
},
|
|
274
|
+
onCreate: ({ editor: e }) => syncState(e),
|
|
275
|
+
onUpdate: ({ editor: e }) => {
|
|
276
|
+
syncState(e)
|
|
277
|
+
if (suppressUpdate) return
|
|
278
|
+
const serialized = serialize(e)
|
|
279
|
+
lastEmitted = serialized
|
|
280
|
+
value = serialized
|
|
281
|
+
emit.onInput()
|
|
282
|
+
onValueChange?.(serialized)
|
|
283
|
+
},
|
|
284
|
+
onSelectionUpdate: ({ editor: e }) => syncState(e),
|
|
285
|
+
onFocus: ({ editor: e }) => {
|
|
286
|
+
syncState(e)
|
|
287
|
+
emit.onFocus()
|
|
288
|
+
onFocus?.()
|
|
289
|
+
},
|
|
290
|
+
onBlur: ({ editor: e }) => {
|
|
291
|
+
syncState(e)
|
|
292
|
+
emit.onBlur()
|
|
293
|
+
onBlur?.()
|
|
294
|
+
}
|
|
295
|
+
})
|
|
296
|
+
editor = ed
|
|
297
|
+
}
|
|
280
298
|
|
|
281
|
-
|
|
299
|
+
if (result instanceof Promise) {
|
|
300
|
+
result.then(create)
|
|
301
|
+
} else {
|
|
302
|
+
create(result)
|
|
303
|
+
}
|
|
282
304
|
|
|
283
305
|
return () => {
|
|
284
|
-
|
|
306
|
+
cancelled = true
|
|
307
|
+
ed?.destroy()
|
|
285
308
|
editor = null
|
|
286
309
|
}
|
|
287
310
|
})
|
|
@@ -294,9 +317,6 @@
|
|
|
294
317
|
}
|
|
295
318
|
})
|
|
296
319
|
|
|
297
|
-
// Sync aria attributes on the ProseMirror element when error/id state changes.
|
|
298
|
-
// Tiptap's editorProps.attributes is read once at init, so toggling needs
|
|
299
|
-
// direct DOM access. Run on every state change.
|
|
300
320
|
$effect(() => {
|
|
301
321
|
if (!contentElement) return
|
|
302
322
|
const pm = contentElement.querySelector('.ProseMirror') as HTMLElement | null
|
|
@@ -311,6 +331,7 @@
|
|
|
311
331
|
$effect(() => {
|
|
312
332
|
if (!editor) return
|
|
313
333
|
if (value === undefined) return
|
|
334
|
+
if (typeof value === 'string' && value === lastEmitted) return
|
|
314
335
|
const current = serialize(editor)
|
|
315
336
|
if (isContentEqual(current, value)) return
|
|
316
337
|
suppressUpdate = true
|
|
@@ -334,13 +355,11 @@
|
|
|
334
355
|
TOOLBAR_ACTIONS[action].run(editor)
|
|
335
356
|
},
|
|
336
357
|
getValue(format) {
|
|
337
|
-
if (!editor) return
|
|
338
|
-
const fmt = format ??
|
|
358
|
+
if (!editor) return resolvedOutput === 'json' ? ({} as EditorJSON) : ''
|
|
359
|
+
const fmt = format ?? resolvedOutput
|
|
339
360
|
if (fmt === 'json') return editor.getJSON() as EditorJSON
|
|
340
361
|
if (fmt === 'markdown') {
|
|
341
|
-
const md = (editor
|
|
342
|
-
| { getMarkdown?: () => string }
|
|
343
|
-
| undefined
|
|
362
|
+
const md = getMarkdownStorage(editor)
|
|
344
363
|
if (md && typeof md.getMarkdown === 'function') return md.getMarkdown()
|
|
345
364
|
return editor.getHTML()
|
|
346
365
|
}
|
|
@@ -384,7 +403,6 @@
|
|
|
384
403
|
return {
|
|
385
404
|
root: slots.root({ class: [c.root, className, u.root] }),
|
|
386
405
|
toolbar: slots.toolbar({ class: [c.toolbar, u.toolbar] }),
|
|
387
|
-
toolbarGroup: slots.toolbarGroup({ class: [c.toolbarGroup, u.toolbarGroup] }),
|
|
388
406
|
toolbarButton: slots.toolbarButton({ class: [c.toolbarButton, u.toolbarButton] }),
|
|
389
407
|
toolbarSeparator: slots.toolbarSeparator({
|
|
390
408
|
class: [c.toolbarSeparator, u.toolbarSeparator]
|
|
@@ -396,7 +414,6 @@
|
|
|
396
414
|
}
|
|
397
415
|
})
|
|
398
416
|
|
|
399
|
-
// ----- URL prompt modal (shared by YouTube/Image/Link toolbar + slash) -----
|
|
400
417
|
interface UrlPromptState {
|
|
401
418
|
open: boolean
|
|
402
419
|
title: string
|
|
@@ -440,7 +457,6 @@
|
|
|
440
457
|
}
|
|
441
458
|
}
|
|
442
459
|
|
|
443
|
-
// ----- Image upload via hidden file input -----
|
|
444
460
|
let fileInput: HTMLInputElement | null = $state(null)
|
|
445
461
|
|
|
446
462
|
async function handleFileSelected(event: Event): Promise<void> {
|
|
@@ -452,10 +468,19 @@
|
|
|
452
468
|
if (!onImageUpload) return
|
|
453
469
|
try {
|
|
454
470
|
const url = await onImageUpload(file)
|
|
471
|
+
if (!isSafeImageSrc(url)) {
|
|
472
|
+
// eslint-disable-next-line no-console
|
|
473
|
+
console.warn('[Editor] blocked unsafe image src from onImageUpload:', url)
|
|
474
|
+
return
|
|
475
|
+
}
|
|
455
476
|
editor.chain().focus().setImage({ src: url }).run()
|
|
456
477
|
} catch (err) {
|
|
457
|
-
|
|
458
|
-
|
|
478
|
+
if (onImageUploadError) {
|
|
479
|
+
onImageUploadError(err)
|
|
480
|
+
} else {
|
|
481
|
+
// eslint-disable-next-line no-console
|
|
482
|
+
console.error('[Editor] image upload failed', err)
|
|
483
|
+
}
|
|
459
484
|
}
|
|
460
485
|
}
|
|
461
486
|
|
|
@@ -503,7 +528,6 @@
|
|
|
503
528
|
})
|
|
504
529
|
}
|
|
505
530
|
|
|
506
|
-
// ----- Table dimension picker -----
|
|
507
531
|
let tableMenuOpen = $state(false)
|
|
508
532
|
let tablePickerRows = $state(0)
|
|
509
533
|
let tablePickerCols = $state(0)
|