@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.
@@ -1,7 +1,9 @@
1
- <script lang="ts">import { Icon } from '../icon';
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
- const { children, title, actions, windowToggleActive = false, scrollable = false, on } = $props();
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 on?.windowToggle}
22
- <button
23
- type="button"
24
- class="page-layout__window-toggle"
25
- class:page-layout__window-toggle--active={windowToggleActive}
26
- onclick={on.windowToggle}
27
- aria-label={localization.toggleNavigation}
28
- aria-pressed={windowToggleActive}>
29
- <Icon src={IconWindow} />
30
- </button>
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
- window-toggle button) above a flex-column body that typically contains a `PagePanels` row.
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
- Pass `on.windowToggle` to surface a navigation-toggle button at the header's right edge wire it to
45
- whatever sidebar / drawer state your app owns. Omit it to hide the button. Set `scrollable` to let
46
- the whole page scroll as one document with sticky panel headers.
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-layout__window-toggle {
90
- --sc-kit--icon--size: 1.125rem;
91
- display: inline-flex;
106
+ .page-layout__controls {
107
+ display: flex;
92
108
  align-items: center;
93
- justify-content: center;
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
- /** Visual + ARIA state for the window-toggle button. Set `true` when the navigation it controls is hidden / collapsed — the button gets `aria-pressed` and an active background. @default false */
7
- windowToggleActive?: boolean;
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
- * window-toggle button) above a flex-column body that typically contains a `PagePanels` row.
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
- * Pass `on.windowToggle` to surface a navigation-toggle button at the header's right edge wire it to
19
- * whatever sidebar / drawer state your app owns. Omit it to hide the button. Set `scrollable` to let
20
- * the whole page scroll as one document with sticky panel headers.
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;
@@ -2,4 +2,6 @@ export declare class PageLayoutLocalization {
2
2
  get resizePanel(): string;
3
3
  get togglePanel(): string;
4
4
  get toggleNavigation(): string;
5
+ get togglePanels(): string;
6
+ get close(): string;
5
7
  }
@@ -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', canCreate, icon, chevronIcon, optionSnippet, selectionSnippet, id, name, autocomplete = 'off', 'aria-label': ariaLabel, 'aria-describedby': ariaDescribedby, 'aria-required': ariaRequired, on } = $props();
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
- canCreate={canCreate}
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. The predicate validates each query. 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. */
61
- canCreate?: (query: string) => boolean;
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, canCreate, icon, chevronIcon, optionSnippet, selectionSnippet, listbox, id, name, autocomplete = 'off', 'aria-label': ariaLabel, 'aria-describedby': ariaDescribedby, 'aria-required': ariaRequired, ...mode } = $props();
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', canCreate, icon, chevronIcon, optionSnippet, selectionSnippet, listbox, id, name, autocomplete = 'off', 'aria-label': ariaLabel, 'aria-describedby': ariaDescribedby, 'aria-required': ariaRequired, ...mode } = $props();
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
- canCreate={canCreate}
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
- canCreate={canCreate}
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', canCreate, 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();
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 `canCreate` + groups in items),
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 `canCreate` + groups in items),
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;
@@ -1,4 +1,4 @@
1
1
  export declare class SelectListboxLocalization {
2
2
  get noMatches(): string;
3
- get createLabel(): (value: string) => string;
3
+ createLabel: (value: string, prefix?: string) => string;
4
4
  }
@@ -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) => `Create "${value}"`,
6
- no: (value) => `Opprett "${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
- get createLabel() {
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
- <span class="select-listbox__row-icon"><Icon src={IconAdd} /></span>
153
- <span class="select-listbox__row-label">{localization.createLabel(row.query)}</span>
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. The predicate validates each query only when it returns true does the "Create …" row appear (and isn't a duplicate of an existing label). */
76
- canCreate?: (query: string) => boolean;
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. The predicate validates each query — only when it returns true does the create UI appear. When `parentSource` is non-empty, a create-with-parent section replaces the simple "Create …" row. */
160
- canCreate?: (query: string) => boolean;
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@streamscloud/kit",
3
- "version": "0.25.2",
3
+ "version": "0.27.0",
4
4
  "author": "StreamsCloud",
5
5
  "repository": {
6
6
  "type": "git",