@selvajs/ui 6.6.0-beta.8 → 6.6.0-beta.9

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.
Files changed (93) hide show
  1. package/dist/components/compute/AppLayout.svelte +37 -7
  2. package/dist/components/compute/AppLayout.svelte.d.ts +5 -0
  3. package/dist/components/compute/ParameterPresetManager.svelte +8 -20
  4. package/dist/components/compute/SolveMessages.svelte +26 -3
  5. package/dist/components/compute/SolveMessages.svelte.d.ts +7 -0
  6. package/dist/components/layout/PageFooter.svelte +3 -1
  7. package/dist/components/layout/SideNav.svelte +55 -14
  8. package/dist/components/preview/ChartOutput.svelte +18 -13
  9. package/dist/components/preview/Group.svelte +6 -1
  10. package/dist/components/preview/ImageOutput.svelte +3 -1
  11. package/dist/components/preview/InputControl.svelte +108 -44
  12. package/dist/components/preview/InputControl.svelte.d.ts +5 -1
  13. package/dist/components/preview/TabContent.svelte +61 -29
  14. package/dist/components/preview/TabContent.svelte.d.ts +5 -1
  15. package/dist/components/preview/TabLayout.svelte +25 -3
  16. package/dist/components/preview/TabLayout.svelte.d.ts +5 -0
  17. package/dist/components/preview/inputs/ChecklistInput.svelte +7 -2
  18. package/dist/components/preview/inputs/ChecklistInput.svelte.d.ts +2 -1
  19. package/dist/components/preview/inputs/DropdownInput.svelte +16 -4
  20. package/dist/components/preview/inputs/DropdownInput.svelte.d.ts +2 -1
  21. package/dist/components/preview/inputs/FileInput.svelte +56 -65
  22. package/dist/components/preview/inputs/GeometryInput.svelte +128 -0
  23. package/dist/components/preview/inputs/GeometryInput.svelte.d.ts +21 -0
  24. package/dist/components/preview/inputs/OptionGlyph.svelte +29 -0
  25. package/dist/components/preview/inputs/OptionGlyph.svelte.d.ts +8 -0
  26. package/dist/components/preview/inputs/SegmentedInput.svelte +44 -0
  27. package/dist/components/preview/inputs/SegmentedInput.svelte.d.ts +12 -0
  28. package/dist/components/preview/inputs/TileInput.svelte +80 -0
  29. package/dist/components/preview/inputs/TileInput.svelte.d.ts +12 -0
  30. package/dist/components/preview/inputs/index.d.ts +3 -0
  31. package/dist/components/preview/inputs/index.js +3 -0
  32. package/dist/components/primitives/SolvingIndicator.svelte +28 -3
  33. package/dist/components/primitives/SolvingIndicator.svelte.d.ts +7 -0
  34. package/dist/components/primitives/button/button.svelte +3 -0
  35. package/dist/components/primitives/button/button.svelte.d.ts +3 -0
  36. package/dist/components/primitives/toggle/toggle.svelte +3 -1
  37. package/dist/components/primitives/toggle/toggle.svelte.d.ts +3 -0
  38. package/dist/components/viewer/Viewer.svelte +11 -11
  39. package/dist/compute/solving.svelte.d.ts +7 -1
  40. package/dist/compute/solving.svelte.js +12 -1
  41. package/dist/contexts/hostContext.svelte.d.ts +39 -0
  42. package/dist/contexts/hostContext.svelte.js +9 -0
  43. package/dist/i18n/messages.d.ts +33 -0
  44. package/dist/i18n/messages.js +58 -0
  45. package/dist/index.d.ts +3 -0
  46. package/dist/index.js +3 -0
  47. package/dist/schema/host-support.d.ts +29 -0
  48. package/dist/schema/host-support.js +97 -0
  49. package/dist/schema/option-display.d.ts +12 -0
  50. package/dist/schema/option-display.js +29 -0
  51. package/dist/schema/param-exporter.d.ts +4 -1
  52. package/dist/schema/param-exporter.js +10 -6
  53. package/dist/styles/base.css +6 -3
  54. package/dist/utils/file-download.js +6 -10
  55. package/dist/utils/host-files.d.ts +12 -0
  56. package/dist/utils/host-files.js +46 -0
  57. package/dist/utils/webp-to-png.d.ts +10 -0
  58. package/dist/utils/webp-to-png.js +41 -0
  59. package/package.json +9 -9
  60. package/src/lib/components/compute/AppLayout.svelte +37 -7
  61. package/src/lib/components/compute/ParameterPresetManager.svelte +8 -20
  62. package/src/lib/components/compute/SolveMessages.svelte +26 -3
  63. package/src/lib/components/layout/PageFooter.svelte +3 -1
  64. package/src/lib/components/layout/SideNav.svelte +55 -14
  65. package/src/lib/components/preview/ChartOutput.svelte +18 -13
  66. package/src/lib/components/preview/Group.svelte +6 -1
  67. package/src/lib/components/preview/ImageOutput.svelte +3 -1
  68. package/src/lib/components/preview/InputControl.svelte +108 -44
  69. package/src/lib/components/preview/TabContent.svelte +61 -29
  70. package/src/lib/components/preview/TabLayout.svelte +25 -3
  71. package/src/lib/components/preview/inputs/ChecklistInput.svelte +7 -2
  72. package/src/lib/components/preview/inputs/DropdownInput.svelte +16 -4
  73. package/src/lib/components/preview/inputs/FileInput.svelte +56 -65
  74. package/src/lib/components/preview/inputs/GeometryInput.svelte +128 -0
  75. package/src/lib/components/preview/inputs/OptionGlyph.svelte +29 -0
  76. package/src/lib/components/preview/inputs/SegmentedInput.svelte +44 -0
  77. package/src/lib/components/preview/inputs/TileInput.svelte +80 -0
  78. package/src/lib/components/preview/inputs/index.ts +3 -0
  79. package/src/lib/components/primitives/SolvingIndicator.svelte +28 -3
  80. package/src/lib/components/primitives/button/button.svelte +3 -0
  81. package/src/lib/components/primitives/toggle/toggle.svelte +3 -1
  82. package/src/lib/components/viewer/Viewer.svelte +11 -11
  83. package/src/lib/compute/solving.svelte.ts +16 -1
  84. package/src/lib/contexts/hostContext.svelte.ts +51 -0
  85. package/src/lib/i18n/messages.ts +96 -0
  86. package/src/lib/index.ts +20 -0
  87. package/src/lib/schema/host-support.ts +129 -0
  88. package/src/lib/schema/option-display.ts +44 -0
  89. package/src/lib/schema/param-exporter.ts +10 -7
  90. package/src/lib/styles/base.css +6 -3
  91. package/src/lib/utils/file-download.ts +7 -11
  92. package/src/lib/utils/host-files.ts +61 -0
  93. package/src/lib/utils/webp-to-png.ts +48 -0
