@selvajs/ui 6.6.0-beta.16 → 6.6.0-beta.18

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 (46) hide show
  1. package/dist/components/layout/AppShell.svelte +1 -1
  2. package/dist/components/layout/PageContent.svelte +3 -2
  3. package/dist/components/layout/PageHeader.svelte +1 -1
  4. package/dist/components/preview/Group.svelte +2 -2
  5. package/dist/components/preview/InputControl.svelte +3 -3
  6. package/dist/components/preview/TabContent.svelte +1 -1
  7. package/dist/components/preview/inputs/SegmentedInput.svelte +2 -2
  8. package/dist/components/preview/inputs/TileInput.svelte +4 -2
  9. package/dist/components/primitives/Callout.svelte +1 -1
  10. package/dist/components/primitives/FilterableDropdown.svelte +13 -1
  11. package/dist/components/primitives/FilterableDropdown.svelte.d.ts +5 -0
  12. package/dist/components/primitives/tabs/tabs-list.svelte +1 -1
  13. package/dist/components/primitives/tabs/tabs-list.svelte.d.ts +3 -3
  14. package/dist/components/primitives/tabs/tabs-trigger.svelte +1 -1
  15. package/dist/components/primitives/tabs/tabs-trigger.svelte.d.ts +3 -3
  16. package/dist/components/primitives/toggle/toggle.svelte +1 -1
  17. package/dist/components/primitives/toggle/toggle.svelte.d.ts +3 -3
  18. package/dist/components/primitives/toggle-group/toggle-group.svelte +1 -1
  19. package/dist/components/viewer/ViewerDock.svelte +9 -9
  20. package/dist/index.d.ts +2 -0
  21. package/dist/index.js +2 -0
  22. package/dist/public.d.ts +1 -0
  23. package/dist/public.js +1 -0
  24. package/dist/schema/visibility-rules.d.ts +7 -1
  25. package/dist/schema/visibility-rules.js +17 -0
  26. package/dist/styles/base.css +49 -44
  27. package/package.json +6 -5
  28. package/src/lib/components/layout/AppShell.svelte +1 -1
  29. package/src/lib/components/layout/PageContent.svelte +3 -2
  30. package/src/lib/components/layout/PageHeader.svelte +1 -1
  31. package/src/lib/components/preview/Group.svelte +2 -2
  32. package/src/lib/components/preview/InputControl.svelte +3 -3
  33. package/src/lib/components/preview/TabContent.svelte +1 -1
  34. package/src/lib/components/preview/inputs/SegmentedInput.svelte +2 -2
  35. package/src/lib/components/preview/inputs/TileInput.svelte +4 -2
  36. package/src/lib/components/primitives/Callout.svelte +1 -1
  37. package/src/lib/components/primitives/FilterableDropdown.svelte +13 -1
  38. package/src/lib/components/primitives/tabs/tabs-list.svelte +1 -1
  39. package/src/lib/components/primitives/tabs/tabs-trigger.svelte +1 -1
  40. package/src/lib/components/primitives/toggle/toggle.svelte +1 -1
  41. package/src/lib/components/primitives/toggle-group/toggle-group.svelte +1 -1
  42. package/src/lib/components/viewer/ViewerDock.svelte +9 -9
  43. package/src/lib/index.ts +2 -0
  44. package/src/lib/public.ts +1 -0
  45. package/src/lib/schema/visibility-rules.ts +27 -1
  46. package/src/lib/styles/base.css +49 -44
@@ -101,7 +101,7 @@
101
101
  // from the main column's left edge — that one spans the full body height.
102
102
  const mainClass = $derived(
103
103
  _mode === 'fixed'
104
- ? 'min-h-0 flex-1 overflow-y-auto'
104
+ ? 'min-h-0 min-w-0 flex-1 overflow-y-auto'
105
105
  : 'min-w-0 flex-1 overflow-y-auto border-border sm:border-l'
106
106
  );
107
107
  </script>
@@ -7,6 +7,7 @@
7
7
  let { class: className = '', children }: PageContentProps = $props();
8
8
  </script>
9
9
 
10
- <main class={`px-(--page-px) py-(--page-py) ${className}`}>
10
+ <!-- One content width for every page, so moving between pages doesn't change the frame. -->
11
+ <div class={`max-w-6xl min-w-0 mx-auto w-full px-(--page-px) py-(--page-py) ${className}`}>
11
12
  {@render children?.()}
12
- </main>
13
+ </div>
@@ -34,7 +34,7 @@
34
34
  <div class="gap-2 sm:gap-5 flex h-(--header-h) items-center px-(--page-px)">
35
35
  <!-- Logo -->
36
36
  <a href={homeUrl} class="gap-2 flex shrink-0 items-center" aria-label={brandName}>
37
- <img src={logo} alt="" aria-hidden="true" class="h-5 w-5" />
37
+ <img src={logo} alt="" aria-hidden="true" class="h-7 w-7" />
38
38
  <!-- On a phone the page title needs the room more than the brand does. -->
39
39
  <span
40
40
  class="font-semibold text-sm tracking-tight {title !== undefined ? 'sm:inline hidden' : ''}"
@@ -107,7 +107,7 @@
107
107
  {/if}
108
108
  <div class="content-wrapper" class:collapsed inert={collapsed}>
109
109
  <div class="content-inner">
110
- <div class="px-4 pt-1 pb-4">
110
+ <div class="px-4 pt-2 pb-5">
111
111
  {@render grid()}
112
112
  </div>
113
113
  </div>
@@ -152,7 +152,7 @@
152
152
  .schema-grid {
153
153
  display: grid;
154
154
  grid-template-columns: repeat(var(--schema-cols), minmax(0, 1fr));
155
- gap: 1rem var(--column-gap);
155
+ gap: 1.25rem var(--column-gap);
156
156
  align-items: start;
157
157
  }
158
158
 
@@ -283,7 +283,7 @@
283
283
 
