@streamscloud/kit 0.25.2 → 0.27.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/ui/page-layout/cmp.page-layout.svelte +37 -37
- package/dist/ui/page-layout/cmp.page-layout.svelte.d.ts +12 -11
- package/dist/ui/page-layout/index.d.ts +1 -1
- package/dist/ui/page-layout/layout-control-button.svelte +71 -0
- package/dist/ui/page-layout/layout-control-button.svelte.d.ts +19 -0
- package/dist/ui/page-layout/page-layout-localization.d.ts +2 -0
- package/dist/ui/page-layout/page-layout-localization.js +14 -0
- package/dist/ui/page-layout/types.d.ts +13 -0
- package/dist/ui/select/cmp.multiselect-tree.svelte +2 -2
- package/dist/ui/select/cmp.multiselect-tree.svelte.d.ts +3 -3
- package/dist/ui/select/cmp.singleselect-async.svelte +3 -2
- package/dist/ui/select/cmp.singleselect.svelte +3 -3
- package/dist/ui/select/index.d.ts +1 -1
- package/dist/ui/select/multiselect-base.svelte +4 -3
- package/dist/ui/select/multiselect-base.svelte.d.ts +1 -1
- package/dist/ui/select/select-core.svelte.d.ts +1 -1
- package/dist/ui/select/select-listbox-localization.d.ts +1 -1
- package/dist/ui/select/select-listbox-localization.js +5 -5
- package/dist/ui/select/select-listbox.svelte +5 -3
- package/dist/ui/select/select-listbox.svelte.d.ts +2 -0
- package/dist/ui/select/types.d.ts +12 -5
- package/package.json +1 -1
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
<script lang="ts">import
|
|
1
|
+
<script lang="ts">import LayoutControlButton from './layout-control-button.svelte';
|
|
2
2
|
import { PageLayoutLocalization } from './page-layout-localization';
|
|
3
|
+
import IconDismiss from '@fluentui/svg-icons/icons/dismiss_20_regular.svg?raw';
|
|
3
4
|
import IconWindow from '@fluentui/svg-icons/icons/window_20_regular.svg?raw';
|
|
4
|
-
|
|
5
|
+
import IconWindowMultiple from '@fluentui/svg-icons/icons/window_multiple_20_regular.svg?raw';
|
|
6
|
+
const { children, title, actions, navigationToggle, editorControls, scrollable = false } = $props();
|
|
5
7
|
const localization = new PageLayoutLocalization();
|
|
6
8
|
</script>
|
|
7
9
|
|
|
@@ -18,16 +20,29 @@ const localization = new PageLayoutLocalization();
|
|
|
18
20
|
{#if actions}
|
|
19
21
|
{@render actions()}
|
|
20
22
|
{/if}
|
|
21
|
-
{#if
|
|
22
|
-
<
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
23
|
+
{#if navigationToggle || editorControls}
|
|
24
|
+
<div class="page-layout__controls">
|
|
25
|
+
{#if navigationToggle}
|
|
26
|
+
<LayoutControlButton
|
|
27
|
+
icon={IconWindow}
|
|
28
|
+
label={localization.toggleNavigation}
|
|
29
|
+
variant="flat"
|
|
30
|
+
active={navigationToggle.active}
|
|
31
|
+
on={{ click: navigationToggle.onClick }} />
|
|
32
|
+
{/if}
|
|
33
|
+
{#if editorControls}
|
|
34
|
+
<LayoutControlButton
|
|
35
|
+
icon={IconWindowMultiple}
|
|
36
|
+
label={localization.togglePanels}
|
|
37
|
+
active={editorControls.panelsToggle.active}
|
|
38
|
+
on={{ click: editorControls.panelsToggle.onClick }} />
|
|
39
|
+
<LayoutControlButton
|
|
40
|
+
icon={IconDismiss}
|
|
41
|
+
label={localization.close}
|
|
42
|
+
active={editorControls.close.active}
|
|
43
|
+
on={{ click: editorControls.close.onClick }} />
|
|
44
|
+
{/if}
|
|
45
|
+
</div>
|
|
31
46
|
{/if}
|
|
32
47
|
</div>
|
|
33
48
|
</div>
|
|
@@ -38,12 +53,14 @@ const localization = new PageLayoutLocalization();
|
|
|
38
53
|
|
|
39
54
|
<!--
|
|
40
55
|
@component
|
|
41
|
-
PageLayout — top-level window chrome. Renders a fixed-height header (title + actions + optional
|
|
42
|
-
|
|
56
|
+
PageLayout — top-level window chrome. Renders a fixed-height header (title + actions + an optional
|
|
57
|
+
group of chrome buttons) above a flex-column body that typically contains a `PagePanels` row.
|
|
43
58
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
59
|
+
`navigationToggle` surfaces the app-navigation button as a bare glyph; `editorControls` is the fixed
|
|
60
|
+
editor pair — panels toggle + close, both required so an editor never ships half the preset, drawn as
|
|
61
|
+
outlined squares. Every entry takes `{ active?, onClick }`, and the group renders in the order
|
|
62
|
+
navigation → panels → close. Set `scrollable` to let the whole page scroll as one document with
|
|
63
|
+
sticky panel headers.
|
|
47
64
|
|
|
48
65
|
### CSS Custom Properties
|
|
49
66
|
| Property | Description | Default |
|
|
@@ -86,27 +103,10 @@ the whole page scroll as one document with sticky panel headers.
|
|
|
86
103
|
align-items: center;
|
|
87
104
|
gap: 1rem;
|
|
88
105
|
}
|
|
89
|
-
.page-
|
|
90
|
-
|
|
91
|
-
display: inline-flex;
|
|
106
|
+
.page-layout__controls {
|
|
107
|
+
display: flex;
|
|
92
108
|
align-items: center;
|
|
93
|
-
|
|
94
|
-
width: 1.75rem;
|
|
95
|
-
height: 1.75rem;
|
|
96
|
-
padding: 0;
|
|
97
|
-
background: none;
|
|
98
|
-
border: none;
|
|
99
|
-
border-radius: var(--sc-kit--radius--sm);
|
|
100
|
-
color: var(--sc-kit--color--text--muted);
|
|
101
|
-
cursor: pointer;
|
|
102
|
-
}
|
|
103
|
-
.page-layout__window-toggle:hover {
|
|
104
|
-
background: var(--sc-kit--color--bg--hover);
|
|
105
|
-
color: var(--sc-kit--color--text--primary);
|
|
106
|
-
}
|
|
107
|
-
.page-layout__window-toggle--active {
|
|
108
|
-
background: var(--sc-kit--color--bg--active);
|
|
109
|
-
color: var(--sc-kit--color--text--primary);
|
|
109
|
+
gap: var(--sc-kit--space--2);
|
|
110
110
|
}
|
|
111
111
|
.page-layout__body {
|
|
112
112
|
flex: 1;
|
|
@@ -1,23 +1,24 @@
|
|
|
1
|
+
import type { EditorControls, PageLayoutAction } from './types';
|
|
1
2
|
import type { Snippet } from 'svelte';
|
|
2
3
|
type Props = {
|
|
3
4
|
children: Snippet;
|
|
4
5
|
title?: string | Snippet;
|
|
5
6
|
actions?: Snippet;
|
|
6
|
-
/**
|
|
7
|
-
|
|
7
|
+
/** Hides / shows the host app's own navigation. When set, a bare glyph button is rendered in the header's control group. */
|
|
8
|
+
navigationToggle?: PageLayoutAction;
|
|
9
|
+
/** Editor chrome — the fixed panels-toggle + close pair. Set it to render both buttons; omit it entirely for a non-editor page. */
|
|
10
|
+
editorControls?: EditorControls;
|
|
8
11
|
scrollable?: boolean;
|
|
9
|
-
on?: {
|
|
10
|
-
/** When set, a window-toggle button is rendered at the right edge of the header. */
|
|
11
|
-
windowToggle?: () => void;
|
|
12
|
-
};
|
|
13
12
|
};
|
|
14
13
|
/**
|
|
15
|
-
* PageLayout — top-level window chrome. Renders a fixed-height header (title + actions + optional
|
|
16
|
-
*
|
|
14
|
+
* PageLayout — top-level window chrome. Renders a fixed-height header (title + actions + an optional
|
|
15
|
+
* group of chrome buttons) above a flex-column body that typically contains a `PagePanels` row.
|
|
17
16
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
17
|
+
* `navigationToggle` surfaces the app-navigation button as a bare glyph; `editorControls` is the fixed
|
|
18
|
+
* editor pair — panels toggle + close, both required so an editor never ships half the preset, drawn as
|
|
19
|
+
* outlined squares. Every entry takes `{ active?, onClick }`, and the group renders in the order
|
|
20
|
+
* navigation → panels → close. Set `scrollable` to let the whole page scroll as one document with
|
|
21
|
+
* sticky panel headers.
|
|
21
22
|
*
|
|
22
23
|
* ### CSS Custom Properties
|
|
23
24
|
* | Property | Description | Default |
|
|
@@ -5,4 +5,4 @@ export { default as PagePanels } from './cmp.page-panels.svelte';
|
|
|
5
5
|
export { default as PanelCollapseButton } from './cmp.panel-collapse-button.svelte';
|
|
6
6
|
export { default as PanelContent } from './cmp.panel-content.svelte';
|
|
7
7
|
export { PanelEntry, PanelState } from './panel-state.svelte';
|
|
8
|
-
export type { PanelBorder, PanelResizingSettings } from './types';
|
|
8
|
+
export type { EditorControls, PageLayoutAction, PanelBorder, PanelResizingSettings } from './types';
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<script lang="ts">import { Icon } from '../icon';
|
|
2
|
+
const { icon, label, variant = 'outlined', active, on } = $props();
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<button
|
|
6
|
+
type="button"
|
|
7
|
+
class="layout-control-button layout-control-button--{variant}"
|
|
8
|
+
class:layout-control-button--active={active}
|
|
9
|
+
aria-label={label}
|
|
10
|
+
aria-pressed={active}
|
|
11
|
+
onclick={on.click}>
|
|
12
|
+
<Icon src={icon} size={variant === 'flat' ? 'control-xl' : 'control-md'} />
|
|
13
|
+
</button>
|
|
14
|
+
|
|
15
|
+
<!--
|
|
16
|
+
@component
|
|
17
|
+
Folder-internal chrome button for the `PageLayout` header. Two variants: `flat` for the
|
|
18
|
+
app-navigation toggle and `outlined` for the editor pair. Rendered by `PageLayout`; not exported.
|
|
19
|
+
-->
|
|
20
|
+
<style>.layout-control-button {
|
|
21
|
+
display: inline-flex;
|
|
22
|
+
align-items: center;
|
|
23
|
+
justify-content: center;
|
|
24
|
+
flex-shrink: 0;
|
|
25
|
+
inline-size: var(--_lcb--size);
|
|
26
|
+
block-size: var(--_lcb--size);
|
|
27
|
+
padding: 0;
|
|
28
|
+
color: var(--_lcb--color);
|
|
29
|
+
background: var(--_lcb--background);
|
|
30
|
+
border: 1px solid var(--_lcb--border-color);
|
|
31
|
+
border-radius: var(--_lcb--border-radius);
|
|
32
|
+
cursor: pointer;
|
|
33
|
+
transition: background-color var(--sc-kit--duration--base) var(--sc-kit--ease--default), border-color var(--sc-kit--duration--base) var(--sc-kit--ease--default), color var(--sc-kit--duration--base) var(--sc-kit--ease--default);
|
|
34
|
+
}
|
|
35
|
+
.layout-control-button--outlined {
|
|
36
|
+
--_lcb--size: var(--sc-kit--field--height--md);
|
|
37
|
+
--_lcb--border-radius: var(--sc-kit--field--border-radius--md);
|
|
38
|
+
--_lcb--color: var(--sc-kit--color--text--primary);
|
|
39
|
+
--_lcb--color-hover: var(--sc-kit--color--text--primary);
|
|
40
|
+
--_lcb--color-active: var(--sc-kit--color--text--primary);
|
|
41
|
+
--_lcb--background: var(--sc-kit--color--bg--panel);
|
|
42
|
+
--_lcb--background-hover: var(--sc-kit--color--bg--hover);
|
|
43
|
+
--_lcb--background-active: var(--sc-kit--color--bg--active);
|
|
44
|
+
--_lcb--border-color: var(--sc-kit--color--border);
|
|
45
|
+
--_lcb--border-color-active: var(--sc-kit--color--border--strong);
|
|
46
|
+
}
|
|
47
|
+
.layout-control-button--flat {
|
|
48
|
+
--_lcb--size: 1.75rem;
|
|
49
|
+
--_lcb--border-radius: var(--sc-kit--radius--sm);
|
|
50
|
+
--_lcb--color: var(--sc-kit--color--text--muted);
|
|
51
|
+
--_lcb--color-hover: var(--sc-kit--color--text--primary);
|
|
52
|
+
--_lcb--color-active: var(--sc-kit--color--text--primary);
|
|
53
|
+
--_lcb--background: transparent;
|
|
54
|
+
--_lcb--background-hover: var(--sc-kit--color--bg--hover);
|
|
55
|
+
--_lcb--background-active: var(--sc-kit--color--bg--active);
|
|
56
|
+
--_lcb--border-color: transparent;
|
|
57
|
+
--_lcb--border-color-active: transparent;
|
|
58
|
+
}
|
|
59
|
+
.layout-control-button:hover {
|
|
60
|
+
color: var(--_lcb--color-hover);
|
|
61
|
+
background: var(--_lcb--background-hover);
|
|
62
|
+
}
|
|
63
|
+
.layout-control-button:focus-visible {
|
|
64
|
+
outline: 2px solid var(--sc-kit--color--border--focus);
|
|
65
|
+
outline-offset: 2px;
|
|
66
|
+
}
|
|
67
|
+
.layout-control-button--active {
|
|
68
|
+
color: var(--_lcb--color-active);
|
|
69
|
+
background: var(--_lcb--background-active);
|
|
70
|
+
border-color: var(--_lcb--border-color-active);
|
|
71
|
+
}</style>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
type Props = {
|
|
2
|
+
/** Glyph — a string SVG source. */
|
|
3
|
+
icon: string;
|
|
4
|
+
label: string;
|
|
5
|
+
/** `flat` = bare 20px glyph in a 28px hit area (app-navigation toggle); `outlined` = 32px bordered square with a 16px glyph (editor chrome). @default 'outlined' */
|
|
6
|
+
variant?: 'outlined' | 'flat';
|
|
7
|
+
/** Toggle state. Leave undefined for a plain action button so no `aria-pressed` is emitted. */
|
|
8
|
+
active?: boolean;
|
|
9
|
+
on: {
|
|
10
|
+
click: () => void;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Folder-internal chrome button for the `PageLayout` header. Two variants: `flat` for the
|
|
15
|
+
* app-navigation toggle and `outlined` for the editor pair. Rendered by `PageLayout`; not exported.
|
|
16
|
+
*/
|
|
17
|
+
declare const LayoutControlButton: import("svelte").Component<Props, {}, "">;
|
|
18
|
+
type LayoutControlButton = ReturnType<typeof LayoutControlButton>;
|
|
19
|
+
export default LayoutControlButton;
|
|
@@ -11,6 +11,14 @@ const loc = {
|
|
|
11
11
|
toggleNavigation: {
|
|
12
12
|
en: 'Toggle navigation',
|
|
13
13
|
no: 'Veksle navigasjon'
|
|
14
|
+
},
|
|
15
|
+
togglePanels: {
|
|
16
|
+
en: 'Toggle panels',
|
|
17
|
+
no: 'Veksle paneler'
|
|
18
|
+
},
|
|
19
|
+
close: {
|
|
20
|
+
en: 'Close',
|
|
21
|
+
no: 'Lukk'
|
|
14
22
|
}
|
|
15
23
|
};
|
|
16
24
|
export class PageLayoutLocalization {
|
|
@@ -23,4 +31,10 @@ export class PageLayoutLocalization {
|
|
|
23
31
|
get toggleNavigation() {
|
|
24
32
|
return loc.toggleNavigation[AppLocale.current];
|
|
25
33
|
}
|
|
34
|
+
get togglePanels() {
|
|
35
|
+
return loc.togglePanels[AppLocale.current];
|
|
36
|
+
}
|
|
37
|
+
get close() {
|
|
38
|
+
return loc.close[AppLocale.current];
|
|
39
|
+
}
|
|
26
40
|
}
|
|
@@ -17,3 +17,16 @@ export type PanelResizingSettings = {
|
|
|
17
17
|
maxWidth?: number;
|
|
18
18
|
};
|
|
19
19
|
export type PanelBorder = 'top' | 'right' | 'bottom' | 'left';
|
|
20
|
+
/**
|
|
21
|
+
* Config for one `PageLayout` header chrome button. Set `active` only for toggles — it drives both
|
|
22
|
+
* `aria-pressed` and the pressed background, and is omitted entirely for plain action buttons.
|
|
23
|
+
*/
|
|
24
|
+
export type PageLayoutAction = {
|
|
25
|
+
active?: boolean;
|
|
26
|
+
onClick: () => void;
|
|
27
|
+
};
|
|
28
|
+
/** Editor chrome — a fixed pair, so an editor never ships half of it. */
|
|
29
|
+
export type EditorControls = {
|
|
30
|
+
panelsToggle: PageLayoutAction;
|
|
31
|
+
close: PageLayoutAction;
|
|
32
|
+
};
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<script lang="ts" generics="T">import { default as MultiselectBase } from './multiselect-base.svelte';
|
|
5
5
|
import { buildFuseIndex, defaultCompare, runFuseSearch } from './select-internals';
|
|
6
6
|
import { isSelectGroup } from './types';
|
|
7
|
-
const { options, value, compare = defaultCompare, size = 'md', placeholder = '', disabled = false, readonly = false, inert = false, error = false, borderless = false, searchable = false, groupHeader = 'toggle-all', selectionMode = 'children-only', selectedDisplay = 'checkbox',
|
|
7
|
+
const { options, value, compare = defaultCompare, size = 'md', placeholder = '', disabled = false, readonly = false, inert = false, error = false, borderless = false, searchable = false, groupHeader = 'toggle-all', selectionMode = 'children-only', selectedDisplay = 'checkbox', createOptions, icon, chevronIcon, optionSnippet, selectionSnippet, id, name, autocomplete = 'off', 'aria-label': ariaLabel, 'aria-describedby': ariaDescribedby, 'aria-required': ariaRequired, on } = $props();
|
|
8
8
|
const fuse = $derived(buildFuseIndex(options));
|
|
9
9
|
const loadOptionsShim = (q) => Promise.resolve(runFuseSearch(fuse, q, options));
|
|
10
10
|
// Include flat roots, not only groups — else a childless root can't be picked as a create-parent.
|
|
@@ -60,7 +60,7 @@ $effect(() => {
|
|
|
60
60
|
groupHeader={groupHeader}
|
|
61
61
|
selectionMode={selectionMode}
|
|
62
62
|
selectedDisplay={selectedDisplay}
|
|
63
|
-
|
|
63
|
+
createOptions={createOptions}
|
|
64
64
|
icon={icon}
|
|
65
65
|
chevronIcon={chevronIcon}
|
|
66
66
|
optionSnippet={optionSnippet}
|
|
@@ -4,7 +4,7 @@ export type MultiselectTreeInstance = {
|
|
|
4
4
|
focus: () => void;
|
|
5
5
|
};
|
|
6
6
|
import type { IconProp } from '../icon';
|
|
7
|
-
import { type SelectItem, type SelectOption } from './types';
|
|
7
|
+
import { type SelectCreateOptions, type SelectItem, type SelectOption } from './types';
|
|
8
8
|
import type { Snippet } from 'svelte';
|
|
9
9
|
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
10
10
|
declare function $$render<T>(): {
|
|
@@ -57,8 +57,8 @@ declare function $$render<T>(): {
|
|
|
57
57
|
selectedDisplay?: "checkmark" | "checkbox" | "highlight";
|
|
58
58
|
/** Equality comparator for `T`. Required when `T` is an object. @default (a, b) => a === b */
|
|
59
59
|
compare?: (a: T, b: T) => boolean;
|
|
60
|
-
/** Enables creatable mode.
|
|
61
|
-
|
|
60
|
+
/** Enables creatable mode. When at least one top-level root has a `value`, the "Create …" UI expands into a section whose parent picker offers those roots (group or flat); otherwise a plain "Create …" row is shown. `createOptions.prefix` applies only to the plain row — the create-with-parent section keeps its own localization. See {@link SelectCreateOptions}. */
|
|
61
|
+
createOptions?: SelectCreateOptions;
|
|
62
62
|
/** Leading icon — string SVG source, `{ src, color?, size? }` object, or custom snippet. */
|
|
63
63
|
icon?: IconProp;
|
|
64
64
|
/** Trailing chevron icon override. Same shape as `icon`. */
|
|
@@ -9,7 +9,7 @@ import { defaultCompare } from './select-internals';
|
|
|
9
9
|
import { default as SelectListbox } from './select-listbox.svelte';
|
|
10
10
|
import { SingleselectLocalization } from './singleselect-localization';
|
|
11
11
|
import IconChevronDown from '@fluentui/svg-icons/icons/chevron_down_16_regular.svg?raw';
|
|
12
|
-
const { loadOptions, debounceMs = 400, size = 'md', placeholder = '', disabled = false, readonly = false, inert = false, error = false, borderless = false, searchable = true, groupHeader = 'static', compare = defaultCompare,
|
|
12
|
+
const { loadOptions, debounceMs = 400, size = 'md', placeholder = '', disabled = false, readonly = false, inert = false, error = false, borderless = false, searchable = true, groupHeader = 'static', compare = defaultCompare, createOptions, icon, chevronIcon, optionSnippet, selectionSnippet, listbox, id, name, autocomplete = 'off', 'aria-label': ariaLabel, 'aria-describedby': ariaDescribedby, 'aria-required': ariaRequired, ...mode } = $props();
|
|
13
13
|
const localization = new SingleselectLocalization();
|
|
14
14
|
const selectedOption = $derived(mode.value ?? null);
|
|
15
15
|
const selectionLabel = $derived(selectedOption?.label ?? '');
|
|
@@ -23,7 +23,7 @@ const core = createSelectCore({
|
|
|
23
23
|
getDebounceMs: () => debounceMs,
|
|
24
24
|
getCompare: () => compare,
|
|
25
25
|
getSearchable: () => searchable,
|
|
26
|
-
getCanCreate: () => canCreate,
|
|
26
|
+
getCanCreate: () => createOptions?.canCreate,
|
|
27
27
|
getGroupHeader: () => groupHeader,
|
|
28
28
|
isPicked: (option) => selectedOption !== null && compare(option.value, selectedOption.value),
|
|
29
29
|
getSelectedOptions: () => (selectedOption ? [selectedOption] : []),
|
|
@@ -140,6 +140,7 @@ const showClear = $derived(mode.clearable && hasValue && isInteractive && !core.
|
|
|
140
140
|
listboxId={listboxId}
|
|
141
141
|
optionSnippet={optionSnippet}
|
|
142
142
|
bodySnippet={listbox}
|
|
143
|
+
createPrefix={createOptions?.prefix}
|
|
143
144
|
on={{ pickRow: core.pickRow, hoverRow: core.setHighlight, dismiss: core.closePopover }} />
|
|
144
145
|
</div>
|
|
145
146
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<script lang="ts" generics="T">import { default as SingleselectAsync } from './cmp.singleselect-async.svelte';
|
|
5
5
|
import { buildFuseIndex, defaultCompare, runFuseSearch } from './select-internals';
|
|
6
6
|
import { isSelectGroup } from './types';
|
|
7
|
-
const { options, compare = defaultCompare, size = 'md', placeholder = '', disabled = false, readonly = false, inert = false, error = false, borderless = false, searchable = true, groupHeader = 'static',
|
|
7
|
+
const { options, compare = defaultCompare, size = 'md', placeholder = '', disabled = false, readonly = false, inert = false, error = false, borderless = false, searchable = true, groupHeader = 'static', createOptions, icon, chevronIcon, optionSnippet, selectionSnippet, listbox, id, name, autocomplete = 'off', 'aria-label': ariaLabel, 'aria-describedby': ariaDescribedby, 'aria-required': ariaRequired, ...mode } = $props();
|
|
8
8
|
const fuse = $derived(buildFuseIndex(options));
|
|
9
9
|
const loadOptionsShim = (q) => Promise.resolve(runFuseSearch(fuse, q, options));
|
|
10
10
|
const resolvedValue = $derived.by(() => {
|
|
@@ -54,7 +54,7 @@ $effect(() => {
|
|
|
54
54
|
borderless={borderless}
|
|
55
55
|
searchable={searchable}
|
|
56
56
|
groupHeader={groupHeader}
|
|
57
|
-
|
|
57
|
+
createOptions={createOptions}
|
|
58
58
|
icon={icon}
|
|
59
59
|
chevronIcon={chevronIcon}
|
|
60
60
|
optionSnippet={optionSnippet}
|
|
@@ -87,7 +87,7 @@ $effect(() => {
|
|
|
87
87
|
borderless={borderless}
|
|
88
88
|
searchable={searchable}
|
|
89
89
|
groupHeader={groupHeader}
|
|
90
|
-
|
|
90
|
+
createOptions={createOptions}
|
|
91
91
|
icon={icon}
|
|
92
92
|
chevronIcon={chevronIcon}
|
|
93
93
|
optionSnippet={optionSnippet}
|
|
@@ -9,4 +9,4 @@ export type { MultiselectAsyncInstance } from './cmp.multiselect-async.svelte';
|
|
|
9
9
|
export { default as MultiselectTree } from './cmp.multiselect-tree.svelte';
|
|
10
10
|
export type { MultiselectTreeInstance } from './cmp.multiselect-tree.svelte';
|
|
11
11
|
export { isSelectGroup } from './types';
|
|
12
|
-
export type { SelectItem, SelectOption, SelectOptionGroup } from './types';
|
|
12
|
+
export type { SelectCreateOptions, SelectItem, SelectOption, SelectOptionGroup } from './types';
|
|
@@ -13,7 +13,7 @@ import { default as SelectListbox } from './select-listbox.svelte';
|
|
|
13
13
|
import IconCheckmark from '@fluentui/svg-icons/icons/checkmark_16_regular.svg?raw';
|
|
14
14
|
import IconChevronDown from '@fluentui/svg-icons/icons/chevron_down_16_regular.svg?raw';
|
|
15
15
|
import { dndzone } from 'svelte-dnd-action';
|
|
16
|
-
const { loadOptions, debounceMs = 400, value, size = 'md', placeholder = '', disabled = false, readonly = false, inert = false, error = false, borderless = false, searchable = true, groupHeader = 'static', compare = defaultCompare, selectionMode = 'children-only', selectedDisplay = 'checkmark',
|
|
16
|
+
const { loadOptions, debounceMs = 400, value, size = 'md', placeholder = '', disabled = false, readonly = false, inert = false, error = false, borderless = false, searchable = true, groupHeader = 'static', compare = defaultCompare, selectionMode = 'children-only', selectedDisplay = 'checkmark', createOptions, chipMode = 'inline', reorderable = false, maxVisible = Infinity, showFullItem = false, chipVariant, showChevron = true, hideSelected = false, icon, chevronIcon, optionSnippet, chipSnippet, selectionSnippet, parentSource, id, name, autocomplete = 'off', 'aria-label': ariaLabel, 'aria-describedby': ariaDescribedby, 'aria-required': ariaRequired, on } = $props();
|
|
17
17
|
const localization = new MultiselectLocalization();
|
|
18
18
|
const isPicked = (v) => value.some((existing) => compare(existing.value, v));
|
|
19
19
|
const hasValue = $derived(value.length > 0);
|
|
@@ -35,7 +35,7 @@ const core = createSelectCore({
|
|
|
35
35
|
getDebounceMs: () => debounceMs,
|
|
36
36
|
getCompare: () => compare,
|
|
37
37
|
getSearchable: () => searchable,
|
|
38
|
-
getCanCreate: () => canCreate,
|
|
38
|
+
getCanCreate: () => createOptions?.canCreate,
|
|
39
39
|
getHideCreateRow: () => showCreateSection,
|
|
40
40
|
getGroupHeader: () => groupHeader,
|
|
41
41
|
getSelectionMode: () => selectionMode,
|
|
@@ -265,6 +265,7 @@ const handleDndFinalize = (e) => {
|
|
|
265
265
|
optionSnippet={optionSnippet}
|
|
266
266
|
headerSnippet={showCreateSection ? createWithParentSection : undefined}
|
|
267
267
|
hideCreateRows={showCreateSection}
|
|
268
|
+
createPrefix={createOptions?.prefix}
|
|
268
269
|
suppressEmpty={hideSelected}
|
|
269
270
|
on={{ pickRow: core.pickRow, hoverRow: core.setHighlight, dismiss: core.closePopover }} />
|
|
270
271
|
</div>
|
|
@@ -307,7 +308,7 @@ Public Multi cmps proxy here:
|
|
|
307
308
|
- `MultiselectTree` (sync grouped) — groups + cascade + create-with-parent.
|
|
308
309
|
|
|
309
310
|
Owns: input + trigger markup, chip rendering (`inline`/`external`/`selectionSnippet`),
|
|
310
|
-
DnD reorder, listbox, create-with-parent section (when `
|
|
311
|
+
DnD reorder, listbox, create-with-parent section (when `createOptions` + groups in items),
|
|
311
312
|
tri-state group headers, race-prevention against in-flight handlers, 600ms spinner gate,
|
|
312
313
|
debounced/Fuse-driven `loadOptions` plumbing through `select-core`.
|
|
313
314
|
|
|
@@ -50,7 +50,7 @@ interface $$IsomorphicComponent {
|
|
|
50
50
|
* - `MultiselectTree` (sync grouped) — groups + cascade + create-with-parent.
|
|
51
51
|
*
|
|
52
52
|
* Owns: input + trigger markup, chip rendering (`inline`/`external`/`selectionSnippet`),
|
|
53
|
-
* DnD reorder, listbox, create-with-parent section (when `
|
|
53
|
+
* DnD reorder, listbox, create-with-parent section (when `createOptions` + groups in items),
|
|
54
54
|
* tri-state group headers, race-prevention against in-flight handlers, 600ms spinner gate,
|
|
55
55
|
* debounced/Fuse-driven `loadOptions` plumbing through `select-core`.
|
|
56
56
|
*
|
|
@@ -13,7 +13,7 @@ export type SelectCoreConfig<T> = {
|
|
|
13
13
|
getCompare: () => (a: T, b: T) => boolean;
|
|
14
14
|
/** When false, typing never enters filtering mode — the popover is a click/keyboard-only picker. @default true */
|
|
15
15
|
getSearchable?: () => boolean;
|
|
16
|
-
/** Optional `canCreate` validator — presence enables creatable mode. */
|
|
16
|
+
/** Optional `createOptions.canCreate` validator — presence enables creatable mode. */
|
|
17
17
|
getCanCreate: () => ((q: string) => boolean) | undefined;
|
|
18
18
|
/** When true, no `kind:'create'` row is emitted (the host renders its own create UI) — keyboard navigation must not reach a row the listbox doesn't show. @default false */
|
|
19
19
|
getHideCreateRow?: () => boolean;
|
|
@@ -2,15 +2,15 @@ import { AppLocale } from '../../core/locale';
|
|
|
2
2
|
const loc = {
|
|
3
3
|
noMatches: { en: 'No matches', no: 'Ingen treff' },
|
|
4
4
|
createLabel: {
|
|
5
|
-
en: (value) =>
|
|
6
|
-
no: (value) =>
|
|
5
|
+
en: (value, prefix = 'Create') => `${prefix} "${value}"`,
|
|
6
|
+
no: (value, prefix = 'Opprett') => `${prefix} "${value}"`
|
|
7
7
|
}
|
|
8
8
|
};
|
|
9
9
|
export class SelectListboxLocalization {
|
|
10
10
|
get noMatches() {
|
|
11
11
|
return loc.noMatches[AppLocale.current];
|
|
12
12
|
}
|
|
13
|
-
|
|
14
|
-
return loc.createLabel[AppLocale.current];
|
|
15
|
-
}
|
|
13
|
+
createLabel = (value, prefix) => {
|
|
14
|
+
return loc.createLabel[AppLocale.current](value, prefix);
|
|
15
|
+
};
|
|
16
16
|
}
|
|
@@ -6,7 +6,7 @@ import IconCheckboxChecked from '@fluentui/svg-icons/icons/checkbox_checked_16_f
|
|
|
6
6
|
import IconCheckboxIndeterminate from '@fluentui/svg-icons/icons/checkbox_indeterminate_16_filled.svg?raw';
|
|
7
7
|
import IconCheckboxUnchecked from '@fluentui/svg-icons/icons/checkbox_unchecked_16_regular.svg?raw';
|
|
8
8
|
import IconCheckmark from '@fluentui/svg-icons/icons/checkmark_16_regular.svg?raw';
|
|
9
|
-
let { triggerEl, isOpen, rows, highlight, loading = false, listboxId, matchTriggerWidth = true, selectedDisplay = 'checkmark', optionSnippet, headerSnippet, bodySnippet, hideCreateRows = false, suppressEmpty = false, on } = $props();
|
|
9
|
+
let { triggerEl, isOpen, rows, highlight, loading = false, listboxId, matchTriggerWidth = true, selectedDisplay = 'checkmark', optionSnippet, headerSnippet, bodySnippet, hideCreateRows = false, createPrefix, suppressEmpty = false, on } = $props();
|
|
10
10
|
const localization = new SelectListboxLocalization();
|
|
11
11
|
// rows keep their index into the full `rows` array — `highlight` and row ids are core-side indices,
|
|
12
12
|
// so filtering without preserving them shifts every row by one and Enter acts on the wrong row
|
|
@@ -149,8 +149,10 @@ $effect(() => {
|
|
|
149
149
|
onclick={() => on.pickRow(row)}
|
|
150
150
|
onkeydown={() => undefined}
|
|
151
151
|
onmouseenter={() => on.hoverRow(index)}>
|
|
152
|
-
|
|
153
|
-
|
|
152
|
+
{#if !createPrefix}
|
|
153
|
+
<span class="select-listbox__row-icon"><Icon src={IconAdd} /></span>
|
|
154
|
+
{/if}
|
|
155
|
+
<span class="select-listbox__row-label">{localization.createLabel(row.query, createPrefix)}</span>
|
|
154
156
|
</div>
|
|
155
157
|
{:else if row.kind === 'group-header'}
|
|
156
158
|
{#if row.selectable}
|
|
@@ -37,6 +37,8 @@ declare function $$render<T>(): {
|
|
|
37
37
|
}]>;
|
|
38
38
|
/** When true, `kind:'create'` rows are skipped (used when `headerSnippet` already renders the create UI). @default false */
|
|
39
39
|
hideCreateRows?: boolean;
|
|
40
|
+
/** Text in front of the query on a `kind:'create'` row, replacing the localized default ("Create"). When set, the leading `+` glyph is dropped. */
|
|
41
|
+
createPrefix?: string;
|
|
40
42
|
/** When true, the panel is not rendered at all if there's nothing to show (no rows, no create, not loading) — suppresses the "No matches" empty state. Used by tag-input style hosts. @default false */
|
|
41
43
|
suppressEmpty?: boolean;
|
|
42
44
|
on: {
|
|
@@ -47,6 +47,13 @@ export type SelectRowCreate = {
|
|
|
47
47
|
query: string;
|
|
48
48
|
};
|
|
49
49
|
export type SelectRow<T> = SelectRowOption<T> | SelectRowGroupHeader<T> | SelectRowCreate;
|
|
50
|
+
/** Creatable mode — passing this object to a select enables the create UI. */
|
|
51
|
+
export type SelectCreateOptions = {
|
|
52
|
+
/** Validates each query — only when it returns true does the create UI appear (and the query isn't a duplicate of an existing label). */
|
|
53
|
+
canCreate: (query: string) => boolean;
|
|
54
|
+
/** Replaces the whole localized text in front of the typed query on the create row: `Create "foo"` → `Invite "foo"`, and drops the leading `+` glyph (the label fills the full row width). Not localized by the kit; pass already-translated text. Has no effect on the create-with-parent section, which keeps its own localized wording. */
|
|
55
|
+
prefix?: string;
|
|
56
|
+
};
|
|
50
57
|
/** Common (data-source-agnostic) prop surface shared by `Singleselect` (sync) and `SingleselectAsync`. */
|
|
51
58
|
export type SingleselectCommonProps<T> = {
|
|
52
59
|
/** Trigger height preset. @default 'md' */
|
|
@@ -72,8 +79,8 @@ export type SingleselectCommonProps<T> = {
|
|
|
72
79
|
groupHeader?: 'static' | 'value';
|
|
73
80
|
/** Equality comparator for `T`. Required when `T` is an object. @default (a, b) => a === b */
|
|
74
81
|
compare?: (a: T, b: T) => boolean;
|
|
75
|
-
/** Enables creatable mode
|
|
76
|
-
|
|
82
|
+
/** Enables creatable mode — a "Create …" row for the current query. See {@link SelectCreateOptions}. */
|
|
83
|
+
createOptions?: SelectCreateOptions;
|
|
77
84
|
/** Leading icon — string SVG source, `{ src, color?, size? }` object, or custom snippet. */
|
|
78
85
|
icon?: IconProp;
|
|
79
86
|
/** Trailing chevron icon override. Same shape as `icon`. */
|
|
@@ -156,8 +163,8 @@ export type MultiselectBaseProps<T> = {
|
|
|
156
163
|
* @default 'checkmark'
|
|
157
164
|
*/
|
|
158
165
|
selectedDisplay?: 'checkmark' | 'checkbox' | 'highlight';
|
|
159
|
-
/** Enables creatable mode.
|
|
160
|
-
|
|
166
|
+
/** Enables creatable mode. When `parentSource` is non-empty, a create-with-parent section replaces the simple "Create …" row. See {@link SelectCreateOptions}. */
|
|
167
|
+
createOptions?: SelectCreateOptions;
|
|
161
168
|
/** Where chips render. `'external'` puts them in a row above the trigger and unlocks `reorderable`. @default 'inline' */
|
|
162
169
|
chipMode?: 'inline' | 'external';
|
|
163
170
|
/** Enables drag-and-drop reorder on the external chip row. Ignored unless `chipMode='external'`. @default false */
|
|
@@ -189,7 +196,7 @@ export type MultiselectBaseProps<T> = {
|
|
|
189
196
|
options: SelectOption<T>[];
|
|
190
197
|
}]>;
|
|
191
198
|
/**
|
|
192
|
-
* Enables the create-with-parent section (shown above the listbox when `canCreate` gates true).
|
|
199
|
+
* Enables the create-with-parent section (shown above the listbox when `createOptions.canCreate` gates true).
|
|
193
200
|
* Receives the FULL parent-candidate list independent of current filter — so the parent picker offers
|
|
194
201
|
* every candidate regardless of what's currently visible in the listbox. When undefined or empty, a
|
|
195
202
|
* single "Create …" row is shown instead.
|