@@ -4,6 +4,7 @@
4
4
  FileInputWidgetConfig,
5
5
  DynamicValueListWidgetConfig,
6
6
  DropdownWidgetConfig,
7
+ UISchema,
7
8
  SupportedTypes
8
9
  } from '@selvajs/schemas';
9
10
  import {
@@ -16,6 +17,8 @@
16
17
  isColorWidget
17
18
  } from '@selvajs/schemas';
18
19
  import * as Field from '../primitives/field';
20
+ import { Button } from '../primitives/button';
21
+ import { toast } from '../primitives/sonner';
19
22
  import HelpButton from './HelpButton.svelte';
20
23
  import {
21
24
  CheckboxInput,
@@ -23,11 +26,21 @@
23
26
  ColorInput,
24
27
  DropdownInput,
25
28
  FileInput,
29
+ GeometryInput,
26
30
  NumberInput,
27
- TextInput
31
+ SegmentedInput,
32
+ TextInput,
33
+ TileInput
28
34
  } from './inputs';
29
35
  import { getClientSlot } from '../../contexts/clientSlotContext.svelte';
36
+ import {
37
+ getHostContext,
38
+ type GeometryPickTarget,
39
+ type PickKind
40
+ } from '../../contexts/hostContext.svelte';
30
41
  import { getLocaleContext } from '../../i18n/localeContext.svelte';