284
284
  {#if isClientSlot}
285
285
  {#if clientSlot}
286
- <div role="group" class="group/field gap-1.5 min-w-0 flex flex-col">
286
+ <div role="group" class="group/field gap-2 min-w-0 flex flex-col">
287
287
  {@render fieldHeader()}
288
288
  {@render clientSlot({
289
289
  inputId: item.paramId,
@@ -294,7 +294,7 @@
294
294
  </div>
295
295
  {/if}
296
296
  {:else if isGeometry}
297
- <div role="group" class="group/field gap-1.5 min-w-0 flex flex-col">
297
+ <div role="group" class="group/field gap-2 min-w-0 flex flex-col">
298
298
  {@render fieldHeader(inputId)}
299
299
  <GeometryInput
300
300
  {inputId}
@@ -322,7 +322,7 @@
322
322
  {@render helpButton()}
323
323
  </div>
324
324
  {:else if !hideDynamicListWhenEmpty}
325
- <div role="group" class="group/field gap-1.5 min-w-0 flex flex-col">
325
+ <div role="group" class="group/field gap-2 min-w-0 flex flex-col">
326
326
  {#if !isCheckboxWidget(item)}
327
327
  {@render fieldHeader(inputId)}
328
328
  {/if}
@@ -113,7 +113,7 @@
113
113
  {#if visibleGroups.length === 0}
114
114
  <StateDisplay type="empty" size="medium" message={t.tabNoGroups} />
115
115
  {:else}
116
- <div class="gap-3 py-3 flex flex-col">
116
+ <div class="gap-4 py-4 flex flex-col">
117
117
  {#each visibleGroups as group (group.id)}
118
118
  <Group
119
119
  label={group.label}
@@ -23,8 +23,8 @@
23
23
 
24
24
  <ToggleGroup.Root
25
25
  type="single"
26
- variant="segmented"
27
- spacing={1}
26
+ variant="outline"
27
+ spacing={0}
28
28
  size={hasIcons ? 'stacked' : 'default'}
29
29
  class="w-full"
30
30
  aria-labelledby={labelId}
@@ -52,11 +52,13 @@
52
52
  onclick={() => onChange(option.value)}
53
53
  onkeydown={(e) => onKeydown(e, index)}
54
54
  class="gap-1.5 p-1.5 min-w-0 flex flex-col rounded-lg border text-left transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 {selected
55
- ? 'border-primary bg-primary/5 ring-1 ring-primary'
55
+ ? 'border-primary bg-primary/10 ring-1 ring-primary'
56
56
  : 'border-input hover:bg-hover'}"
57
57
  >
58
58
  <span
59
- class="flex aspect-square w-full items-center justify-center overflow-hidden rounded-md bg-muted text-muted-foreground"
59
+ class="flex aspect-square w-full items-center justify-center overflow-hidden rounded-md bg-muted {selected
60
+ ? 'text-foreground'
61
+ : 'text-muted-foreground'}"
60
62
  >
61
63
  {#if glyph?.kind === 'image'}
62
64
  <OptionGlyph {glyph} class="size-full" />
@@ -55,7 +55,7 @@
55
55
  {#if Icon}
56
56
  <Icon class={cn('mt-0.5 h-4 w-4 shrink-0', accent)} />
57
57
  {/if}
58
- <div class="min-w-0 space-y-1">
58
+ <div class="min-w-0 space-y-1 wrap-anywhere">
59
59
  {#if title}
60
60
  <p class={cn('leading-tight font-medium', accent)}>{title}</p>
61
61
  {/if}
@@ -11,6 +11,7 @@
11
11
  label: string;
12
12
  icon?: Component;
13
13
  iconClass?: string;
14
+ disabled?: boolean;
14
15
  }
15
16
 
16
17
  interface Props {
@@ -24,6 +25,10 @@
24
25
  /** Above this many items the list gets a filter box. */
25
26
  searchThreshold?: number;
26
27
  disabled?: boolean;
28
+ /** Submits the picked id with a plain <form>, like a native select's name. */
29
+ name?: string;
30
+ size?: 'default' | 'sm';
31
+ 'aria-label'?: string;
27
32
  class?: string;
28
33
  }
29
34
 
@@ -37,6 +42,9 @@
37
42
  searchPlaceholder = 'Filter...',
38
43
  searchThreshold = 4,
39
44
  disabled = false,
45
+ name,
46
+ size = 'default',
47
+ 'aria-label': ariaLabel,
40
48
  class: className
41
49
  }: Props = $props();
42
50
 
@@ -51,14 +59,17 @@
51
59
  }
52
60
  </script>
53
61
 
62
+ {#if name}<input type="hidden" {name} value={value ?? ''} />{/if}
63
+
54
64
  <Popover.Root bind:open>
55
65
  <Popover.Trigger
56
66
  {id}
57
67
  {disabled}
58
68
  role="combobox"
59
69
  aria-expanded={open}
70
+ aria-label={ariaLabel}
60
71
  class={cn(
61
- buttonVariants({ variant: 'outline' }),
72
+ buttonVariants({ variant: 'outline', size }),
62
73
  'font-normal w-full justify-start',
63
74
  className
64
75
  )}
@@ -85,6 +96,7 @@
85
96
  value={item.id}
86
97
  keywords={[item.label]}
87
98
  data-checked={item.id === value}
99
+ disabled={item.disabled}
88
100
  onSelect={() => pick(item)}
89
101
  >
90
102
  {#if ItemIcon}
@@ -4,6 +4,7 @@ export interface FilterableDropdownItem {
4
4
  label: string;
5
5
  icon?: Component;
6
6
  iconClass?: string;
7
+ disabled?: boolean;
7
8
  }
8
9
  interface Props {
9
10
  items: FilterableDropdownItem[];
@@ -16,6 +17,10 @@ interface Props {
16
17
  /** Above this many items the list gets a filter box. */
17
18
  searchThreshold?: number;
18
19
  disabled?: boolean;
20
+ /** Submits the picked id with a plain <form>, like a native select's name. */
21
+ name?: string;
22
+ size?: 'default' | 'sm';
23
+ 'aria-label'?: string;
19
24
  class?: string;
20
25
  }
21
26
  declare const FilterableDropdown: Component<Props, {}, "">;
@@ -6,7 +6,7 @@
6
6
  variants: {
7
7
  variant: {
8
8
  default: 'h-9 w-fit justify-center rounded-md bg-muted p-0.5',
9
- line: 'gap-1 px-2 w-full justify-start'
9
+ line: 'gap-1 px-1 w-full justify-start'
10
10
  }
11
11
  },
12
12
  defaultVariants: {
@@ -2,17 +2,17 @@ import { type VariantProps } from 'tailwind-variants';
2
2
  export declare const tabsListVariants: import("tailwind-variants").TVReturnType<{
3
3
  variant: {
4
4
  default: "h-9 w-fit justify-center rounded-md bg-muted p-0.5";
5
- line: "gap-1 px-2 w-full justify-start";
5
+ line: "gap-1 px-1 w-full justify-start";
6
6
  };
7
7
  }, undefined, "inline-flex items-center text-muted-foreground", {
8
8
  variant: {
9
9
  default: "h-9 w-fit justify-center rounded-md bg-muted p-0.5";
10
- line: "gap-1 px-2 w-full justify-start";
10
+ line: "gap-1 px-1 w-full justify-start";
11
11
  };
12
12
  }, undefined, import("tailwind-variants").TVReturnTypeLike<{
13
13
  variant: {
14
14
  default: "h-9 w-fit justify-center rounded-md bg-muted p-0.5";
15
- line: "gap-1 px-2 w-full justify-start";
15
+ line: "gap-1 px-1 w-full justify-start";
16
16
  };
17
17
  }, undefined>>;
18
18
  export type TabsListVariant = VariantProps<typeof tabsListVariants>['variant'];
@@ -7,7 +7,7 @@
7
7
  variant: {
8
8
  default:
9
9
  'px-2 py-1 data-[state=active]:shadow-sm h-full flex-1 rounded-inner border border-transparent text-foreground transition focus-visible:border-ring data-[state=active]:bg-raised dark:text-muted-foreground data-[state=active]:border-border-strong dark:data-[state=active]:text-foreground',
10
- line: 'px-2.5 py-3 relative flex-none text-muted-foreground transition-colors after:absolute after:inset-x-2 after:bottom-0 after:h-0.5 after:rounded-full hover:text-foreground data-[state=active]:text-foreground data-[state=active]:after:bg-primary'
10
+ line: 'px-2.5 py-2.5 relative flex-none text-muted-foreground transition-colors after:absolute after:inset-x-2 after:bottom-0 after:h-0.5 after:rounded-full hover:text-foreground data-[state=active]:text-foreground data-[state=active]:after:bg-primary'
11
11
  }
12
12
  },
13
13
  defaultVariants: {
@@ -2,17 +2,17 @@ import { type VariantProps } from 'tailwind-variants';
2
2
  export declare const tabsTriggerVariants: import("tailwind-variants").TVReturnType<{
3
3
  variant: {
4
4
  default: "px-2 py-1 data-[state=active]:shadow-sm h-full flex-1 rounded-inner border border-transparent text-foreground transition focus-visible:border-ring data-[state=active]:bg-raised dark:text-muted-foreground data-[state=active]:border-border-strong dark:data-[state=active]:text-foreground";
5
- line: "px-2.5 py-3 relative flex-none text-muted-foreground transition-colors after:absolute after:inset-x-2 after:bottom-0 after:h-0.5 after:rounded-full hover:text-foreground data-[state=active]:text-foreground data-[state=active]:after:bg-primary";
5
+ line: "px-2.5 py-2.5 relative flex-none text-muted-foreground transition-colors after:absolute after:inset-x-2 after:bottom-0 after:h-0.5 after:rounded-full hover:text-foreground data-[state=active]:text-foreground data-[state=active]:after:bg-primary";
6
6
  };
7
7
  }, undefined, "gap-1.5 text-sm font-medium [&_svg:not([class*='size-'])]:size-4 inline-flex items-center justify-center whitespace-nowrap focus-visible:ring-3 focus-visible:ring-ring focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", {
8
8
  variant: {
9
9
  default: "px-2 py-1 data-[state=active]:shadow-sm h-full flex-1 rounded-inner border border-transparent text-foreground transition focus-visible:border-ring data-[state=active]:bg-raised dark:text-muted-foreground data-[state=active]:border-border-strong dark:data-[state=active]:text-foreground";
10
- line: "px-2.5 py-3 relative flex-none text-muted-foreground transition-colors after:absolute after:inset-x-2 after:bottom-0 after:h-0.5 after:rounded-full hover:text-foreground data-[state=active]:text-foreground data-[state=active]:after:bg-primary";
10
+ line: "px-2.5 py-2.5 relative flex-none text-muted-foreground transition-colors after:absolute after:inset-x-2 after:bottom-0 after:h-0.5 after:rounded-full hover:text-foreground data-[state=active]:text-foreground data-[state=active]:after:bg-primary";
11
11
  };
12
12
  }, undefined, import("tailwind-variants").TVReturnTypeLike<{
13
13
  variant: {
14
14
  default: "px-2 py-1 data-[state=active]:shadow-sm h-full flex-1 rounded-inner border border-transparent text-foreground transition focus-visible:border-ring data-[state=active]:bg-raised dark:text-muted-foreground data-[state=active]:border-border-strong dark:data-[state=active]:text-foreground";
15
- line: "px-2.5 py-3 relative flex-none text-muted-foreground transition-colors after:absolute after:inset-x-2 after:bottom-0 after:h-0.5 after:rounded-full hover:text-foreground data-[state=active]:text-foreground data-[state=active]:after:bg-primary";
15
+ line: "px-2.5 py-2.5 relative flex-none text-muted-foreground transition-colors after:absolute after:inset-x-2 after:bottom-0 after:h-0.5 after:rounded-full hover:text-foreground data-[state=active]:text-foreground data-[state=active]:after:bg-primary";
16
16
  };
17
17
  }, undefined>>;
18
18
  export type TabsTriggerVariant = VariantProps<typeof tabsTriggerVariants>['variant'];
@@ -9,7 +9,7 @@
9
9
  // Item inside a segmented track: the chosen value sits raised above the track.
10
10
  segmented:
11
11
  'rounded-inner bg-transparent text-muted-foreground data-[state=on]:bg-raised data-[state=on]:text-foreground data-[state=on]:shadow-raised data-[state=on]:ring-1 data-[state=on]:ring-border-strong',
12
- outline: 'border-input hover:bg-hover border bg-transparent',
12
+ outline: 'border-input hover:bg-hover border bg-field',
13
13
  // Filled accent for the active item; a bare bg-muted is too faint on a form.
14
14
  accent:
15
15
  'border-input hover:bg-hover border bg-transparent data-[state=on]:bg-primary/15 data-[state=on]:ring-1 data-[state=on]:ring-primary data-[state=on]:ring-inset'
@@ -3,7 +3,7 @@ export declare const toggleVariants: import("tailwind-variants").TVReturnType<{
3
3
  variant: {
4
4
  default: "bg-transparent";
5
5
  segmented: "rounded-inner bg-transparent text-muted-foreground data-[state=on]:bg-raised data-[state=on]:text-foreground data-[state=on]:shadow-raised data-[state=on]:ring-1 data-[state=on]:ring-border-strong";
6
- outline: "border-input hover:bg-hover border bg-transparent";
6
+ outline: "border-input hover:bg-hover border bg-field";
7
7
  accent: "border-input hover:bg-hover border bg-transparent data-[state=on]:bg-primary/15 data-[state=on]:ring-1 data-[state=on]:ring-primary data-[state=on]:ring-inset";
8
8
  };
9
9
  size: {
@@ -16,7 +16,7 @@ export declare const toggleVariants: import("tailwind-variants").TVReturnType<{
16
16
  variant: {
17
17
  default: "bg-transparent";
18
18
  segmented: "rounded-inner bg-transparent text-muted-foreground data-[state=on]:bg-raised data-[state=on]:text-foreground data-[state=on]:shadow-raised data-[state=on]:ring-1 data-[state=on]:ring-border-strong";
19
- outline: "border-input hover:bg-hover border bg-transparent";
19
+ outline: "border-input hover:bg-hover border bg-field";
20
20
  accent: "border-input hover:bg-hover border bg-transparent data-[state=on]:bg-primary/15 data-[state=on]:ring-1 data-[state=on]:ring-primary data-[state=on]:ring-inset";
21
21
  };
22
22
  size: {
@@ -29,7 +29,7 @@ export declare const toggleVariants: import("tailwind-variants").TVReturnType<{
29
29
  variant: {
30
30
  default: "bg-transparent";
31
31
  segmented: "rounded-inner bg-transparent text-muted-foreground data-[state=on]:bg-raised data-[state=on]:text-foreground data-[state=on]:shadow-raised data-[state=on]:ring-1 data-[state=on]:ring-border-strong";
32
- outline: "border-input hover:bg-hover border bg-transparent";
32
+ outline: "border-input hover:bg-hover border bg-field";
33
33
  accent: "border-input hover:bg-hover border bg-transparent data-[state=on]:bg-primary/15 data-[state=on]:ring-1 data-[state=on]:ring-primary data-[state=on]:ring-inset";
34
34
  };
35
35
  size: {
@@ -68,7 +68,7 @@ get along, so we shut typescript up by casting `value` to `never`.
68
68
  data-spacing={spacing}
69
69
  style="--gap: calc(var(--spacing) * {spacing})"
70
70
  class={cn(
71
- 'group/toggle-group data-[variant=segmented]:p-0.5 flex w-fit flex-row items-center gap-(--gap) rounded-md data-[orientation=vertical]:flex-col data-[orientation=vertical]:items-stretch data-[variant=segmented]:border data-[variant=segmented]:border-input data-[variant=segmented]:bg-muted',
71
+ 'group/toggle-group data-[variant=segmented]:p-0.5 flex w-fit flex-row items-center gap-(--gap) rounded-md data-[orientation=vertical]:flex-col data-[orientation=vertical]:items-stretch data-[variant=segmented]:rounded-lg data-[variant=segmented]:border data-[variant=segmented]:border-input data-[variant=segmented]:bg-muted',
72
72
  className
73
73
  )}
74
74
  {...restProps}
@@ -87,8 +87,8 @@
87
87
  const touch = new MediaQuery('(pointer: coarse)');
88
88
  const reducedMotion = new MediaQuery('(prefers-reduced-motion: reduce)');
89
89
 
90
- const COLLAPSED = $derived(touch.current ? { w: 44, h: 44 } : { w: 72, h: 8 });
91
- const EXPANDED_H = $derived(touch.current ? 54 : 46);
90
+ const COLLAPSED = $derived(touch.current ? { w: 44, h: 44 } : { w: 56, h: 5 });
91
+ const EXPANDED_H = $derived(touch.current ? 54 : 42);
92
92
  const PAD = 4;
93
93
 
94
94
  let dockEl: HTMLDivElement | undefined = $state();
@@ -108,7 +108,7 @@
108
108
  const containerStyle = $derived.by(() => {
109
109
  const bottom = touch.current ? 'max(1rem,env(safe-area-inset-bottom))' : '0.75rem';
110
110
  const geometry = wantOpen
111
- ? `width:${rowWidth + PAD * 2 + 2}px;height:${EXPANDED_H}px;border-radius:14px;padding:${PAD}px`
111
+ ? `width:${rowWidth + PAD * 2 + 2}px;height:${EXPANDED_H}px;border-radius:12px;padding:${PAD}px`
112
112
  : `width:${COLLAPSED.w}px;height:${COLLAPSED.h}px;border-radius:${touch.current ? 22 : 99}px;padding:0px`;
113
113
  const size = `bottom:${bottom};${geometry}`;
114
114
  if (reducedMotion.current) return size;
@@ -195,7 +195,7 @@
195
195
  const idx = (id: string) => buttons.indexOf(id);
196
196
 
197
197
  const BTN =
198
- 'relative grid size-9.5 shrink-0 place-items-center rounded-lg text-muted-foreground outline-none transition-colors hover:bg-hover hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring aria-pressed:text-foreground data-[state=open]:bg-selected data-[state=open]:text-foreground pointer-coarse:size-11';
198
+ 'relative grid size-8.5 shrink-0 place-items-center rounded-lg text-foreground outline-none transition-colors hover:bg-hover hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring aria-pressed:text-foreground data-[state=open]:bg-selected data-[state=open]:text-foreground pointer-coarse:size-11';
199
199
  const DOT =
200
200
  'aria-pressed:after:absolute aria-pressed:after:bottom-0.5 aria-pressed:after:size-1 aria-pressed:after:rounded-full aria-pressed:after:bg-primary';
201
201
  </script>
@@ -210,15 +210,15 @@
210
210
  role="toolbar"
211
211
  aria-label={t.toolsMenu}
212
212
  tabindex="-1"
213
- class="shadow-lg backdrop-blur-md absolute left-1/2 z-50 -translate-x-1/2 overflow-hidden border {hidden
213
+ class="backdrop-blur-md absolute left-1/2 z-50 -translate-x-1/2 overflow-hidden transition-colors {hidden
214
214
  ? 'pointer-events-none opacity-0'
215
215
  : ''} {wantOpen
216
- ? 'bg-background/70'
216
+ ? 'shadow-md border border-border-strong bg-card/75'
217
217
  : touch.current
218
- ? 'cursor-pointer border-border bg-background/90'
218
+ ? 'shadow-lg cursor-pointer border border-border bg-background/90'
219
219
  : nightOn
220
- ? 'border-white/50 bg-white/40 hover:bg-white/65 cursor-pointer'
221
- : 'cursor-pointer border-border-strong bg-faint-foreground hover:bg-muted-foreground'}"
220
+ ? 'border-white/25 bg-white/35 hover:bg-white/70 cursor-pointer border'
221
+ : 'shadow-sm cursor-pointer border border-border-strong bg-card/90 hover:bg-card'}"
222
222
  style={containerStyle}
223
223
  onclick={openFromCollapsed}
224
224
  onkeydown={(e) => (e.key === 'Escape' ? (engaged = false) : undefined)}
package/dist/index.d.ts CHANGED
@@ -5,10 +5,12 @@ export { default as SolveMessages } from './components/compute/SolveMessages.sve
5
5
  export { default as ErrorScreen } from './components/ErrorScreen.svelte';
6
6
  export * from './components/primitives';
7
7
  export { default as StateDisplay } from './components/primitives/StateDisplay.svelte';
8
+ export { default as ColorInput } from './components/preview/inputs/ColorInput.svelte';
8
9
  export { default as Viewer } from './components/viewer/Viewer.svelte';
9
10
  export * from './schema/defaults';
10
11
  export * from './schema/dynamic-value-list';
11
12
  export * from './schema/traversal';
13
+ export { visibleInputItems } from './schema/visibility-rules';
12
14
  export { type UIHost, type ElementKind, HOST_LABELS, isGeometryParamType, layoutItemKind, hostsFor, isSupportedOn, valuelessInputIds, unsupportedItems } from './schema/host-support';
13
15
  export * from './compute/solving.svelte';
14
16
  export { useSolveSession } from './compute/useSolveSession.svelte';
package/dist/index.js CHANGED
@@ -9,10 +9,12 @@ export { default as ErrorScreen } from './components/ErrorScreen.svelte';
9
9
  // Design-system primitives (shadcn-svelte + custom)
10
10
  export * from './components/primitives';
11
11
  export { default as StateDisplay } from './components/primitives/StateDisplay.svelte';
12
+ export { default as ColorInput } from './components/preview/inputs/ColorInput.svelte';
12
13
  export { default as Viewer } from './components/viewer/Viewer.svelte';
13
14
  export * from './schema/defaults';
14
15
  export * from './schema/dynamic-value-list';
15
16
  export * from './schema/traversal';
17
+ export { visibleInputItems } from './schema/visibility-rules';
16
18
  export { HOST_LABELS, isGeometryParamType, layoutItemKind, hostsFor, isSupportedOn, valuelessInputIds, unsupportedItems } from './schema/host-support';
17
19
  export * from './compute/solving.svelte';
18
20
  // Solve Session seam. Re-exported so transports outside this package — e.g.
package/dist/public.d.ts CHANGED
@@ -14,6 +14,7 @@ export { writeExternalValue, readExternalValue, clearExternalValue, getExternalI
14
14
  export * from './schema/defaults';
15
15
  export * from './schema/traversal';
16
16
  export * from './schema/dynamic-value-list';
17
+ export { visibleInputItems } from './schema/visibility-rules';
17
18
  export type { ActionButton } from './types/actionButton';
18
19
  export type { SolveFn, SolveResult, SolveDiagnostic } from '@selvajs/solve/shared';
19
20
  export { DEFAULT_PRESET_LABELS, type PresetLabels } from './types/presetLabels';
package/dist/public.js CHANGED
@@ -28,4 +28,5 @@ export { writeExternalValue, readExternalValue, clearExternalValue, getExternalI
28
28
  export * from './schema/defaults';
29
29
  export * from './schema/traversal';
30
30
  export * from './schema/dynamic-value-list';
31
+ export { visibleInputItems } from './schema/visibility-rules';
31
32
  export { DEFAULT_PRESET_LABELS } from './types/presetLabels';
@@ -1,4 +1,4 @@
1
- import type { LayoutItem, VisibilityRule, VisibilityCondition, GroupVisibilityCondition, TabConfig } from '@selvajs/schemas';
1
+ import type { InputLayoutItem, LayoutItem, VisibilityRule, VisibilityCondition, GroupVisibilityCondition, TabConfig, UISchema } from '@selvajs/schemas';
2
2
  export interface VisibilityResult {
3
3
  visible: boolean;
4
4
  disabled: boolean;
@@ -20,6 +20,12 @@ export declare function evaluateGroupVisibility(group: {
20
20
  items?: LayoutItem[];
21
21
  }, values: Record<string, unknown>): boolean;
22
22
  export declare function isTabVisible(tab: TabConfig, values: Record<string, unknown>): boolean;
23
+ /**
24
+ * The input items the form shows for these values: each passes its own condition, inside a
25
+ * shown group and, when tabbed, a shown tab. For a host that repeats the form back, such as a
26
+ * summary before a commit, without re-deriving the renderer's rules.
27
+ */
28
+ export declare function visibleInputItems(schema: UISchema, values: Record<string, unknown>): InputLayoutItem[];
23
29
  /**
24
30
  * A linebreak is always "visible" but renders as a divider only, so a group left with
25
31
  * nothing but separators counts as empty.
@@ -106,6 +106,23 @@ export function evaluateGroupVisibility(group, values) {
106
106
  export function isTabVisible(tab, values) {
107
107
  return (tab.visible !== false && tab.groups.some((group) => evaluateGroupVisibility(group, values)));
108
108
  }
109
+ /**
110
+ * The input items the form shows for these values: each passes its own condition, inside a
111
+ * shown group and, when tabbed, a shown tab. For a host that repeats the form back, such as a
112
+ * summary before a commit, without re-deriving the renderer's rules.
113
+ */
114
+ export function visibleInputItems(schema, values) {
115
+ const layout = schema?.layout;
116
+ const groups = layout?.type === 'tabbed'
117
+ ? (layout.tabs ?? []).filter((t) => isTabVisible(t, values)).flatMap((t) => t.groups ?? [])
118
+ : layout?.type === 'flat'
119
+ ? (layout.groups ?? [])
120
+ : [];
121
+ return groups
122
+ .filter((g) => evaluateGroupVisibility(g, values))
123
+ .flatMap((g) => g.items ?? [])
124
+ .filter((i) => i.type === 'input' && evaluateVisibility(i, values).visible);
125
+ }
109
126
  /**
110
127
  * A linebreak is always "visible" but renders as a divider only, so a group left with
111
128
  * nothing but separators counts as empty.
@@ -1,3 +1,5 @@
1
+ @import '@fontsource-variable/hanken-grotesk';
2
+
1
3
  @custom-variant dark (&:where(.dark, .dark *));
2
4
 
3
5
  /* Surfaces, edges and states share one set of names in both modes; each mode only changes the
@@ -8,40 +10,41 @@
8
10
  --page-px: 1rem;
9
11
  --page-py: 1.75rem;
10
12
  /* Canvas, then surface (cards, panels), then raised (rows, menus, popovers). Light can't go
11
- lighter than white, so raised is white too and lifts with a border and a shadow instead. */
12
- --background: oklch(0.91 0 0);
13
+ lighter than white, so raised is white too and lifts with a border and a shadow instead.
14
+ Values follow stock shadcn neutral. */
15
+ --background: oklch(0.985 0 0);
13
16
  --card: oklch(1 0 0);
14
17
  --raised: oklch(1 0 0);
15
18
  /* Recessed fill for trays, table heads, code and skeletons: darker than the surface around it. */
16
- --muted: oklch(0.94 0 0);
17
- /* Fill of anything you type into, on a card: the same step as raised, in both modes. The edge
18
- (`--input`) carries the boundary. A recessed fill reads as a disabled control. */
19
- --field: var(--raised);
20
- --foreground: oklch(0.22 0 0);
21
- --card-foreground: oklch(0.22 0 0);
22
- --popover-foreground: oklch(0.22 0 0);
19
+ --muted: oklch(0.97 0 0);
20
+ /* Fill of anything you type into, on a card: recessed, the same step as muted, in both modes.
21
+ The edge (`--input`) carries the boundary. */
22
+ --field: var(--muted);
23
+ --foreground: oklch(0.145 0 0);
24
+ --card-foreground: oklch(0.145 0 0);
25
+ --popover-foreground: oklch(0.145 0 0);
23
26
  /* 4.5:1 on every surface. */
24
- --muted-foreground: oklch(0.48 0 0);
27
+ --muted-foreground: oklch(0.5 0 0);
25
28
  /* Icons, placeholders, disabled text: 3:1 on every surface. */
26
29
  --faint-foreground: oklch(0.6 0 0);
27
30
  /* State overlays, laid over whatever surface is underneath. */
28
- --hover: oklch(0 0 0 / 6%);
29
- --selected: oklch(0 0 0 / 12%);
31
+ --hover: oklch(0 0 0 / 4%);
32
+ --selected: oklch(0 0 0 / 8%);
30
33
  /* Rest fill of a neutral filled control; its hover is `selected`. */
31
- --secondary: oklch(0 0 0 / 7%);
32
- --secondary-foreground: oklch(0.22 0 0);
34
+ --secondary: oklch(0.97 0 0);
35
+ --secondary-foreground: oklch(0.205 0 0);
33
36
  /* Outlines and separators. */
34
- --border: oklch(0 0 0 / 11%);
37
+ --border: oklch(0 0 0 / 8%);
35
38
  /* Popovers, outline buttons, dashed drop zones. */
36
- --border-strong: oklch(0 0 0 / 24%);
37
- /* Edge of anything you type into or toggle, slider tracks included: at least 3:1 against the
38
- surfaces around it, the WCAG minimum for a control's boundary. */
39
- --input: oklch(0 0 0 / 44%);
40
- --ring: oklch(0.55 0 0);
41
- --elevation-raised: 0 1px 2px oklch(0 0 0 / 6%);
42
- --elevation-popover: 0 8px 24px oklch(0 0 0 / 12%);
39
+ --border-strong: oklch(0 0 0 / 14%);
40
+ /* Edge of anything you type into or toggle, slider tracks included. Softer than the 3:1 WCAG
41
+ boundary minimum in both modes: the recessed field fill carries the rest of the boundary. */
42
+ --input: oklch(0 0 0 / 10%);
43
+ --ring: oklch(0.708 0 0);
44
+ --elevation-raised: 0 1px 2px oklch(0 0 0 / 5%);
45
+ --elevation-popover: 0 8px 24px oklch(0 0 0 / 10%);
43
46
  /* 4.5:1 as text on a card and under its foreground; 3:1 as a fill on every surface. */
44
- --primary: #4a7c52;
47
+ --primary: oklch(0.205 0 0);
45
48
  --primary-foreground: oklch(0.985 0 0);
46
49
  --destructive: #c9302d;
47
50
  --destructive-foreground: #fbfbfb;
@@ -62,28 +65,28 @@
62
65
  }
63
66
 
64
67
  .dark {
65
- --background: oklch(0.155 0 0);
66
- --card: oklch(0.235 0 0);
67
- --raised: oklch(0.275 0 0);
68
- --muted: oklch(0.18 0 0);
69
- --foreground: oklch(0.96 0 0);
70
- --card-foreground: oklch(0.96 0 0);
71
- --popover-foreground: oklch(0.96 0 0);
72
- --muted-foreground: oklch(0.72 0 0);
73
- --faint-foreground: oklch(0.56 0 0);
68
+ --background: oklch(0.145 0 0);
69
+ --card: oklch(0.205 0 0);
70
+ --raised: oklch(0.24 0 0);
71
+ --muted: oklch(0.269 0 0);
72
+ --foreground: oklch(0.985 0 0);
73
+ --card-foreground: oklch(0.985 0 0);
74
+ --popover-foreground: oklch(0.985 0 0);
75
+ --muted-foreground: oklch(0.708 0 0);
76
+ --faint-foreground: oklch(0.556 0 0);
74
77
  --hover: oklch(1 0 0 / 6%);
75
- --selected: oklch(1 0 0 / 12%);
76
- --secondary: oklch(1 0 0 / 8%);
77
- --secondary-foreground: oklch(0.96 0 0);
78
- --border: oklch(1 0 0 / 11%);
79
- --border-strong: oklch(1 0 0 / 24%);
80
- --input: oklch(1 0 0 / 41%);
81
- --ring: oklch(0.74 0 0);
82
- /* Shadows barely show on a dark canvas, so raised things catch a top highlight instead. */
83
- --elevation-raised: inset 0 1px 0 oklch(1 0 0 / 5%);
84
- --elevation-popover: 0 8px 24px oklch(0 0 0 / 50%), inset 0 1px 0 oklch(1 0 0 / 6%);
85
- --primary: #86ab78;
86
- --primary-foreground: oklch(0.18 0 0);
78
+ --selected: oklch(1 0 0 / 10%);
79
+ --secondary: oklch(0.269 0 0);
80
+ --secondary-foreground: oklch(0.985 0 0);
81
+ --border: oklch(1 0 0 / 10%);
82
+ --border-strong: oklch(1 0 0 / 18%);
83
+ --input: oklch(1 0 0 / 15%);
84
+ --ring: oklch(0.556 0 0);
85
+ /* Shadows barely show on a dark canvas, so raised things lean on the border instead. */
86
+ --elevation-raised: none;
87
+ --elevation-popover: 0 8px 24px oklch(0 0 0 / 50%);
88
+ --primary: oklch(0.922 0 0);
89
+ --primary-foreground: oklch(0.205 0 0);
87
90
  /* White text can't reach 4.5:1 on a red light enough to also be text on the dark card. */
88
91
  --destructive: oklch(0.7 0.17 22);
89
92
  --destructive-foreground: oklch(0.18 0 0);
@@ -151,6 +154,8 @@
151
154
  /* Design tokens beyond Tailwind's defaults. Reach for these before an arbitrary value: the
152
155
  design-system lint rejects `text-[10px]`, `max-h-[85dvh]` and the like. */
153
156
  @theme {
157
+ /* Hanken Grotesk (OFL), self-hosted via Fontsource: the plugin UI runs offline from the .gha. */
158
+ --font-sans: 'Hanken Grotesk Variable', ui-sans-serif, system-ui, sans-serif;
154
159
  /* Type tags, counters and other dense labels. */
155
160
  --text-2xs: 0.625rem;
156
161
  --text-2xs--line-height: calc(0.875 / 0.625);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@selvajs/ui",
3
- "version": "6.6.0-beta.16",
3
+ "version": "6.6.0-beta.18",
4
4
  "description": "Shared UI components and utilities for Selva applications",
5
5
  "license": "MIT",
6
6
  "author": "VektorNode",
@@ -63,9 +63,9 @@
63
63
  "tailwind-variants": "^3.3.1",
64
64
  "three": "^0.187.0",
65
65
  "@selvajs/compute": "^5.0.0-beta.9",
66
- "@selvajs/solve": "^2.0.0-beta.12",
67
- "@selvajs/schemas": "^5.3.0-beta.2",
68
- "@selvajs/visualization": "^1.6.0-beta.9"
66
+ "@selvajs/schemas": "^5.3.0-beta.3",
67
+ "@selvajs/visualization": "^1.6.0-beta.9",
68
+ "@selvajs/solve": "^2.0.0-beta.12"
69
69
  },
70
70
  "peerDependenciesMeta": {
71
71
  "three": {
@@ -74,6 +74,7 @@
74
74
  },
75
75
  "dependencies": {
76
76
  "@floating-ui/dom": "^1.8.0",
77
+ "@fontsource-variable/hanken-grotesk": "^5.3.0",
77
78
  "@iconify/svelte": "^5.2.3",
78
79
  "@lucide/svelte": "^1.61.0",
79
80
  "clsx": "^2.1.1",
@@ -95,8 +96,8 @@
95
96
  "vitest": "^5.0.5",
96
97
  "@selvajs/compute": "5.0.0-beta.9",
97
98
  "@selvajs/config": "0.0.4",
99
+ "@selvajs/schemas": "5.3.0-beta.3",
98
100
  "@selvajs/visualization": "1.6.0-beta.9",
99
- "@selvajs/schemas": "5.3.0-beta.2",
100
101
  "@selvajs/solve": "2.0.0-beta.12"
101
102
  },
102
103
  "scripts": {
@@ -101,7 +101,7 @@
101
101
  // from the main column's left edge — that one spans the full body height.
102
102
  const mainClass = $derived(
103
103
  _mode === 'fixed'
104
- ? 'min-h-0 flex-1 overflow-y-auto'
104
+ ? 'min-h-0 min-w-0 flex-1 overflow-y-auto'
105
105
  : 'min-w-0 flex-1 overflow-y-auto border-border sm:border-l'
106
106
  );
107
107
  </script>
@@ -7,6 +7,7 @@
7
7
  let { class: className = '', children }: PageContentProps = $props();
8
8
  </script>
9
9
 
10
- <main class={`px-(--page-px) py-(--page-py) ${className}`}>
10
+ <!-- One content width for every page, so moving between pages doesn't change the frame. -->
11
+ <div class={`max-w-6xl min-w-0 mx-auto w-full px-(--page-px) py-(--page-py) ${className}`}>
11
12
  {@render children?.()}
12
- </main>
13
+ </div>
@@ -34,7 +34,7 @@
34
34
  <div class="gap-2 sm:gap-5 flex h-(--header-h) items-center px-(--page-px)">
35
35
  <!-- Logo -->
36
36
  <a href={homeUrl} class="gap-2 flex shrink-0 items-center" aria-label={brandName}>
37
- <img src={logo} alt="" aria-hidden="true" class="h-5 w-5" />
37
+ <img src={logo} alt="" aria-hidden="true" class="h-7 w-7" />
38
38
  <!-- On a phone the page title needs the room more than the brand does. -->
39
39
  <span
40
40
  class="font-semibold text-sm tracking-tight {title !== undefined ? 'sm:inline hidden' : ''}"
@@ -107,7 +107,7 @@
107
107
  {/if}
108
108
  <div class="content-wrapper" class:collapsed inert={collapsed}>
109
109
  <div class="content-inner">
110
- <div class="px-4 pt-1 pb-4">
110
+ <div class="px-4 pt-2 pb-5">
111
111
  {@render grid()}
112
112
  </div>
113
113
  </div>
@@ -152,7 +152,7 @@
152
152
  .schema-grid {
153
153
  display: grid;
154
154
  grid-template-columns: repeat(var(--schema-cols), minmax(0, 1fr));
155
- gap: 1rem var(--column-gap);
155
+ gap: 1.25rem var(--column-gap);
156
156
  align-items: start;
157
157
  }
158
158
 
@@ -283,7 +283,7 @@
283
283
 
284
284
  {#if isClientSlot}
285
285
  {#if clientSlot}
286
- <div role="group" class="group/field gap-1.5 min-w-0 flex flex-col">
286
+ <div role="group" class="group/field gap-2 min-w-0 flex flex-col">
287
287
  {@render fieldHeader()}
288
288
  {@render clientSlot({
289
289
  inputId: item.paramId,
@@ -294,7 +294,7 @@
294
294
  </div>
295
295
  {/if}
296
296
  {:else if isGeometry}
297
- <div role="group" class="group/field gap-1.5 min-w-0 flex flex-col">
297
+ <div role="group" class="group/field gap-2 min-w-0 flex flex-col">
298
298
  {@render fieldHeader(inputId)}
299
299
  <GeometryInput
300
300
  {inputId}
@@ -322,7 +322,7 @@
322
322
  {@render helpButton()}
323
323
  </div>
324
324
  {:else if !hideDynamicListWhenEmpty}
325
- <div role="group" class="group/field gap-1.5 min-w-0 flex flex-col">
325
+ <div role="group" class="group/field gap-2 min-w-0 flex flex-col">
326
326
  {#if !isCheckboxWidget(item)}
327
327
  {@render fieldHeader(inputId)}
328
328
  {/if}
@@ -113,7 +113,7 @@
113
113
  {#if visibleGroups.length === 0}
114
114
  <StateDisplay type="empty" size="medium" message={t.tabNoGroups} />
115
115
  {:else}
116
- <div class="gap-3 py-3 flex flex-col">
116
+ <div class="gap-4 py-4 flex flex-col">
117
117
  {#each visibleGroups as group (group.id)}
118
118
  <Group
119
119
  label={group.label}
@@ -23,8 +23,8 @@
23
23
 
24
24
  <ToggleGroup.Root
25
25
  type="single"
26
- variant="segmented"
27
- spacing={1}
26
+ variant="outline"
27
+ spacing={0}
28
28
  size={hasIcons ? 'stacked' : 'default'}
29
29
  class="w-full"
30
30
  aria-labelledby={labelId}
@@ -52,11 +52,13 @@
52
52
  onclick={() => onChange(option.value)}
53
53
  onkeydown={(e) => onKeydown(e, index)}
54
54
  class="gap-1.5 p-1.5 min-w-0 flex flex-col rounded-lg border text-left transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 {selected
55
- ? 'border-primary bg-primary/5 ring-1 ring-primary'
55
+ ? 'border-primary bg-primary/10 ring-1 ring-primary'
56
56
  : 'border-input hover:bg-hover'}"
57
57
  >
58
58
  <span
59
- class="flex aspect-square w-full items-center justify-center overflow-hidden rounded-md bg-muted text-muted-foreground"
59
+ class="flex aspect-square w-full items-center justify-center overflow-hidden rounded-md bg-muted {selected
60
+ ? 'text-foreground'
61
+ : 'text-muted-foreground'}"
60
62
  >
61
63
  {#if glyph?.kind === 'image'}
62
64
  <OptionGlyph {glyph} class="size-full" />
@@ -55,7 +55,7 @@
55
55
  {#if Icon}
56
56
  <Icon class={cn('mt-0.5 h-4 w-4 shrink-0', accent)} />
57
57
  {/if}
58
- <div class="min-w-0 space-y-1">
58
+ <div class="min-w-0 space-y-1 wrap-anywhere">
59
59
  {#if title}
60
60
  <p class={cn('leading-tight font-medium', accent)}>{title}</p>
61
61
  {/if}
@@ -11,6 +11,7 @@
11
11
  label: string;
12
12
  icon?: Component;
13
13
  iconClass?: string;
14
+ disabled?: boolean;
14
15
  }
15
16
 
16
17
  interface Props {
@@ -24,6 +25,10 @@
24
25
  /** Above this many items the list gets a filter box. */
25
26
  searchThreshold?: number;
26
27
  disabled?: boolean;
28
+ /** Submits the picked id with a plain <form>, like a native select's name. */
29
+ name?: string;
30
+ size?: 'default' | 'sm';
31
+ 'aria-label'?: string;
27
32
  class?: string;
28
33
  }
29
34
 
@@ -37,6 +42,9 @@
37
42
  searchPlaceholder = 'Filter...',
38
43
  searchThreshold = 4,
39
44
  disabled = false,
45
+ name,
46
+ size = 'default',
47
+ 'aria-label': ariaLabel,
40
48
  class: className
41
49
  }: Props = $props();
42
50
 
@@ -51,14 +59,17 @@
51
59
  }
52
60
  </script>
53
61
 
62
+ {#if name}<input type="hidden" {name} value={value ?? ''} />{/if}
63
+
54
64
  <Popover.Root bind:open>
55
65
  <Popover.Trigger
56
66
  {id}
57
67
  {disabled}
58
68
  role="combobox"
59
69
  aria-expanded={open}
70
+ aria-label={ariaLabel}
60
71
  class={cn(
61
- buttonVariants({ variant: 'outline' }),
72
+ buttonVariants({ variant: 'outline', size }),
62
73
  'font-normal w-full justify-start',
63
74
  className
64
75
  )}
@@ -85,6 +96,7 @@
85
96
  value={item.id}
86
97
  keywords={[item.label]}
87
98
  data-checked={item.id === value}
99
+ disabled={item.disabled}
88
100
  onSelect={() => pick(item)}
89
101
  >
90
102
  {#if ItemIcon}
@@ -6,7 +6,7 @@
6
6
  variants: {
7
7
  variant: {
8
8
  default: 'h-9 w-fit justify-center rounded-md bg-muted p-0.5',
9
- line: 'gap-1 px-2 w-full justify-start'
9
+ line: 'gap-1 px-1 w-full justify-start'
10
10
  }
11
11
  },
12
12
  defaultVariants: {
@@ -7,7 +7,7 @@
7
7
  variant: {
8
8
  default:
9
9
  'px-2 py-1 data-[state=active]:shadow-sm h-full flex-1 rounded-inner border border-transparent text-foreground transition focus-visible:border-ring data-[state=active]:bg-raised dark:text-muted-foreground data-[state=active]:border-border-strong dark:data-[state=active]:text-foreground',
10
- line: 'px-2.5 py-3 relative flex-none text-muted-foreground transition-colors after:absolute after:inset-x-2 after:bottom-0 after:h-0.5 after:rounded-full hover:text-foreground data-[state=active]:text-foreground data-[state=active]:after:bg-primary'
10
+ line: 'px-2.5 py-2.5 relative flex-none text-muted-foreground transition-colors after:absolute after:inset-x-2 after:bottom-0 after:h-0.5 after:rounded-full hover:text-foreground data-[state=active]:text-foreground data-[state=active]:after:bg-primary'
11
11
  }
12
12
  },
13
13
  defaultVariants: {
@@ -9,7 +9,7 @@
9
9
  // Item inside a segmented track: the chosen value sits raised above the track.
10
10
  segmented:
11
11
  'rounded-inner bg-transparent text-muted-foreground data-[state=on]:bg-raised data-[state=on]:text-foreground data-[state=on]:shadow-raised data-[state=on]:ring-1 data-[state=on]:ring-border-strong',
12
- outline: 'border-input hover:bg-hover border bg-transparent',
12
+ outline: 'border-input hover:bg-hover border bg-field',
13
13
  // Filled accent for the active item; a bare bg-muted is too faint on a form.
14
14
  accent:
15
15
  'border-input hover:bg-hover border bg-transparent data-[state=on]:bg-primary/15 data-[state=on]:ring-1 data-[state=on]:ring-primary data-[state=on]:ring-inset'
@@ -68,7 +68,7 @@ get along, so we shut typescript up by casting `value` to `never`.
68
68
  data-spacing={spacing}
69
69
  style="--gap: calc(var(--spacing) * {spacing})"
70
70
  class={cn(
71
- 'group/toggle-group data-[variant=segmented]:p-0.5 flex w-fit flex-row items-center gap-(--gap) rounded-md data-[orientation=vertical]:flex-col data-[orientation=vertical]:items-stretch data-[variant=segmented]:border data-[variant=segmented]:border-input data-[variant=segmented]:bg-muted',
71
+ 'group/toggle-group data-[variant=segmented]:p-0.5 flex w-fit flex-row items-center gap-(--gap) rounded-md data-[orientation=vertical]:flex-col data-[orientation=vertical]:items-stretch data-[variant=segmented]:rounded-lg data-[variant=segmented]:border data-[variant=segmented]:border-input data-[variant=segmented]:bg-muted',
72
72
  className
73
73
  )}
74
74
  {...restProps}
@@ -87,8 +87,8 @@
87
87
  const touch = new MediaQuery('(pointer: coarse)');
88
88
  const reducedMotion = new MediaQuery('(prefers-reduced-motion: reduce)');
89
89
 
90
- const COLLAPSED = $derived(touch.current ? { w: 44, h: 44 } : { w: 72, h: 8 });
91
- const EXPANDED_H = $derived(touch.current ? 54 : 46);
90
+ const COLLAPSED = $derived(touch.current ? { w: 44, h: 44 } : { w: 56, h: 5 });
91
+ const EXPANDED_H = $derived(touch.current ? 54 : 42);
92
92
  const PAD = 4;
93
93
 
94
94
  let dockEl: HTMLDivElement | undefined = $state();
@@ -108,7 +108,7 @@
108
108
  const containerStyle = $derived.by(() => {
109
109
  const bottom = touch.current ? 'max(1rem,env(safe-area-inset-bottom))' : '0.75rem';
110
110
  const geometry = wantOpen
111
- ? `width:${rowWidth + PAD * 2 + 2}px;height:${EXPANDED_H}px;border-radius:14px;padding:${PAD}px`
111
+ ? `width:${rowWidth + PAD * 2 + 2}px;height:${EXPANDED_H}px;border-radius:12px;padding:${PAD}px`
112
112
  : `width:${COLLAPSED.w}px;height:${COLLAPSED.h}px;border-radius:${touch.current ? 22 : 99}px;padding:0px`;
113
113
  const size = `bottom:${bottom};${geometry}`;
114
114
  if (reducedMotion.current) return size;
@@ -195,7 +195,7 @@
195
195
  const idx = (id: string) => buttons.indexOf(id);
196
196
 
197
197
  const BTN =
198
- 'relative grid size-9.5 shrink-0 place-items-center rounded-lg text-muted-foreground outline-none transition-colors hover:bg-hover hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring aria-pressed:text-foreground data-[state=open]:bg-selected data-[state=open]:text-foreground pointer-coarse:size-11';
198
+ 'relative grid size-8.5 shrink-0 place-items-center rounded-lg text-foreground outline-none transition-colors hover:bg-hover hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring aria-pressed:text-foreground data-[state=open]:bg-selected data-[state=open]:text-foreground pointer-coarse:size-11';
199
199
  const DOT =
200
200
  'aria-pressed:after:absolute aria-pressed:after:bottom-0.5 aria-pressed:after:size-1 aria-pressed:after:rounded-full aria-pressed:after:bg-primary';
201
201
  </script>
@@ -210,15 +210,15 @@
210
210
  role="toolbar"
211
211
  aria-label={t.toolsMenu}
212
212
  tabindex="-1"
213
- class="shadow-lg backdrop-blur-md absolute left-1/2 z-50 -translate-x-1/2 overflow-hidden border {hidden
213
+ class="backdrop-blur-md absolute left-1/2 z-50 -translate-x-1/2 overflow-hidden transition-colors {hidden
214
214
  ? 'pointer-events-none opacity-0'
215
215
  : ''} {wantOpen
216
- ? 'bg-background/70'
216
+ ? 'shadow-md border border-border-strong bg-card/75'
217
217
  : touch.current
218
- ? 'cursor-pointer border-border bg-background/90'
218
+ ? 'shadow-lg cursor-pointer border border-border bg-background/90'
219
219
  : nightOn
220
- ? 'border-white/50 bg-white/40 hover:bg-white/65 cursor-pointer'
221
- : 'cursor-pointer border-border-strong bg-faint-foreground hover:bg-muted-foreground'}"
220
+ ? 'border-white/25 bg-white/35 hover:bg-white/70 cursor-pointer border'
221
+ : 'shadow-sm cursor-pointer border border-border-strong bg-card/90 hover:bg-card'}"
222
222
  style={containerStyle}
223
223
  onclick={openFromCollapsed}
224
224
  onkeydown={(e) => (e.key === 'Escape' ? (engaged = false) : undefined)}
package/src/lib/index.ts CHANGED
@@ -13,12 +13,14 @@ export { default as ErrorScreen } from './components/ErrorScreen.svelte';
13
13
  // Design-system primitives (shadcn-svelte + custom)
14
14
  export * from './components/primitives';
15
15
  export { default as StateDisplay } from './components/primitives/StateDisplay.svelte';
16
+ export { default as ColorInput } from './components/preview/inputs/ColorInput.svelte';
16
17
 
17
18
  export { default as Viewer } from './components/viewer/Viewer.svelte';
18
19
 
19
20
  export * from './schema/defaults';
20
21
  export * from './schema/dynamic-value-list';
21
22
  export * from './schema/traversal';
23
+ export { visibleInputItems } from './schema/visibility-rules';
22
24
  export {
23
25
  type UIHost,
24
26
  type ElementKind,
package/src/lib/public.ts CHANGED
@@ -80,6 +80,7 @@ export {
80
80
  export * from './schema/defaults';
81
81
  export * from './schema/traversal';
82
82
  export * from './schema/dynamic-value-list';
83
+ export { visibleInputItems } from './schema/visibility-rules';
83
84
 
84
85
  // UI-facing runtime types (not from schema)
85
86
  export type { ActionButton } from './types/actionButton';
@@ -1,10 +1,12 @@
1
1
  import type {
2
+ InputLayoutItem,
2
3
  LayoutItem,
3
4
  OutputLayoutItem,
4
5
  VisibilityRule,
5
6
  VisibilityCondition,
6
7
  GroupVisibilityCondition,
7
- TabConfig
8
+ TabConfig,
9
+ UISchema
8
10
  } from '@selvajs/schemas';
9
11
  import { isFileData } from '../utils/file-download';
10
12
 
@@ -148,6 +150,30 @@ export function isTabVisible(tab: TabConfig, values: Record<string, unknown>): b
148
150
  );
149
151
  }
150
152
 
153
+ /**
154
+ * The input items the form shows for these values: each passes its own condition, inside a
155
+ * shown group and, when tabbed, a shown tab. For a host that repeats the form back, such as a
156
+ * summary before a commit, without re-deriving the renderer's rules.
157
+ */
158
+ export function visibleInputItems(
159
+ schema: UISchema,
160
+ values: Record<string, unknown>
161
+ ): InputLayoutItem[] {
162
+ const layout = schema?.layout;
163
+ const groups =
164
+ layout?.type === 'tabbed'
165
+ ? (layout.tabs ?? []).filter((t) => isTabVisible(t, values)).flatMap((t) => t.groups ?? [])
166
+ : layout?.type === 'flat'
167
+ ? (layout.groups ?? [])
168
+ : [];
169
+ return groups
170
+ .filter((g) => evaluateGroupVisibility(g, values))
171
+ .flatMap((g) => g.items ?? [])
172
+ .filter(
173
+ (i): i is InputLayoutItem => i.type === 'input' && evaluateVisibility(i, values).visible
174
+ );
175
+ }
176
+
151
177
  /**
152
178
  * A linebreak is always "visible" but renders as a divider only, so a group left with
153
179
  * nothing but separators counts as empty.
@@ -1,3 +1,5 @@
1
+ @import '@fontsource-variable/hanken-grotesk';
2
+
1
3
  @custom-variant dark (&:where(.dark, .dark *));
2
4
 
3
5
  /* Surfaces, edges and states share one set of names in both modes; each mode only changes the
@@ -8,40 +10,41 @@
8
10
  --page-px: 1rem;
9
11
  --page-py: 1.75rem;
10
12
  /* Canvas, then surface (cards, panels), then raised (rows, menus, popovers). Light can't go
11
- lighter than white, so raised is white too and lifts with a border and a shadow instead. */
12
- --background: oklch(0.91 0 0);
13
+ lighter than white, so raised is white too and lifts with a border and a shadow instead.
14
+ Values follow stock shadcn neutral. */
15
+ --background: oklch(0.985 0 0);
13
16
  --card: oklch(1 0 0);
14
17
  --raised: oklch(1 0 0);
15
18
  /* Recessed fill for trays, table heads, code and skeletons: darker than the surface around it. */
16
- --muted: oklch(0.94 0 0);
17
- /* Fill of anything you type into, on a card: the same step as raised, in both modes. The edge
18
- (`--input`) carries the boundary. A recessed fill reads as a disabled control. */
19
- --field: var(--raised);
20
- --foreground: oklch(0.22 0 0);
21
- --card-foreground: oklch(0.22 0 0);
22
- --popover-foreground: oklch(0.22 0 0);
19
+ --muted: oklch(0.97 0 0);
20
+ /* Fill of anything you type into, on a card: recessed, the same step as muted, in both modes.
21
+ The edge (`--input`) carries the boundary. */
22
+ --field: var(--muted);
23
+ --foreground: oklch(0.145 0 0);
24
+ --card-foreground: oklch(0.145 0 0);
25
+ --popover-foreground: oklch(0.145 0 0);
23
26
  /* 4.5:1 on every surface. */
24
- --muted-foreground: oklch(0.48 0 0);
27
+ --muted-foreground: oklch(0.5 0 0);
25
28
  /* Icons, placeholders, disabled text: 3:1 on every surface. */
26
29
  --faint-foreground: oklch(0.6 0 0);
27
30
  /* State overlays, laid over whatever surface is underneath. */
28
- --hover: oklch(0 0 0 / 6%);
29
- --selected: oklch(0 0 0 / 12%);
31
+ --hover: oklch(0 0 0 / 4%);
32
+ --selected: oklch(0 0 0 / 8%);
30
33
  /* Rest fill of a neutral filled control; its hover is `selected`. */
31
- --secondary: oklch(0 0 0 / 7%);
32
- --secondary-foreground: oklch(0.22 0 0);
34
+ --secondary: oklch(0.97 0 0);
35
+ --secondary-foreground: oklch(0.205 0 0);
33
36
  /* Outlines and separators. */
34
- --border: oklch(0 0 0 / 11%);
37
+ --border: oklch(0 0 0 / 8%);
35
38
  /* Popovers, outline buttons, dashed drop zones. */
36
- --border-strong: oklch(0 0 0 / 24%);
37
- /* Edge of anything you type into or toggle, slider tracks included: at least 3:1 against the
38
- surfaces around it, the WCAG minimum for a control's boundary. */
39
- --input: oklch(0 0 0 / 44%);
40
- --ring: oklch(0.55 0 0);
41
- --elevation-raised: 0 1px 2px oklch(0 0 0 / 6%);
42
- --elevation-popover: 0 8px 24px oklch(0 0 0 / 12%);
39
+ --border-strong: oklch(0 0 0 / 14%);
40
+ /* Edge of anything you type into or toggle, slider tracks included. Softer than the 3:1 WCAG
41
+ boundary minimum in both modes: the recessed field fill carries the rest of the boundary. */
42
+ --input: oklch(0 0 0 / 10%);
43
+ --ring: oklch(0.708 0 0);
44
+ --elevation-raised: 0 1px 2px oklch(0 0 0 / 5%);
45
+ --elevation-popover: 0 8px 24px oklch(0 0 0 / 10%);
43
46
  /* 4.5:1 as text on a card and under its foreground; 3:1 as a fill on every surface. */
44
- --primary: #4a7c52;
47
+ --primary: oklch(0.205 0 0);
45
48
  --primary-foreground: oklch(0.985 0 0);
46
49
  --destructive: #c9302d;
47
50
  --destructive-foreground: #fbfbfb;
@@ -62,28 +65,28 @@
62
65
  }
63
66
 
64
67
  .dark {
65
- --background: oklch(0.155 0 0);
66
- --card: oklch(0.235 0 0);
67
- --raised: oklch(0.275 0 0);
68
- --muted: oklch(0.18 0 0);
69
- --foreground: oklch(0.96 0 0);
70
- --card-foreground: oklch(0.96 0 0);
71
- --popover-foreground: oklch(0.96 0 0);
72
- --muted-foreground: oklch(0.72 0 0);
73
- --faint-foreground: oklch(0.56 0 0);
68
+ --background: oklch(0.145 0 0);
69
+ --card: oklch(0.205 0 0);
70
+ --raised: oklch(0.24 0 0);
71
+ --muted: oklch(0.269 0 0);
72
+ --foreground: oklch(0.985 0 0);
73
+ --card-foreground: oklch(0.985 0 0);
74
+ --popover-foreground: oklch(0.985 0 0);
75
+ --muted-foreground: oklch(0.708 0 0);
76
+ --faint-foreground: oklch(0.556 0 0);
74
77
  --hover: oklch(1 0 0 / 6%);
75
- --selected: oklch(1 0 0 / 12%);
76
- --secondary: oklch(1 0 0 / 8%);
77
- --secondary-foreground: oklch(0.96 0 0);
78
- --border: oklch(1 0 0 / 11%);
79
- --border-strong: oklch(1 0 0 / 24%);
80
- --input: oklch(1 0 0 / 41%);
81
- --ring: oklch(0.74 0 0);
82
- /* Shadows barely show on a dark canvas, so raised things catch a top highlight instead. */
83
- --elevation-raised: inset 0 1px 0 oklch(1 0 0 / 5%);
84
- --elevation-popover: 0 8px 24px oklch(0 0 0 / 50%), inset 0 1px 0 oklch(1 0 0 / 6%);
85
- --primary: #86ab78;
86
- --primary-foreground: oklch(0.18 0 0);
78
+ --selected: oklch(1 0 0 / 10%);
79
+ --secondary: oklch(0.269 0 0);
80
+ --secondary-foreground: oklch(0.985 0 0);
81
+ --border: oklch(1 0 0 / 10%);
82
+ --border-strong: oklch(1 0 0 / 18%);
83
+ --input: oklch(1 0 0 / 15%);
84
+ --ring: oklch(0.556 0 0);
85
+ /* Shadows barely show on a dark canvas, so raised things lean on the border instead. */
86
+ --elevation-raised: none;
87
+ --elevation-popover: 0 8px 24px oklch(0 0 0 / 50%);
88
+ --primary: oklch(0.922 0 0);
89
+ --primary-foreground: oklch(0.205 0 0);
87
90
  /* White text can't reach 4.5:1 on a red light enough to also be text on the dark card. */
88
91
  --destructive: oklch(0.7 0.17 22);
89
92
  --destructive-foreground: oklch(0.18 0 0);
@@ -151,6 +154,8 @@
151
154
  /* Design tokens beyond Tailwind's defaults. Reach for these before an arbitrary value: the
152
155
  design-system lint rejects `text-[10px]`, `max-h-[85dvh]` and the like. */
153
156
  @theme {
157
+ /* Hanken Grotesk (OFL), self-hosted via Fontsource: the plugin UI runs offline from the .gha. */
158
+ --font-sans: 'Hanken Grotesk Variable', ui-sans-serif, system-ui, sans-serif;
154
159
  /* Type tags, counters and other dense labels. */
155
160
  --text-2xs: 0.625rem;
156
161
  --text-2xs--line-height: calc(0.875 / 0.625);