@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.
- package/dist/components/layout/AppShell.svelte +1 -1
- package/dist/components/layout/PageContent.svelte +3 -2
- package/dist/components/layout/PageHeader.svelte +1 -1
- package/dist/components/preview/Group.svelte +2 -2
- package/dist/components/preview/InputControl.svelte +3 -3
- package/dist/components/preview/TabContent.svelte +1 -1
- package/dist/components/preview/inputs/SegmentedInput.svelte +2 -2
- package/dist/components/preview/inputs/TileInput.svelte +4 -2
- package/dist/components/primitives/Callout.svelte +1 -1
- package/dist/components/primitives/FilterableDropdown.svelte +13 -1
- package/dist/components/primitives/FilterableDropdown.svelte.d.ts +5 -0
- package/dist/components/primitives/tabs/tabs-list.svelte +1 -1
- package/dist/components/primitives/tabs/tabs-list.svelte.d.ts +3 -3
- package/dist/components/primitives/tabs/tabs-trigger.svelte +1 -1
- package/dist/components/primitives/tabs/tabs-trigger.svelte.d.ts +3 -3
- package/dist/components/primitives/toggle/toggle.svelte +1 -1
- package/dist/components/primitives/toggle/toggle.svelte.d.ts +3 -3
- package/dist/components/primitives/toggle-group/toggle-group.svelte +1 -1
- package/dist/components/viewer/ViewerDock.svelte +9 -9
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/public.d.ts +1 -0
- package/dist/public.js +1 -0
- package/dist/schema/visibility-rules.d.ts +7 -1
- package/dist/schema/visibility-rules.js +17 -0
- package/dist/styles/base.css +49 -44
- package/package.json +6 -5
- package/src/lib/components/layout/AppShell.svelte +1 -1
- package/src/lib/components/layout/PageContent.svelte +3 -2
- package/src/lib/components/layout/PageHeader.svelte +1 -1
- package/src/lib/components/preview/Group.svelte +2 -2
- package/src/lib/components/preview/InputControl.svelte +3 -3
- package/src/lib/components/preview/TabContent.svelte +1 -1
- package/src/lib/components/preview/inputs/SegmentedInput.svelte +2 -2
- package/src/lib/components/preview/inputs/TileInput.svelte +4 -2
- package/src/lib/components/primitives/Callout.svelte +1 -1
- package/src/lib/components/primitives/FilterableDropdown.svelte +13 -1
- package/src/lib/components/primitives/tabs/tabs-list.svelte +1 -1
- package/src/lib/components/primitives/tabs/tabs-trigger.svelte +1 -1
- package/src/lib/components/primitives/toggle/toggle.svelte +1 -1
- package/src/lib/components/primitives/toggle-group/toggle-group.svelte +1 -1
- package/src/lib/components/viewer/ViewerDock.svelte +9 -9
- package/src/lib/index.ts +2 -0
- package/src/lib/public.ts +1 -0
- package/src/lib/schema/visibility-rules.ts +27 -1
- 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
|
-
|
|
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
|
-
</
|
|
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-
|
|
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-
|
|
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:
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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}
|
|
@@ -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/
|
|
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
|
|
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" />
|
|
@@ -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, {}, "">;
|
|
@@ -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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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:
|
|
91
|
-
const EXPANDED_H = $derived(touch.current ? 54 :
|
|
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:
|
|
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-
|
|
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="
|
|
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-
|
|
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/
|
|
221
|
-
: 'cursor-pointer border-border-strong bg-
|
|
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.
|
package/dist/styles/base.css
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
17
|
-
/* Fill of anything you type into, on a card: the same step as
|
|
18
|
-
(`--input`) carries the boundary.
|
|
19
|
-
--field: var(--
|
|
20
|
-
--foreground: oklch(0.
|
|
21
|
-
--card-foreground: oklch(0.
|
|
22
|
-
--popover-foreground: oklch(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.
|
|
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 /
|
|
29
|
-
--selected: oklch(0 0 0 /
|
|
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
|
|
32
|
-
--secondary-foreground: oklch(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 /
|
|
37
|
+
--border: oklch(0 0 0 / 8%);
|
|
35
38
|
/* Popovers, outline buttons, dashed drop zones. */
|
|
36
|
-
--border-strong: oklch(0 0 0 /
|
|
37
|
-
/* Edge of anything you type into or toggle, slider tracks included
|
|
38
|
-
|
|
39
|
-
--input: oklch(0 0 0 /
|
|
40
|
-
--ring: oklch(0.
|
|
41
|
-
--elevation-raised: 0 1px 2px oklch(0 0 0 /
|
|
42
|
-
--elevation-popover: 0 8px 24px oklch(0 0 0 /
|
|
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:
|
|
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.
|
|
66
|
-
--card: oklch(0.
|
|
67
|
-
--raised: oklch(0.
|
|
68
|
-
--muted: oklch(0.
|
|
69
|
-
--foreground: oklch(0.
|
|
70
|
-
--card-foreground: oklch(0.
|
|
71
|
-
--popover-foreground: oklch(0.
|
|
72
|
-
--muted-foreground: oklch(0.
|
|
73
|
-
--faint-foreground: oklch(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 /
|
|
76
|
-
--secondary: oklch(
|
|
77
|
-
--secondary-foreground: oklch(0.
|
|
78
|
-
--border: oklch(1 0 0 /
|
|
79
|
-
--border-strong: oklch(1 0 0 /
|
|
80
|
-
--input: oklch(1 0 0 /
|
|
81
|
-
--ring: oklch(0.
|
|
82
|
-
/* Shadows barely show on a dark canvas, so raised things
|
|
83
|
-
--elevation-raised:
|
|
84
|
-
--elevation-popover: 0 8px 24px oklch(0 0 0 / 50%)
|
|
85
|
-
--primary:
|
|
86
|
-
--primary-foreground: oklch(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.
|
|
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/
|
|
67
|
-
"@selvajs/
|
|
68
|
-
"@selvajs/
|
|
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
|
-
|
|
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
|
-
</
|
|
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-
|
|
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-
|
|
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:
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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}
|
|
@@ -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/
|
|
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
|
|
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" />
|
|
@@ -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}
|
|
@@ -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-
|
|
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-
|
|
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:
|
|
91
|
-
const EXPANDED_H = $derived(touch.current ? 54 :
|
|
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:
|
|
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-
|
|
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="
|
|
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-
|
|
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/
|
|
221
|
-
: 'cursor-pointer border-border-strong bg-
|
|
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.
|
package/src/lib/styles/base.css
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
17
|
-
/* Fill of anything you type into, on a card: the same step as
|
|
18
|
-
(`--input`) carries the boundary.
|
|
19
|
-
--field: var(--
|
|
20
|
-
--foreground: oklch(0.
|
|
21
|
-
--card-foreground: oklch(0.
|
|
22
|
-
--popover-foreground: oklch(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.
|
|
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 /
|
|
29
|
-
--selected: oklch(0 0 0 /
|
|
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
|
|
32
|
-
--secondary-foreground: oklch(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 /
|
|
37
|
+
--border: oklch(0 0 0 / 8%);
|
|
35
38
|
/* Popovers, outline buttons, dashed drop zones. */
|
|
36
|
-
--border-strong: oklch(0 0 0 /
|
|
37
|
-
/* Edge of anything you type into or toggle, slider tracks included
|
|
38
|
-
|
|
39
|
-
--input: oklch(0 0 0 /
|
|
40
|
-
--ring: oklch(0.
|
|
41
|
-
--elevation-raised: 0 1px 2px oklch(0 0 0 /
|
|
42
|
-
--elevation-popover: 0 8px 24px oklch(0 0 0 /
|
|
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:
|
|
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.
|
|
66
|
-
--card: oklch(0.
|
|
67
|
-
--raised: oklch(0.
|
|
68
|
-
--muted: oklch(0.
|
|
69
|
-
--foreground: oklch(0.
|
|
70
|
-
--card-foreground: oklch(0.
|
|
71
|
-
--popover-foreground: oklch(0.
|
|
72
|
-
--muted-foreground: oklch(0.
|
|
73
|
-
--faint-foreground: oklch(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 /
|
|
76
|
-
--secondary: oklch(
|
|
77
|
-
--secondary-foreground: oklch(0.
|
|
78
|
-
--border: oklch(1 0 0 /
|
|
79
|
-
--border-strong: oklch(1 0 0 /
|
|
80
|
-
--input: oklch(1 0 0 /
|
|
81
|
-
--ring: oklch(0.
|
|
82
|
-
/* Shadows barely show on a dark canvas, so raised things
|
|
83
|
-
--elevation-raised:
|
|
84
|
-
--elevation-popover: 0 8px 24px oklch(0 0 0 / 50%)
|
|
85
|
-
--primary:
|
|
86
|
-
--primary-foreground: oklch(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);
|