42
+ import { isGeometryParamType, isTriggerItem } from '../../schema/host-support';
43
+ import { resolveOptionDisplay } from '../../schema/option-display';
31
44
 
32
45
  interface Props {
33
46
  item: InputLayoutItem;
@@ -42,6 +55,10 @@
42
55
  disabled?: boolean;
43
56
  /** Runtime-computed options for a dynamic value list input (name -> value). */
44
57
  dynamicOptions?: Record<string, string>;
58
+ /** The input's paramType: a geometry input gets its control from this, not from the widget type. */
59
+ paramType?: string;
60
+ /** The schema's icons and thumbnails, which value-list options point into. */
61
+ assets?: UISchema['assets'];
45
62
  }
46
63
 
47
64
  let {
@@ -50,10 +67,25 @@
50
67
  displayName,
51
68
  onChange,
52
69
  disabled = false,
53
- dynamicOptions
70
+ dynamicOptions,
71
+ paramType,
72
+ assets
54
73
  }: Props = $props();
55
74
 
56
75
  const locale = getLocaleContext();
76
+ const hostActions = getHostContext();
77
+ const { pickGeometry, clearGeometry, onGeometrySummary } = hostActions;
78
+ const isGeometry = $derived(isGeometryParamType(paramType));
79
+ const isTrigger = $derived(isTriggerItem(item));
80
+ const pickTarget = $derived(
81
+ isGeometry ? hostActions.describeGeometry?.(item.paramId) : undefined
82
+ );
83
+
84
+ // Get Point/Line/Plane take their own kind; Get Geometry takes what its filter allows.
85
+ function pickKinds(target: GeometryPickTarget | undefined): readonly PickKind[] | undefined {
86
+ if (paramType === 'point' || paramType === 'line' || paramType === 'plane') return [paramType];
87
+ return target?.kinds;
88
+ }
57
89
 
58
90
  const inputId = $derived(`input-${item.paramId}`);
59
91
  const label = $derived(displayName || item.displayName || item.paramId);
@@ -102,7 +134,8 @@
102
134
  // As a dropdown config so DropdownInput/ChecklistInput can consume it unchanged.
103
135
  const dynamicListAsDropdownConfig = $derived<DropdownWidgetConfig>({
104
136
  options: dynamicListOptions,
105
- displayAs: dynamicListConfig?.displayAs ?? 'dropdown'
137
+ displayAs: dynamicListConfig?.displayAs ?? 'dropdown',
138
+ optionMedia: dynamicListConfig?.optionMedia
106
139
  });
107
140
  const hideDynamicListWhenEmpty = $derived(
108
141
  isDynamicValueListWidget(item) &&
@@ -173,6 +206,48 @@
173
206
  }
174
207
  </script>
175
208
 
209
+ {#snippet valueList(config: DropdownWidgetConfig)}
210
+ {@const display = resolveOptionDisplay(
211
+ config.displayAs,
212
+ Object.keys(config.options ?? {}).length
213
+ )}
214
+ {@const single = typeof value === 'string' ? value : ''}
215
+ {#if display === 'checklist'}
216
+ <ChecklistInput
217
+ {inputId}
218
+ value={Array.isArray(value)
219
+ ? (value as string[])
220
+ : typeof value === 'string' && value
221
+ ? [value]
222
+ : []}
223
+ {config}
224
+ {assets}
225
+ onChange={commit}
226
+ {disabled}
227
+ />
228
+ {:else if display === 'segmented'}
229
+ <SegmentedInput
230
+ labelId="{inputId}-label"
231
+ value={single}
232
+ {config}
233
+ {assets}
234
+ onChange={commit}
235
+ {disabled}
236
+ />
237
+ {:else if display === 'tiles'}
238
+ <TileInput
239
+ labelId="{inputId}-label"
240
+ value={single}
241
+ {config}
242
+ {assets}
243
+ onChange={commit}
244
+ {disabled}
245
+ />
246
+ {:else}
247
+ <DropdownInput value={single} {config} {assets} onChange={commit} {disabled} />
248
+ {/if}
249
+ {/snippet}
250
+
176
251
  {#snippet labelText(forId?: string, tone: 'quiet' | 'strong' = 'quiet')}
177
252
  <Field.Label
178
253
  for={forId}
@@ -218,6 +293,34 @@
218
293
  })}
219
294
  </div>
220
295
  {/if}
296
+ {:else if isGeometry}
297
+ <div role="group" class="group/field gap-1.5 min-w-0 flex flex-col">
298
+ {@render fieldHeader(inputId)}
299
+ <GeometryInput
300
+ {inputId}
301
+ kinds={pickKinds(pickTarget)}
302
+ atMost={pickTarget?.atMost}
303
+ summary={typeof value === 'string' ? value : undefined}
304
+ onPick={pickGeometry && (() => pickGeometry(item.paramId))}
305
+ onClear={clearGeometry && (() => clearGeometry(item.paramId))}
306
+ watchSummary={onGeometrySummary && ((listener) => onGeometrySummary(item.paramId, listener))}
307
+ onError={(message) => toast.error(message)}
308
+ {disabled}
309
+ />
310
+ </div>
311
+ {:else if isTrigger}
312
+ <div role="group" class="group/field gap-1.5 min-w-0 flex items-center" data-control-line>
313
+ <Button
314
+ id={inputId}
315
+ variant="outline"
316
+ class="min-w-0 flex-1"
317
+ onclick={() => hostActions.trigger?.(item.paramId)}
318
+ disabled={disabled || !hostActions.trigger || hostActions.isSolving?.()}
319
+ >
320
+ <span class="truncate">{label}</span>
321
+ </Button>
322
+ {@render helpButton()}
323
+ </div>
221
324
  {:else if !hideDynamicListWhenEmpty}
222
325
  <div role="group" class="group/field gap-1.5 min-w-0 flex flex-col">
223
326
  {#if !isCheckboxWidget(item)}
@@ -254,49 +357,10 @@
254
357
  {disabled}
255
358
  />
256
359
  {:else if isDropdownWidget(item)}
257
- {@const config = item.config}
258
- {#if config.displayAs === 'checklist'}
259
- <ChecklistInput
260
- {inputId}
261
- value={Array.isArray(value)
262
- ? (value as string[])
263
- : typeof value === 'string' && value
264
- ? [value]
265
- : []}
266
- {config}
267
- onChange={commit}
268
- {disabled}
269
- />
270
- {:else}
271
- <DropdownInput
272
- value={typeof value === 'string' ? value : ''}
273
- {config}
274
- onChange={commit}
275
- {disabled}
276
- />
277
- {/if}
360
+ {@render valueList(item.config)}
278
361
  {:else if isDynamicValueListWidget(item)}
279
362
  {#if dynamicListHasOptions}
280
- {#if dynamicListAsDropdownConfig.displayAs === 'checklist'}
281
- <ChecklistInput
282
- {inputId}
283
- value={Array.isArray(value)
284
- ? (value as string[])
285
- : typeof value === 'string' && value
286
- ? [value]
287
- : []}
288
- config={dynamicListAsDropdownConfig}
289
- onChange={commit}
290
- {disabled}
291
- />
292
- {:else}
293
- <DropdownInput
294
- value={typeof value === 'string' ? value : ''}
295
- config={dynamicListAsDropdownConfig}
296
- onChange={commit}
297
- {disabled}
298
- />
299
- {/if}
363
+ {@render valueList(dynamicListAsDropdownConfig)}
300
364
  {:else}
301
365
  <p class="text-sm text-muted-foreground">{locale.messages.noOptionsYet}</p>
302
366
  {/if}
@@ -1,4 +1,4 @@
1
- import type { InputLayoutItem, SupportedTypes } from '@selvajs/schemas';
1
+ import type { InputLayoutItem, UISchema, SupportedTypes } from '@selvajs/schemas';
2
2
  interface Props {
3
3
  item: InputLayoutItem;
4
4
  value?: SupportedTypes;
@@ -12,6 +12,10 @@ interface Props {
12
12
  disabled?: boolean;
13
13
  /** Runtime-computed options for a dynamic value list input (name -> value). */
14
14
  dynamicOptions?: Record<string, string>;
15
+ /** The input's paramType: a geometry input gets its control from this, not from the widget type. */
16
+ paramType?: string;
17
+ /** The schema's icons and thumbnails, which value-list options point into. */
18
+ assets?: UISchema['assets'];
15
19
  }
16
20
  declare const InputControl: import("svelte").Component<Props, {}, "value">;
17
21
  type InputControl = ReturnType<typeof InputControl>;
@@ -3,6 +3,7 @@
3
3
  TabConfig,
4
4
  SchemaInput,
5
5
  DiscoveredOutput,
6
+ UISchema,
6
7
  SupportedTypes,
7
8
  InputLayoutItem,
8
9
  OutputLayoutItem
@@ -14,6 +15,8 @@
14
15
  import InputControl from './InputControl.svelte';
15
16
  import OutputDisplay from './OutputDisplay.svelte';
16
17
  import { evaluateGroupVisibility } from '../../schema/visibility-rules';
18
+ import { isSupportedOn, layoutItemKind } from '../../schema/host-support';
19
+ import { getHostContext } from '../../contexts/hostContext.svelte';
17
20
  import { getLocaleContext } from '../../i18n/localeContext.svelte';
18
21
 
19
22
  const locale = getLocaleContext();
@@ -29,6 +32,10 @@
29
32
  outputs: DiscoveredOutput[];
30
33
  /** Computed value list options keyed by the target dynamic-value-list input id. */
31
34
  dynamicOptions?: Record<string, Record<string, string>>;
35
+ /** The schema's icons and thumbnails, which value-list options point into. */
36
+ assets?: UISchema['assets'];
37
+ /** No tinted backdrop and no scroll area of its own: the container scrolls. */
38
+ bare?: boolean;
32
39
  }
33
40
 
34
41
  let {
@@ -39,11 +46,24 @@
39
46
  onValueChange,
40
47
  inputs,
41
48
  outputs,
42
- dynamicOptions = {}
49
+ dynamicOptions = {},
50
+ assets,
51
+ bare = false
43
52
  }: Props = $props();
44
53
 
54
+ const { host } = getHostContext();
55
+
56
+ // An element this host can't render is left out, not shown broken: its input still solves with
57
+ // the definition's own default. A group left with nothing to show goes too.
45
58
  const visibleGroups = $derived(
46
- tab.groups.filter((group) => evaluateGroupVisibility(group, values))
59
+ tab.groups
60
+ .filter((group) => evaluateGroupVisibility(group, values))
61
+ .flatMap((group) => {
62
+ const items = group.items.filter((item) =>
63
+ isSupportedOn(host, layoutItemKind(item, inputs))
64
+ );
65
+ return items.length > 0 || group.items.length === 0 ? [{ ...group, items }] : [];
66
+ })
47
67
  );
48
68
 
49
69
  function getInputById(paramId: string): SchemaInput | undefined {
@@ -68,6 +88,8 @@
68
88
  onChange={onValueChange}
69
89
  disabled={visibility.disabled}
70
90
  dynamicOptions={dynamicOptions[input.id]}
91
+ paramType={input.paramType}
92
+ {assets}
71
93
  />
72
94
  {/if}
73
95
  {/snippet}
@@ -83,34 +105,44 @@
83
105
  {/if}
84
106
  {/snippet}
85
107
 
86
- <Tabs.Content value={tab.id} class="min-h-0">
87
- <div class="h-full bg-muted dark:bg-background">
88
- <ScrollArea class="h-full" orientation="vertical">
89
- <div class="px-3">
90
- <!-- A heading per panel lets a screen reader jump between them; the tab, when shown,
108
+ {#snippet body()}
109
+ <div class={bare ? 'px-1' : 'px-3'}>
110
+ <!-- A heading per panel lets a screen reader jump between them; the tab, when shown,
91
111
  already names it on screen. -->
92
- <h2 class="sr-only">{tab.label}</h2>
93
- {#if visibleGroups.length === 0}
94
- <StateDisplay type="empty" size="medium" message={t.tabNoGroups} />
95
- {:else}
96
- <div class="gap-3 py-3 flex flex-col">
97
- {#each visibleGroups as group (group.id)}
98
- <Group
99
- label={group.label}
100
- description={group.description}
101
- items={group.items}
102
- columns={group.columns ?? 1}
103
- collapsed={collapsedGroups[group.id] ?? false}
104
- {values}
105
- onToggle={() => onToggleGroup(group.id)}
106
- inputSnippet={renderInput}
107
- outputSnippet={renderOutput}
108
- flat={visibleGroups.length === 1}
109
- />
110
- {/each}
111
- </div>
112
- {/if}
112
+ <h2 class="sr-only">{tab.label}</h2>
113
+ {#if visibleGroups.length === 0}
114
+ <StateDisplay type="empty" size="medium" message={t.tabNoGroups} />
115
+ {:else}
116
+ <div class="gap-3 py-3 flex flex-col">
117
+ {#each visibleGroups as group (group.id)}
118
+ <Group
119
+ label={group.label}
120
+ description={group.description}
121
+ items={group.items}
122
+ columns={group.columns ?? 1}
123
+ collapsed={collapsedGroups[group.id] ?? false}
124
+ {values}
125
+ onToggle={() => onToggleGroup(group.id)}
126
+ inputSnippet={renderInput}
127
+ outputSnippet={renderOutput}
128
+ flat={visibleGroups.length === 1}
129
+ />
130
+ {/each}
113
131
  </div>
114
- </ScrollArea>
132
+ {/if}
115
133
  </div>
134
+ {/snippet}
135
+
136
+ <Tabs.Content value={tab.id} class="min-h-0">
137
+ {#if bare}
138
+ {@render body()}
139
+ {:else}
140
+ <div class="h-full bg-muted dark:bg-background">
141
+ <!-- `auto`, not the default `hover`: a hover-only bar comes and goes with the pointer,
142
+ and a host that freezes mid-solve drops the pointer state with it. -->
143
+ <ScrollArea class="h-full" orientation="vertical" type="auto">
144
+ {@render body()}
145
+ </ScrollArea>
146
+ </div>
147
+ {/if}
116
148
  </Tabs.Content>
@@ -1,4 +1,4 @@
1
- import type { TabConfig, SchemaInput, DiscoveredOutput, SupportedTypes } from '@selvajs/schemas';
1
+ import type { TabConfig, SchemaInput, DiscoveredOutput, UISchema, SupportedTypes } from '@selvajs/schemas';
2
2
  interface Props {
3
3
  tab: TabConfig;
4
4
  values: Record<string, unknown>;
@@ -9,6 +9,10 @@ interface Props {
9
9
  outputs: DiscoveredOutput[];
10
10
  /** Computed value list options keyed by the target dynamic-value-list input id. */
11
11
  dynamicOptions?: Record<string, Record<string, string>>;
12
+ /** The schema's icons and thumbnails, which value-list options point into. */
13
+ assets?: UISchema['assets'];
14
+ /** No tinted backdrop and no scroll area of its own: the container scrolls. */
15
+ bare?: boolean;
12
16
  }
13
17
  declare const TabContent: import("svelte").Component<Props, {}, "">;
14
18
  type TabContent = ReturnType<typeof TabContent>;
@@ -15,9 +15,21 @@
15
15
  panelFilter?: 'left' | 'right';
16
16
  /** Externally request a specific tab to be active (e.g. from collapsed strip click) */
17
17
  requestedTabId?: string | null;
18
+ /**
19
+ * Drops the card frame and the tab's own scroll area, for a container that already frames
20
+ * and scrolls it (the phone drawer). Nested, the inner scroll gets only a sliver of height.
21
+ */
22
+ bare?: boolean;
18
23
  }
19
24
 
20
- let { schema, values, onValueChange, panelFilter, requestedTabId = null }: Props = $props();
25
+ let {
26
+ schema,
27
+ values,
28
+ onValueChange,
29
+ panelFilter,
30
+ requestedTabId = null,
31
+ bare = false
32
+ }: Props = $props();
21
33
 
22
34
  let activeTabId = $state('');
23
35
  let collapsedGroups = $state<Record<string, boolean>>({});
@@ -66,7 +78,7 @@
66
78
  }
67
79
  </script>
68
80
 
69
- <Card.Root variant="flush" class="min-h-0 mx-1 flex-1">
81
+ {#snippet tabs()}
70
82
  <Tabs.Root value={activeTabId} variant="line" class="min-h-0 flex-1">
71
83
  {#if showTabBar}
72
84
  <TabBar tabs={visibleTabs} {activeTabId} onTabChange={handleTabChange} />
@@ -82,7 +94,17 @@
82
94
  inputs={schema.inputs}
83
95
  outputs={schema.outputs}
84
96
  {dynamicOptions}
97
+ assets={schema.assets}
98
+ {bare}
85
99
  />
86
100
  {/each}
87
101
  </Tabs.Root>
88
- </Card.Root>
102
+ {/snippet}
103
+
104
+ {#if bare}
105
+ {@render tabs()}
106
+ {:else}
107
+ <Card.Root variant="flush" class="min-h-0 mx-1 flex-1">
108
+ {@render tabs()}
109
+ </Card.Root>
110
+ {/if}
@@ -7,6 +7,11 @@ interface Props {
7
7
  panelFilter?: 'left' | 'right';
8
8
  /** Externally request a specific tab to be active (e.g. from collapsed strip click) */
9
9
  requestedTabId?: string | null;
10
+ /**
11
+ * Drops the card frame and the tab's own scroll area, for a container that already frames
12
+ * and scrolls it (the phone drawer). Nested, the inner scroll gets only a sliver of height.
13
+ */
14
+ bare?: boolean;
10
15
  }
11
16
  declare const TabLayout: import("svelte").Component<Props, {}, "">;
12
17
  type TabLayout = ReturnType<typeof TabLayout>;
@@ -1,17 +1,20 @@
1
1
  <script lang="ts">
2
- import type { DropdownWidgetConfig, SupportedTypes } from '@selvajs/schemas';
2
+ import type { DropdownWidgetConfig, SupportedTypes, UISchema } from '@selvajs/schemas';
3
3
  import { Checkbox } from '../../primitives/checkbox';
4
+ import { optionGlyph } from '../../../schema/option-display';
5
+ import OptionGlyph from './OptionGlyph.svelte';
4
6
  import { SvelteSet } from 'svelte/reactivity';
5
7
 
6
8
  interface Props {
7
9
  inputId: string;
8
10
  value?: string[];
9
11
  config?: DropdownWidgetConfig;
12
+ assets?: UISchema['assets'];
10
13
  onChange: (value: SupportedTypes) => void;
11
14
  disabled?: boolean;
12
15
  }
13
16
 
14
- let { inputId, value, config, onChange, disabled = false }: Props = $props();
17
+ let { inputId, value, config, assets, onChange, disabled = false }: Props = $props();
15
18
 
16
19
  const options = $derived(config?.options ?? {});
17
20
  const selected = $derived(new Set(value ?? []));
@@ -32,6 +35,7 @@
32
35
  {@const optionValue = expr ?? name}
33
36
  {@const optionId = `${inputId}-${optionValue}`}
34
37
  {@const isSelected = selected.has(optionValue)}
38
+ {@const glyph = optionGlyph(config?.optionMedia?.[name], assets)}
35
39
  <label
36
40
  for={optionId}
37
41
  class="gap-3 px-3 py-2 text-sm flex items-center transition-colors select-none hover:bg-accent/50 {isSelected
@@ -44,6 +48,7 @@
44
48
  onCheckedChange={(state) => toggle(optionValue, state === true)}
45
49
  {disabled}
46
50
  />
51
+ {#if glyph}<OptionGlyph {glyph} class="text-muted-foreground" />{/if}
47
52
  <span class="flex-1" class:font-medium={isSelected}>{name}</span>
48
53
  </label>
49
54
  {/each}
@@ -1,8 +1,9 @@
1
- import type { DropdownWidgetConfig, SupportedTypes } from '@selvajs/schemas';
1
+ import type { DropdownWidgetConfig, SupportedTypes, UISchema } from '@selvajs/schemas';
2
2
  interface Props {
3
3
  inputId: string;
4
4
  value?: string[];
5
5
  config?: DropdownWidgetConfig;
6
+ assets?: UISchema['assets'];
6
7
  onChange: (value: SupportedTypes) => void;
7
8
  disabled?: boolean;
8
9
  }
@@ -1,16 +1,19 @@
1
1
  <script lang="ts">
2
- import type { DropdownWidgetConfig, SupportedTypes } from '@selvajs/schemas';
2
+ import type { DropdownWidgetConfig, SupportedTypes, UISchema } from '@selvajs/schemas';
3
3
  import * as Select from '../../primitives/select';
4
+ import { optionGlyph } from '../../../schema/option-display';
5
+ import OptionGlyph from './OptionGlyph.svelte';
4
6
  import { getLocaleContext } from '../../../i18n/localeContext.svelte';
5
7
 
6
8
  interface Props {
7
9
  value?: string;
8
10
  config?: DropdownWidgetConfig;
11
+ assets?: UISchema['assets'];
9
12
  onChange: (value: SupportedTypes) => void;
10
13
  disabled?: boolean;
11
14
  }
12
15
 
13
- let { value, config, onChange, disabled = false }: Props = $props();
16
+ let { value, config, assets, onChange, disabled = false }: Props = $props();
14
17
 
15
18
  const locale = getLocaleContext();
16
19
 
@@ -19,6 +22,8 @@
19
22
  const currentLabel = $derived(
20
23
  Object.entries(options).find(([_, expr]) => expr === currentValue)?.[0] ?? currentValue
21
24
  );
25
+ const glyphOf = (name: string) => optionGlyph(config?.optionMedia?.[name], assets);
26
+ const currentGlyph = $derived(glyphOf(currentLabel));
22
27
  </script>
23
28
 
24
29
  <Select.Root
@@ -33,11 +38,18 @@
33
38
  {disabled}
34
39
  >
35
40
  <Select.Trigger class="w-full" {disabled}>
36
- {currentLabel || locale.messages.selectOption}
41
+ <span class="gap-2 min-w-0 flex items-center">
42
+ {#if currentGlyph}<OptionGlyph glyph={currentGlyph} />{/if}
43
+ <span class="truncate">{currentLabel || locale.messages.selectOption}</span>
44
+ </span>
37
45
  </Select.Trigger>
38
46
  <Select.Content>
39
47
  {#each Object.entries(options) as [name, expr] (name)}
40
- <Select.Item value={expr ?? name} label={name} />
48
+ {@const glyph = glyphOf(name)}
49
+ <Select.Item value={expr ?? name} label={name}>
50
+ {#if glyph}<OptionGlyph {glyph} />{/if}
51
+ {name}
52
+ </Select.Item>
41
53
  {/each}
42
54
  </Select.Content>
43
55
  </Select.Root>
@@ -1,7 +1,8 @@
1
- import type { DropdownWidgetConfig, SupportedTypes } from '@selvajs/schemas';
1
+ import type { DropdownWidgetConfig, SupportedTypes, UISchema } from '@selvajs/schemas';
2
2
  interface Props {
3
3
  value?: string;
4
4
  config?: DropdownWidgetConfig;
5
+ assets?: UISchema['assets'];
5
6
  onChange: (value: SupportedTypes) => void;
6
7
  disabled?: boolean;
7
8
  }