entasis 0.7.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -1
- package/dist/components/AppShell/appShell.theme.js +2 -2
- package/dist/components/FloatingWindow/FloatingWindow.svelte +26 -2
- package/dist/components/FloatingWindow/floatingWindow.dock.svelte.js +11 -2
- package/dist/components/FloatingWindow/floatingWindow.mcp.d.ts +1 -1
- package/dist/components/FloatingWindow/floatingWindow.mcp.js +7 -4
- package/dist/components/FloatingWindow/floatingWindow.props.d.ts +6 -0
- package/dist/components/FloatingWindow/floatingWindow.state.svelte.d.ts +3 -0
- package/dist/components/FloatingWindow/floatingWindow.state.svelte.js +18 -4
- package/dist/components/FloatingWindow/floatingWindow.theme.d.ts +3 -0
- package/dist/components/FloatingWindow/floatingWindow.theme.js +20 -7
- package/dist/components/Form/File/FileInput.svelte +6 -42
- package/dist/components/Form/Form/form.state.svelte.d.ts +4 -0
- package/dist/components/Form/Form/visibility.d.ts +2 -0
- package/dist/components/Form/MultiStepForm/multiStepForm.state.svelte.d.ts +4 -0
- package/dist/components/Form/Select/Select.svelte +28 -2
- package/dist/components/Form/Select/select.align.d.ts +55 -0
- package/dist/components/Form/Select/select.align.js +41 -0
- package/dist/components/Form/Select/select.mcp.d.ts +1 -1
- package/dist/components/Form/Select/select.mcp.js +7 -3
- package/dist/components/Form/Select/select.props.d.ts +8 -0
- package/dist/components/Form/Select/select.state.svelte.d.ts +24 -0
- package/dist/components/Form/Select/select.state.svelte.js +111 -1
- package/dist/components/Popover/Popover.svelte +4 -0
- package/dist/components/Popover/popover.mcp.d.ts +1 -1
- package/dist/components/Popover/popover.mcp.js +1 -0
- package/dist/components/Popover/popover.props.d.ts +16 -0
- package/dist/components/Popover/popover.state.svelte.d.ts +1 -1
- package/dist/components/Popover/popover.state.svelte.js +13 -0
- package/dist/components/Sidebar/Sidebar.svelte +58 -10
- package/dist/components/Sidebar/Sidebar.svelte.d.ts +1 -1
- package/dist/components/Sidebar/SidebarMenuItem.svelte +19 -2
- package/dist/components/Sidebar/SidebarMenuItem.svelte.d.ts +4 -0
- package/dist/components/Sidebar/SidebarPanel.svelte +199 -88
- package/dist/components/Sidebar/SidebarPanel.svelte.d.ts +3 -0
- package/dist/components/Sidebar/SidebarViewStage.svelte +103 -0
- package/dist/components/Sidebar/SidebarViewStage.svelte.d.ts +18 -0
- package/dist/components/Sidebar/index.d.ts +1 -1
- package/dist/components/Sidebar/sidebar.mcp.d.ts +1 -1
- package/dist/components/Sidebar/sidebar.mcp.js +19 -2
- package/dist/components/Sidebar/sidebar.props.d.ts +56 -1
- package/dist/components/Sidebar/sidebar.state.svelte.d.ts +4 -0
- package/dist/components/Sidebar/sidebar.state.svelte.js +9 -1
- package/dist/components/Sidebar/sidebar.theme.d.ts +128 -5
- package/dist/components/Sidebar/sidebar.theme.js +69 -3
- package/dist/components/Sidebar/sidebar.views.svelte.d.ts +138 -0
- package/dist/components/Sidebar/sidebar.views.svelte.js +303 -0
- package/dist/components/Theme/theme.floatingWindows.d.ts +6 -1
- package/dist/components/Theme/theme.floatingWindows.js +7 -2
- package/dist/components/Theme/theme.mcp.d.ts +1 -1
- package/dist/components/Theme/theme.mcp.js +1 -0
- package/dist/generated/componentContract.d.ts +1 -1
- package/dist/generated/componentContract.js +1 -0
- package/dist/generated/componentMcpRegistry.d.ts +6 -6
- package/dist/tailwind/index.mcp.d.ts +1 -1
- package/dist/tailwind/index.mcp.js +4 -0
- package/dist/tailwind/scales.js +11 -3
- package/dist/tailwind/spacing.js +17 -0
- package/dist/utils/cva/merge.d.ts +7 -0
- package/dist/utils/cva/merge.js +6 -1
- package/dist/utils/pointerDrag.js +7 -1
- package/package.json +1 -1
|
@@ -111,6 +111,28 @@ const defaultNav = cva({
|
|
|
111
111
|
density: 'normal'
|
|
112
112
|
}
|
|
113
113
|
});
|
|
114
|
+
// With `views`, the panel and the menu inside it are one-cell grids whose layers are the views
|
|
115
|
+
// they show: one at rest, two while they slide past each other. The cell clips the slide.
|
|
116
|
+
const defaultViewStage = cva({
|
|
117
|
+
base: 'grid min-h-0 flex-1 grid-cols-1 grid-rows-1 overflow-x-clip'
|
|
118
|
+
});
|
|
119
|
+
// `translate` / `opacity` carry the swipe: they follow the finger with no transition while
|
|
120
|
+
// dragging, then ease to rest. The view-change motion animates `transform`, so the two never fight.
|
|
121
|
+
const defaultViewLayer = cva({
|
|
122
|
+
base: 'col-start-1 row-start-1 min-w-0 transition-[translate,opacity] ease-enter data-[dragging=true]:transition-none',
|
|
123
|
+
variants: {
|
|
124
|
+
// A body layer is its own scroll container, and `touch-action` only counts up to the nearest
|
|
125
|
+
// one: set on the layer, it leaves vertical scrolling to the browser and horizontal moves to
|
|
126
|
+
// the back swipe.
|
|
127
|
+
swipe: {
|
|
128
|
+
true: 'touch-pan-y',
|
|
129
|
+
false: null
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
defaultVariants: {
|
|
133
|
+
swipe: false
|
|
134
|
+
}
|
|
135
|
+
});
|
|
114
136
|
const defaultGroup = cva({
|
|
115
137
|
base: 'relative flex w-full min-w-0 flex-col',
|
|
116
138
|
variants: {
|
|
@@ -612,12 +634,19 @@ const defaultMain = cva({
|
|
|
612
634
|
edgeRevealed: {
|
|
613
635
|
true: '',
|
|
614
636
|
false: ''
|
|
637
|
+
},
|
|
638
|
+
// Nothing of the sidebar is left beside the page: the panel is hidden and there is no
|
|
639
|
+
// activity bar. Only then does an inset or split page drop its frame and fill the edge; with
|
|
640
|
+
// an activity bar the rail stays on screen, so the page keeps its framed form.
|
|
641
|
+
flush: {
|
|
642
|
+
true: '',
|
|
643
|
+
false: ''
|
|
615
644
|
}
|
|
616
645
|
},
|
|
617
646
|
compoundVariants: [
|
|
618
647
|
{
|
|
619
648
|
variant: ['inset', 'split'],
|
|
620
|
-
|
|
649
|
+
flush: false,
|
|
621
650
|
class: 'md:[--page-shell-edge-inset:0.5rem]'
|
|
622
651
|
},
|
|
623
652
|
{
|
|
@@ -642,15 +671,32 @@ const defaultMain = cva({
|
|
|
642
671
|
},
|
|
643
672
|
{
|
|
644
673
|
variant: ['inset', 'split'],
|
|
645
|
-
|
|
674
|
+
flush: true,
|
|
646
675
|
class: 'md:p-0 md:rounded-none'
|
|
676
|
+
},
|
|
677
|
+
// A hidden split panel no longer separates the page from the activity bar's card: the page
|
|
678
|
+
// takes the gutter on that side itself, so the two cards stay apart.
|
|
679
|
+
{
|
|
680
|
+
variant: 'split',
|
|
681
|
+
displayState: 'hidden',
|
|
682
|
+
flush: false,
|
|
683
|
+
side: 'left',
|
|
684
|
+
class: 'md:pl-md'
|
|
685
|
+
},
|
|
686
|
+
{
|
|
687
|
+
variant: 'split',
|
|
688
|
+
displayState: 'hidden',
|
|
689
|
+
flush: false,
|
|
690
|
+
side: 'right',
|
|
691
|
+
class: 'md:pr-md'
|
|
647
692
|
}
|
|
648
693
|
],
|
|
649
694
|
defaultVariants: {
|
|
650
695
|
variant: 'admin',
|
|
651
696
|
side: 'left',
|
|
652
697
|
displayState: 'expanded',
|
|
653
|
-
edgeRevealed: false
|
|
698
|
+
edgeRevealed: false,
|
|
699
|
+
flush: false
|
|
654
700
|
}
|
|
655
701
|
});
|
|
656
702
|
const defaultEdgeTrigger = cva({
|
|
@@ -891,6 +937,24 @@ export const defaultSidebarMotion = motion({
|
|
|
891
937
|
out: { axis: 'y', x: 0, y: 0, scale: 1, opacity: 0.2 },
|
|
892
938
|
duration: 'normal',
|
|
893
939
|
easing: 'standard'
|
|
940
|
+
},
|
|
941
|
+
variants: {
|
|
942
|
+
part: {
|
|
943
|
+
collapse: {},
|
|
944
|
+
// The pager between views: the arriving view comes from the inline end and the leaving
|
|
945
|
+
// one goes to the start, side by side, each travelling `x` while it fades between
|
|
946
|
+
// `opacity` and 1. `enter` easing moves on the first frame; an in-out curve would sit
|
|
947
|
+
// still for the first and last fifth and squeeze the whole swap into a blink.
|
|
948
|
+
view: {
|
|
949
|
+
in: { axis: 'x', x: '100%', opacity: 0 },
|
|
950
|
+
out: { axis: 'x', x: '100%', opacity: 0 },
|
|
951
|
+
duration: 'slow',
|
|
952
|
+
easing: 'enter'
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
},
|
|
956
|
+
defaultVariants: {
|
|
957
|
+
part: 'collapse'
|
|
894
958
|
}
|
|
895
959
|
});
|
|
896
960
|
export const sidebarTheme = {
|
|
@@ -899,6 +963,8 @@ export const sidebarTheme = {
|
|
|
899
963
|
panel: defaultPanel,
|
|
900
964
|
header: defaultStackSection,
|
|
901
965
|
nav: defaultNav,
|
|
966
|
+
viewStage: defaultViewStage,
|
|
967
|
+
viewLayer: defaultViewLayer,
|
|
902
968
|
footer: defaultStackSection,
|
|
903
969
|
group: defaultGroup,
|
|
904
970
|
groupLabel: defaultGroupLabel,
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { type Snippet } from 'svelte';
|
|
2
|
+
import type { TransitionConfig } from 'svelte/transition';
|
|
3
|
+
import type { ResolvedMotion } from '../../utils/motion/index.js';
|
|
4
|
+
import type { SidebarApi, SidebarGroup, SidebarMenuButtonItem, SidebarMenuEntry, SidebarSearch, SidebarSide, SidebarView } from './sidebar.props.js';
|
|
5
|
+
/** What the panel renders for a view: the Sidebar's own props, or a view's resolved over its parents. */
|
|
6
|
+
export type SidebarSlotValues = {
|
|
7
|
+
items?: SidebarGroup[];
|
|
8
|
+
content?: Snippet<[SidebarApi]>;
|
|
9
|
+
headerButton?: SidebarMenuButtonItem;
|
|
10
|
+
search?: SidebarSearch;
|
|
11
|
+
headerMenu?: SidebarMenuEntry[];
|
|
12
|
+
header?: Snippet<[SidebarApi]>;
|
|
13
|
+
footerButton?: SidebarMenuButtonItem;
|
|
14
|
+
footerMenu?: SidebarMenuEntry[];
|
|
15
|
+
footer?: Snippet<[SidebarApi]>;
|
|
16
|
+
};
|
|
17
|
+
/** The header and footer props a view can set; together they are the panel's chrome. */
|
|
18
|
+
declare const chromeProps: readonly ["header", "headerButton", "search", "headerMenu", "footerButton", "footerMenu", "footer"];
|
|
19
|
+
/**
|
|
20
|
+
* What slides on a view change. `body` is the menu alone; `panel` is the whole panel (header,
|
|
21
|
+
* menu, footer), which only swaps when the two views do not share the same chrome.
|
|
22
|
+
*/
|
|
23
|
+
export type SidebarViewKind = 'panel' | 'body';
|
|
24
|
+
type Views = Record<string, SidebarView>;
|
|
25
|
+
type PropSlot = (typeof chromeProps)[number];
|
|
26
|
+
/** The view and its ancestors, nearest first. A missing parent or a parent cycle ends the chain. */
|
|
27
|
+
export declare const viewChain: (views: Views, id: string) => string[];
|
|
28
|
+
/**
|
|
29
|
+
* Resolves one header or footer prop for a view: the nearest view in the chain that sets it
|
|
30
|
+
* (`null` counts, and clears it), else the Sidebar. `owner` names where it came from, so two
|
|
31
|
+
* views share the slot exactly when it resolves to the same owner.
|
|
32
|
+
*/
|
|
33
|
+
export declare const resolveSlot: <S extends PropSlot>(views: Views, id: string, slot: S, root: SidebarSlotValues) => {
|
|
34
|
+
owner: string | null;
|
|
35
|
+
value: SidebarSlotValues[S];
|
|
36
|
+
};
|
|
37
|
+
export type SidebarViewDirection = 'forward' | 'back' | 'none';
|
|
38
|
+
/** Which half of a back swipe a layer plays: the view leaving (`over`) or its parent (`under`). */
|
|
39
|
+
export type SidebarSwipeRole = 'over' | 'under';
|
|
40
|
+
export type SidebarViewLayer = {
|
|
41
|
+
key: string;
|
|
42
|
+
view: string;
|
|
43
|
+
role?: SidebarSwipeRole;
|
|
44
|
+
};
|
|
45
|
+
type Swipe = {
|
|
46
|
+
from: string;
|
|
47
|
+
to: string;
|
|
48
|
+
/** `drag` follows the finger; `commit` / `cancel` animate to rest, then the swipe ends. */
|
|
49
|
+
phase: 'drag' | 'commit' | 'cancel';
|
|
50
|
+
/** Distance travelled toward the inline end, in px. */
|
|
51
|
+
offset: number;
|
|
52
|
+
width: number;
|
|
53
|
+
/** The commit has finished moving and waits for the view change to land (a route, say). */
|
|
54
|
+
settled?: boolean;
|
|
55
|
+
};
|
|
56
|
+
type SidebarViewsOptions = {
|
|
57
|
+
readonly views: Views | undefined;
|
|
58
|
+
readonly view: string | undefined;
|
|
59
|
+
readonly defaultView: string | undefined;
|
|
60
|
+
readonly onViewChange: ((view: string) => void) | undefined;
|
|
61
|
+
readonly isMobile: boolean;
|
|
62
|
+
readonly side: SidebarSide;
|
|
63
|
+
readonly root: SidebarSlotValues;
|
|
64
|
+
setViewProp: (view: string | undefined) => void;
|
|
65
|
+
};
|
|
66
|
+
export declare class SidebarViewsState {
|
|
67
|
+
#private;
|
|
68
|
+
private options;
|
|
69
|
+
swipe: Swipe | null;
|
|
70
|
+
rtl: boolean;
|
|
71
|
+
constructor(options: SidebarViewsOptions);
|
|
72
|
+
get enabled(): boolean;
|
|
73
|
+
/** The view on screen: the selected one when it exists, else `defaultView`, else the first. */
|
|
74
|
+
get current(): string | undefined;
|
|
75
|
+
depth(id: string): number;
|
|
76
|
+
parentOf(id: string): string | undefined;
|
|
77
|
+
labelOf(id: string): string | undefined;
|
|
78
|
+
/**
|
|
79
|
+
* The last change of the current view and its direction, read by the layer transitions when
|
|
80
|
+
* they start. Deeper slides forward and shallower slides back; at one depth (sections), the
|
|
81
|
+
* later view in `views` is forward, like pages in order.
|
|
82
|
+
*/
|
|
83
|
+
get lastChange(): {
|
|
84
|
+
from?: string;
|
|
85
|
+
to?: string;
|
|
86
|
+
direction: SidebarViewDirection;
|
|
87
|
+
};
|
|
88
|
+
setView: (view: string) => void;
|
|
89
|
+
/** A view's body. */
|
|
90
|
+
body(view: string): {
|
|
91
|
+
items: SidebarGroup[] | undefined;
|
|
92
|
+
content: Snippet<[SidebarApi]> | undefined;
|
|
93
|
+
};
|
|
94
|
+
/** A view's header and footer props, each from the nearest view that sets it, else the Sidebar. */
|
|
95
|
+
chrome(view: string): SidebarSlotValues;
|
|
96
|
+
/**
|
|
97
|
+
* The layers a stage renders, bottom first. Outside a swipe that is the current view alone;
|
|
98
|
+
* during one it is the parent under the view being swiped away. Two views that share a layer
|
|
99
|
+
* key collapse to one layer that never moves: the panel's key is where its chrome comes from,
|
|
100
|
+
* so views with the same header and footer keep one panel and only their bodies slide.
|
|
101
|
+
*/
|
|
102
|
+
layers(kind: SidebarViewKind): SidebarViewLayer[];
|
|
103
|
+
/**
|
|
104
|
+
* A swipe layer's drag position, as `translate` / `opacity` styles; empty at rest. The view
|
|
105
|
+
* under the finger stays opaque and follows it; its parent comes from `travel` toward the
|
|
106
|
+
* start and from `opacity`, the same way the view motion brings a view in.
|
|
107
|
+
*/
|
|
108
|
+
swipeStyle(role: SidebarSwipeRole | undefined, travel: number | string, opacity: number): {
|
|
109
|
+
translate?: undefined;
|
|
110
|
+
opacity?: undefined;
|
|
111
|
+
} | {
|
|
112
|
+
translate: string;
|
|
113
|
+
opacity: number;
|
|
114
|
+
};
|
|
115
|
+
get canSwipeBack(): boolean;
|
|
116
|
+
/**
|
|
117
|
+
* The mobile drawer dismisses on a swipe toward its own edge. When going back points the same
|
|
118
|
+
* way (a right drawer in LTR, a left one in RTL), the view body keeps the gesture.
|
|
119
|
+
*/
|
|
120
|
+
get ownsDrawerSwipe(): boolean;
|
|
121
|
+
/** Called by the body stage once a settling swipe has had its motion duration. */
|
|
122
|
+
settleSwipe(): void;
|
|
123
|
+
/** Back swipe on the view body: toward the inline end, anywhere on the body, mobile only. */
|
|
124
|
+
swipeAttachment: (node: HTMLElement) => void | (() => void);
|
|
125
|
+
}
|
|
126
|
+
type ViewTransitionParams = {
|
|
127
|
+
phase: 'in' | 'out';
|
|
128
|
+
direction: SidebarViewDirection;
|
|
129
|
+
motion: ResolvedMotion;
|
|
130
|
+
};
|
|
131
|
+
/**
|
|
132
|
+
* Pager motion between views. Forward, the new view comes from the inline end and the old one
|
|
133
|
+
* leaves to the start, side by side, each travelling the motion's `x` and fading along the whole
|
|
134
|
+
* way; back mirrors it. A layer a swipe put in place is already where it belongs, so it enters
|
|
135
|
+
* and leaves without motion.
|
|
136
|
+
*/
|
|
137
|
+
export declare const sidebarViewTransition: (node: HTMLElement, { phase, direction, motion }: ViewTransitionParams) => TransitionConfig;
|
|
138
|
+
export {};
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { untrack } from 'svelte';
|
|
2
|
+
import { easingFunctions } from '../../transitions/easingFunctions.js';
|
|
3
|
+
import { createPointerDrag } from '../../utils/pointerDrag.js';
|
|
4
|
+
import { createBindableValue } from '../../utils/state.svelte.js';
|
|
5
|
+
/** The header and footer props a view can set; together they are the panel's chrome. */
|
|
6
|
+
const chromeProps = [
|
|
7
|
+
'header',
|
|
8
|
+
'headerButton',
|
|
9
|
+
'search',
|
|
10
|
+
'headerMenu',
|
|
11
|
+
'footerButton',
|
|
12
|
+
'footerMenu',
|
|
13
|
+
'footer'
|
|
14
|
+
];
|
|
15
|
+
/** The view and its ancestors, nearest first. A missing parent or a parent cycle ends the chain. */
|
|
16
|
+
export const viewChain = (views, id) => {
|
|
17
|
+
const chain = [];
|
|
18
|
+
for (let key = id; key !== undefined && key in views;) {
|
|
19
|
+
if (chain.includes(key))
|
|
20
|
+
break;
|
|
21
|
+
chain.push(key);
|
|
22
|
+
key = views[key].parent;
|
|
23
|
+
}
|
|
24
|
+
return chain;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Resolves one header or footer prop for a view: the nearest view in the chain that sets it
|
|
28
|
+
* (`null` counts, and clears it), else the Sidebar. `owner` names where it came from, so two
|
|
29
|
+
* views share the slot exactly when it resolves to the same owner.
|
|
30
|
+
*/
|
|
31
|
+
export const resolveSlot = (views, id, slot, root) => {
|
|
32
|
+
const owner = viewChain(views, id).find((key) => views[key][slot] !== undefined);
|
|
33
|
+
const value = owner === undefined ? root[slot] : views[owner][slot];
|
|
34
|
+
return { owner: owner ?? null, value: (value ?? undefined) };
|
|
35
|
+
};
|
|
36
|
+
const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
|
|
37
|
+
const cssLength = (value) => typeof value === 'number' ? `${value}px` : (value ?? '0px');
|
|
38
|
+
export class SidebarViewsState {
|
|
39
|
+
options;
|
|
40
|
+
swipe = $state(null);
|
|
41
|
+
rtl = $state(false);
|
|
42
|
+
#selected;
|
|
43
|
+
#shown;
|
|
44
|
+
#lastChange = {
|
|
45
|
+
direction: 'none'
|
|
46
|
+
};
|
|
47
|
+
constructor(options) {
|
|
48
|
+
this.options = options;
|
|
49
|
+
this.#selected = createBindableValue(() => options.view, (view) => options.setViewProp(view), () => options.defaultView);
|
|
50
|
+
// A committed swipe lets go once the view it asked for is on screen; any other view change
|
|
51
|
+
// (a route, a parent update) ends the swipe on the spot.
|
|
52
|
+
$effect(() => {
|
|
53
|
+
const swipe = this.swipe;
|
|
54
|
+
if (!swipe)
|
|
55
|
+
return;
|
|
56
|
+
const current = this.current;
|
|
57
|
+
if ((swipe.settled && current === swipe.to) ||
|
|
58
|
+
(current !== swipe.from && current !== swipe.to))
|
|
59
|
+
this.swipe = null;
|
|
60
|
+
});
|
|
61
|
+
this.#shown = untrack(() => this.current);
|
|
62
|
+
}
|
|
63
|
+
get enabled() {
|
|
64
|
+
return !!this.options.views && Object.keys(this.options.views).length > 0;
|
|
65
|
+
}
|
|
66
|
+
/** The view on screen: the selected one when it exists, else `defaultView`, else the first. */
|
|
67
|
+
get current() {
|
|
68
|
+
const views = this.options.views;
|
|
69
|
+
if (!views)
|
|
70
|
+
return undefined;
|
|
71
|
+
const selected = this.#selected.value;
|
|
72
|
+
if (selected !== undefined && selected in views)
|
|
73
|
+
return selected;
|
|
74
|
+
const fallback = this.options.defaultView;
|
|
75
|
+
return fallback !== undefined && fallback in views ? fallback : Object.keys(views)[0];
|
|
76
|
+
}
|
|
77
|
+
depth(id) {
|
|
78
|
+
return this.options.views ? viewChain(this.options.views, id).length - 1 : 0;
|
|
79
|
+
}
|
|
80
|
+
parentOf(id) {
|
|
81
|
+
const parent = this.options.views?.[id]?.parent;
|
|
82
|
+
return parent !== undefined && parent in (this.options.views ?? {}) ? parent : undefined;
|
|
83
|
+
}
|
|
84
|
+
labelOf(id) {
|
|
85
|
+
return this.options.views?.[id]?.label;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The last change of the current view and its direction, read by the layer transitions when
|
|
89
|
+
* they start. Deeper slides forward and shallower slides back; at one depth (sections), the
|
|
90
|
+
* later view in `views` is forward, like pages in order.
|
|
91
|
+
*/
|
|
92
|
+
get lastChange() {
|
|
93
|
+
const to = this.current;
|
|
94
|
+
if (to === this.#shown)
|
|
95
|
+
return this.#lastChange;
|
|
96
|
+
const from = this.#shown;
|
|
97
|
+
this.#shown = to;
|
|
98
|
+
if (from === undefined || to === undefined) {
|
|
99
|
+
this.#lastChange = { from, to, direction: 'none' };
|
|
100
|
+
return this.#lastChange;
|
|
101
|
+
}
|
|
102
|
+
const order = Object.keys(this.options.views ?? {});
|
|
103
|
+
const delta = this.depth(to) - this.depth(from) || order.indexOf(to) - order.indexOf(from);
|
|
104
|
+
this.#lastChange = { from, to, direction: delta < 0 ? 'back' : 'forward' };
|
|
105
|
+
return this.#lastChange;
|
|
106
|
+
}
|
|
107
|
+
setView = (view) => {
|
|
108
|
+
if (!this.options.views || !(view in this.options.views) || view === this.current)
|
|
109
|
+
return;
|
|
110
|
+
this.#selected.value = view;
|
|
111
|
+
this.options.onViewChange?.(view);
|
|
112
|
+
};
|
|
113
|
+
/** A view's body. */
|
|
114
|
+
body(view) {
|
|
115
|
+
const entry = this.options.views?.[view];
|
|
116
|
+
return { items: entry?.items, content: entry?.content };
|
|
117
|
+
}
|
|
118
|
+
/** A view's header and footer props, each from the nearest view that sets it, else the Sidebar. */
|
|
119
|
+
chrome(view) {
|
|
120
|
+
const chrome = {};
|
|
121
|
+
for (const prop of chromeProps) {
|
|
122
|
+
const { value } = resolveSlot(this.options.views ?? {}, view, prop, this.options.root);
|
|
123
|
+
if (value != null)
|
|
124
|
+
Object.assign(chrome, { [prop]: value });
|
|
125
|
+
}
|
|
126
|
+
return chrome;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The layers a stage renders, bottom first. Outside a swipe that is the current view alone;
|
|
130
|
+
* during one it is the parent under the view being swiped away. Two views that share a layer
|
|
131
|
+
* key collapse to one layer that never moves: the panel's key is where its chrome comes from,
|
|
132
|
+
* so views with the same header and footer keep one panel and only their bodies slide.
|
|
133
|
+
*/
|
|
134
|
+
layers(kind) {
|
|
135
|
+
const current = this.current;
|
|
136
|
+
if (current === undefined)
|
|
137
|
+
return [];
|
|
138
|
+
const swipe = this.swipe;
|
|
139
|
+
const entries = swipe
|
|
140
|
+
? [
|
|
141
|
+
{ view: swipe.to, role: 'under' },
|
|
142
|
+
{ view: swipe.from, role: 'over' }
|
|
143
|
+
]
|
|
144
|
+
: [{ view: current }];
|
|
145
|
+
const layers = [];
|
|
146
|
+
for (const entry of entries) {
|
|
147
|
+
const key = kind === 'body' ? entry.view : this.#chromeKey(entry.view);
|
|
148
|
+
const shared = layers.find((layer) => layer.key === key);
|
|
149
|
+
if (shared)
|
|
150
|
+
shared.role = undefined;
|
|
151
|
+
else
|
|
152
|
+
layers.push({ key, ...entry });
|
|
153
|
+
}
|
|
154
|
+
return layers;
|
|
155
|
+
}
|
|
156
|
+
#chromeKey(view) {
|
|
157
|
+
const views = this.options.views ?? {};
|
|
158
|
+
return chromeProps
|
|
159
|
+
.map((prop) => resolveSlot(views, view, prop, this.options.root).owner ?? '')
|
|
160
|
+
.join('|');
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* A swipe layer's drag position, as `translate` / `opacity` styles; empty at rest. The view
|
|
164
|
+
* under the finger stays opaque and follows it; its parent comes from `travel` toward the
|
|
165
|
+
* start and from `opacity`, the same way the view motion brings a view in.
|
|
166
|
+
*/
|
|
167
|
+
swipeStyle(role, travel, opacity) {
|
|
168
|
+
const swipe = this.swipe;
|
|
169
|
+
if (!swipe || !role)
|
|
170
|
+
return {};
|
|
171
|
+
const sign = this.rtl ? -1 : 1;
|
|
172
|
+
const rest = `calc(${cssLength(travel)} * ${-sign})`;
|
|
173
|
+
const progress = swipe.width ? swipe.offset / swipe.width : 0;
|
|
174
|
+
if (swipe.phase === 'drag') {
|
|
175
|
+
if (role === 'over')
|
|
176
|
+
return {
|
|
177
|
+
translate: `${swipe.offset * sign}px`,
|
|
178
|
+
opacity: 1 - (1 - opacity) * progress
|
|
179
|
+
};
|
|
180
|
+
return {
|
|
181
|
+
translate: `calc(${cssLength(travel)} * ${-sign * (1 - progress)})`,
|
|
182
|
+
opacity: opacity + (1 - opacity) * progress
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
// Settling: a committed view leaves to the inline end; a cancelled parent returns under it.
|
|
186
|
+
if (role === 'over')
|
|
187
|
+
return swipe.phase === 'commit' ? { translate: `${100 * sign}%`, opacity } : {};
|
|
188
|
+
return swipe.phase === 'cancel' ? { translate: rest, opacity } : {};
|
|
189
|
+
}
|
|
190
|
+
get canSwipeBack() {
|
|
191
|
+
const current = this.current;
|
|
192
|
+
return this.options.isMobile && current !== undefined && !!this.parentOf(current);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* The mobile drawer dismisses on a swipe toward its own edge. When going back points the same
|
|
196
|
+
* way (a right drawer in LTR, a left one in RTL), the view body keeps the gesture.
|
|
197
|
+
*/
|
|
198
|
+
get ownsDrawerSwipe() {
|
|
199
|
+
return this.canSwipeBack && (this.rtl ? -1 : 1) === (this.options.side === 'right' ? 1 : -1);
|
|
200
|
+
}
|
|
201
|
+
/** Called by the body stage once a settling swipe has had its motion duration. */
|
|
202
|
+
settleSwipe() {
|
|
203
|
+
const swipe = this.swipe;
|
|
204
|
+
if (!swipe || swipe.phase === 'drag')
|
|
205
|
+
return;
|
|
206
|
+
if (swipe.phase === 'cancel' || this.current === swipe.to)
|
|
207
|
+
this.swipe = null;
|
|
208
|
+
else
|
|
209
|
+
swipe.settled = true;
|
|
210
|
+
// ponytail: a controlled `view` that ignores `onViewChange` leaves the swipe parked off
|
|
211
|
+
// screen until some view change lands; fine for a consumer bug, revisit if one shows up.
|
|
212
|
+
}
|
|
213
|
+
/** Back swipe on the view body: toward the inline end, anywhere on the body, mobile only. */
|
|
214
|
+
swipeAttachment = (node) => {
|
|
215
|
+
this.rtl = getComputedStyle(node).direction === 'rtl';
|
|
216
|
+
let candidate = false;
|
|
217
|
+
let sign = 1;
|
|
218
|
+
let lastX = 0;
|
|
219
|
+
let lastTime = 0;
|
|
220
|
+
let velocity = 0;
|
|
221
|
+
const release = (cancelled, timeStamp) => {
|
|
222
|
+
candidate = false;
|
|
223
|
+
const swipe = this.swipe;
|
|
224
|
+
if (swipe?.phase !== 'drag')
|
|
225
|
+
return;
|
|
226
|
+
// A long stationary hold is not a flick: the last velocity sample may be stale.
|
|
227
|
+
if (timeStamp - lastTime > 100)
|
|
228
|
+
velocity = 0;
|
|
229
|
+
const commit = !cancelled && (velocity > 0.4 || (swipe.offset > swipe.width * 0.35 && velocity > -0.2));
|
|
230
|
+
swipe.phase = commit ? 'commit' : 'cancel';
|
|
231
|
+
if (commit)
|
|
232
|
+
this.setView(swipe.to);
|
|
233
|
+
};
|
|
234
|
+
return createPointerDrag({
|
|
235
|
+
disabled: () => !this.canSwipeBack || this.swipe !== null,
|
|
236
|
+
// The body is a region full of rows, not a handle: a press that never travels keeps its click.
|
|
237
|
+
capture: 'on-activate',
|
|
238
|
+
onDown: ({ event }) => {
|
|
239
|
+
candidate = true;
|
|
240
|
+
this.rtl = getComputedStyle(node).direction === 'rtl';
|
|
241
|
+
sign = this.rtl ? -1 : 1;
|
|
242
|
+
lastX = event.clientX;
|
|
243
|
+
lastTime = event.timeStamp;
|
|
244
|
+
velocity = 0;
|
|
245
|
+
},
|
|
246
|
+
// Horizontal toward the inline end, clearly more than vertical: anything else is a scroll.
|
|
247
|
+
shouldActivate: ({ deltaX, deltaY }) => {
|
|
248
|
+
if (!candidate)
|
|
249
|
+
return false;
|
|
250
|
+
const forward = deltaX * sign;
|
|
251
|
+
if (forward > 8 && forward > Math.abs(deltaY) * 1.5)
|
|
252
|
+
return true;
|
|
253
|
+
if (Math.abs(deltaX) > 8 || Math.abs(deltaY) > 8)
|
|
254
|
+
candidate = false;
|
|
255
|
+
return false;
|
|
256
|
+
},
|
|
257
|
+
onStart: () => {
|
|
258
|
+
const from = this.current;
|
|
259
|
+
const to = from === undefined ? undefined : this.parentOf(from);
|
|
260
|
+
if (from === undefined || to === undefined)
|
|
261
|
+
return false;
|
|
262
|
+
this.swipe = { from, to, phase: 'drag', offset: 0, width: node.offsetWidth };
|
|
263
|
+
},
|
|
264
|
+
onMove: ({ event, deltaX }) => {
|
|
265
|
+
const swipe = this.swipe;
|
|
266
|
+
if (swipe?.phase !== 'drag')
|
|
267
|
+
return;
|
|
268
|
+
swipe.offset = clamp(deltaX * sign, 0, swipe.width);
|
|
269
|
+
const elapsed = event.timeStamp - lastTime;
|
|
270
|
+
if (elapsed >= 8) {
|
|
271
|
+
velocity = ((event.clientX - lastX) * sign) / elapsed;
|
|
272
|
+
lastX = event.clientX;
|
|
273
|
+
lastTime = event.timeStamp;
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
onEnd: ({ event }) => release(false, event.timeStamp),
|
|
277
|
+
onCancel: ({ event }) => release(true, event.timeStamp)
|
|
278
|
+
})(node);
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Pager motion between views. Forward, the new view comes from the inline end and the old one
|
|
283
|
+
* leaves to the start, side by side, each travelling the motion's `x` and fading along the whole
|
|
284
|
+
* way; back mirrors it. A layer a swipe put in place is already where it belongs, so it enters
|
|
285
|
+
* and leaves without motion.
|
|
286
|
+
*/
|
|
287
|
+
export const sidebarViewTransition = (node, { phase, direction, motion }) => {
|
|
288
|
+
if (phase === 'out')
|
|
289
|
+
node.style.pointerEvents = 'none';
|
|
290
|
+
const side = phase === 'in' ? motion.in : motion.out;
|
|
291
|
+
const duration = side.duration ?? 0;
|
|
292
|
+
if (node.dataset.swipe || direction === 'none' || duration === 0)
|
|
293
|
+
return { duration: 0 };
|
|
294
|
+
const travel = cssLength(side.x);
|
|
295
|
+
const rest = side.opacity ?? 0;
|
|
296
|
+
const rtl = getComputedStyle(node).direction === 'rtl' ? -1 : 1;
|
|
297
|
+
const sign = rtl * (direction === 'back' ? -1 : 1) * (phase === 'in' ? 1 : -1);
|
|
298
|
+
return {
|
|
299
|
+
duration,
|
|
300
|
+
easing: easingFunctions[side.easing ?? 'cubicOut'],
|
|
301
|
+
css: (t, u) => `transform: translateX(calc(${travel} * ${u * sign})); opacity: ${rest + (1 - rest) * t}`
|
|
302
|
+
};
|
|
303
|
+
};
|
|
@@ -14,7 +14,12 @@ export declare class ThemeFloatingWindows {
|
|
|
14
14
|
private surfaces;
|
|
15
15
|
readonly layer: Attachment<HTMLElement>;
|
|
16
16
|
readonly portal: Attachment<HTMLElement>;
|
|
17
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Brings a surface to the top. `reserveBelow` also claims the index just under it, for a
|
|
19
|
+
* window's backdrop: the counter never hands an index out twice, so no other surface can land
|
|
20
|
+
* between a window and its backdrop.
|
|
21
|
+
*/
|
|
22
|
+
activate(id: string, type: FloatingWindowSurface, reserveBelow?: boolean): number;
|
|
18
23
|
unregisterSurface(id: string, type: FloatingWindowSurface): void;
|
|
19
24
|
isTopWindow(id: string): boolean;
|
|
20
25
|
registerDock(entry: FloatingWindowDockEntry): () => void;
|
|
@@ -20,8 +20,13 @@ export class ThemeFloatingWindows {
|
|
|
20
20
|
node.remove();
|
|
21
21
|
};
|
|
22
22
|
};
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Brings a surface to the top. `reserveBelow` also claims the index just under it, for a
|
|
25
|
+
* window's backdrop: the counter never hands an index out twice, so no other surface can land
|
|
26
|
+
* between a window and its backdrop.
|
|
27
|
+
*/
|
|
28
|
+
activate(id, type, reserveBelow = false) {
|
|
29
|
+
this.zIndex += reserveBelow ? 2 : 1;
|
|
25
30
|
this.surfaces.set(id, { type, zIndex: this.zIndex });
|
|
26
31
|
return this.zIndex;
|
|
27
32
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const themeDescription = "\n# Theme\n\n`Theme` owns global theme selection, runtime design tokens, shared overlay state, and theme\ntransitions. Wrap the application once and use the `ThemeState` received by the children snippet.\n\n## Runtime design tokens\n\n```svelte\n<script lang=\"ts\">\n\timport { Theme, type ThemeDesignTokenMap } from 'entasis/theme';\n\n\tlet spacing = $state<'small' | 'normal' | 'large'>('normal');\n\tconst designTokens = $derived({\n\t\tlight: {\n\t\t\tspacing,\n\t\t\tspacingScale: { xs: 1, sm: 1.5, md: 2, lg: 3, xl: 4 },\n\t\t\tradius: 'normal',\n\t\t\ttypeScale: 'default',\n\t\t\traisedWithBorder: true,\n\t\t\tdefaultColor: 'neutral'\n\t\t},\n\t\tdark: {\n\t\t\tspacing,\n\t\t\tspacingScale: { xs: 1, sm: 1.5, md: 2, lg: 3, xl: 4 },\n\t\t\tradius: 'small',\n\t\t\ttypeScale: 'compact',\n\t\t\traisedWithBorder: false,\n\t\t\tdefaultColor: 'neutral'\n\t\t}\n\t} satisfies ThemeDesignTokenMap<readonly ['light', 'dark']>);\n</script>\n\n<Theme {designTokens} transition=\"radial-top-right\">\n\t{#snippet children(theme)}\n\t\t<button onclick={() => (spacing = spacing === 'small' ? 'large' : 'small')}>\n\t\t\tChange density\n\t\t</button>\n\t\t<button onclick={() => (theme.theme = theme.resolvedTheme === 'dark' ? 'light' : 'dark')}>\n\t\t\tToggle color scheme\n\t\t</button>\n\t{/snippet}\n</Theme>\n```\n\n`designTokens` is keyed by logical theme name and respects the `attribute` and `value` props.\nEleven presets ship as `themePresets` (`dense`, `compact`, `balanced`, `comfortable`, `spacious`,\n`sharp`, `rounded`, `display`, `editorial`, `glass`, `terminal`): `designTokens={{ light: themePresets.glass.tokens, dark: themePresets.glass.tokens }}`.\nChanging the controlled object updates already-rendered Tailwind utilities without rebuilding CSS.\n\n### ThemeDesignTokens\n\n- `spacing`: `'small' | 'normal' | 'large' | number`. Globally scales density.\n- `spacingScale`: partial overrides for the strictly increasing `xs`, `sm`, `md`, `lg`,\n and `xl` spacing multipliers. Defaults to 1/1.5/2/3/4.\n- `radius`: `'none' | 'subtile' | 'small' | 'normal' | 'large' | 'round' | number`.\n- `typeScale`: `'compact' | 'default' | 'comfortable' | 'large' | TypeScaleOptions`.\n- `raisedWithBorder`: toggles the border used by `raised-*` utilities.\n- `defaultColor`: `Colors` role kit chrome inherits when a control omits `color`.\n Defaults to `neutral`. Set `primary` to restore an accent-colored kit. Compiles the\n current-color family (`--color`, `--color-readable`, muted/contrast/light/dark variants)\n and `--default-color` onto the theme selector. Do not set `data-color` on `html`.\n- `focusColor`, `selectedColor`, `hoverColor`, `pressedColor`: the four `Colors` **state\n roles**. They pin, for the whole theme, what a focus ring, a persistent selection and the\n transient hover/pressed layer look like, independently of the role of the control the state\n lands on. They compile `--color-focus`, `--color-selected` (plus its `-contrast`,\n `-readable` and `-muted-readable` companions), `--color-hover` and\n `--color-pressed` onto the theme selector. There is no `--color-selected-muted`: the soft\n fill is a translucent tint of `--color-selected` at `--state-selected-opacity`, so it reads\n on any surface. None is declared at `:root`: every use site\n falls back to the matching current role (`ring-focus` is\n `var(--color-focus, var(--color))`, `bg-selected-muted` tints\n `var(--color-selected, var(--color))`, the state layer is\n `var(--color-hover, currentColor)` and on `:active`\n `var(--color-pressed, var(--color-hover, currentColor))`), so leaving them unset changes\n nothing and `data-color` keeps moving the states with `--color`. Theme-level only: there is\n no per-component override. An unknown role throws.\n\nComponent-level density remains a local variant. It selects utility classes whose values inherit\nthe active global spacing token.\n\nGenerated interfaces should use the public `xs | sm | md | lg | xl` vocabulary through component\nprops and named gap/padding utilities. `micro` and `layout-*` are internal recipe tokens. Prefer\nparent-owned gaps over child margins; do not emit arbitrary spacing or unsupported radius values.\n\n## Theme selection\n\nThe selection props wrap `svelte-themes`: `themes`, `defaultTheme`, `forcedTheme`,\n`systemTheme`, `syncColorScheme`, `transitionOnChange`, `storageKey`, `attribute`,\n`value`, and `colorScheme`. The default themes are light and dark, with system selection\nenabled. `systemTheme`, `syncColorScheme`, and `transitionOnChange` all default to\n`true`; they map onto the library's `enableSystem`, `enableColorScheme`, and\n`disableTransitionOnChange` options.\n\n`ThemeState` exposes `theme`, `resolvedTheme`, `themes`, and `systemTheme`. Assign\n`theme.theme` to switch themes. The optional `transition` prop applies a named view transition;\nunsupported browsers and reduced-motion users switch instantly.\n\n`spinnerVariant` sets the global default spinner animation. The children snippet is required.\n\n## Motion tokens\n\nMotion is a token scale like spacing and radius: five duration steps and four easing roles.\n`motion` retunes them app-wide; an omitted token keeps its default.\n\n| Duration | Default | | Easing role | Default |\n| ---------- | ------- | --- | ------------ | ------------- |\n| `instant` | 0ms | | `standard` | `cubicInOut` |\n| `fast` | 100ms | | `enter` | `cubicOut` |\n| `normal` | 200ms | | `exit` | `cubicIn` |\n| `slow` | 300ms | | `emphasized` | `backOut` |\n| `slower` | 500ms | | | |\n\n```svelte\n<script lang=\"ts\">\n\timport { Theme } from 'entasis/theme';\n\n\tlet { children } = $props();\n</script>\n\n<Theme motion={{ duration: { normal: 150, slow: 260 }, easing: { standard: 'quintOut' } }}>\n\t{@render children()}\n</Theme>\n```\n\nThe Tailwind plugin emits the same scale as CSS variables on `html` (`--duration-normal`,\n`--ease-standard`, ...) plus the matching `duration-*` / `ease-*` utilities, so CSS transitions\nand Svelte transitions read one set of numbers. The `motion` prop rewrites those variables on\n`html` at runtime and `designTokens.motion` rewrites them again per theme, layered over the prop;\n`ThemeState.motion` resolves through the same two rungs, so the utilities and the presets never\ndisagree. `ThemeState.transition` is a deprecated alias for its `normal` duration and `standard`\neasing. Reduced motion resolves every duration to 0 and collapses the `--duration-*` variables via\nthe `data-entasis-reduce-motion` attribute on `html`.\n\nComponents keep their own transition in a reserved `motion` slot on their theme, so the `theme`\nprop covers motion as well as classes:\n\n```svelte\n<script lang=\"ts\">\n\timport { Dialog } from 'entasis/dialog';\n</script>\n\n<Dialog theme={{ motion: { duration: 'fast', easing: 'emphasized' } }} title=\"Quick\">Body</Dialog>\n```\n\n## Component theme registry\n\n`components` sets app-wide component theme defaults without a wrapper component per component:\nit is keyed by theme name (`dialog`, `button`, ...) and each entry takes the same slots as that\ncomponent's `theme` prop, the `motion` slot included. A `set<Component>Theme` call in a subtree\nbeats the registry, and an instance `theme` prop beats both \u2014 per slot: each rung layers on the one\nbelow it, so a subtree that restyles one slot keeps the registry's others.\n\n```svelte\n<script lang=\"ts\">\n\timport { Theme } from 'entasis/theme';\n\n\tlet { children } = $props();\n</script>\n\n<Theme\n\tcomponents={{\n\t\tdialog: { motion: { duration: 'fast' }, content: { base: 'rounded-2xl' } },\n\t\tbutton: { root: { base: 'tracking-wide' } }\n\t}}\n>\n\t{@render children()}\n</Theme>\n```\n\n## Reduced motion\n\n`reduceMotion` forces reduced motion on (`true`) or off (`false`) for every entasis animation,\noverriding the OS `prefers-reduced-motion` setting; omit it to follow the OS. The live result is\nexposed as `ThemeState.preferReducesMotion` (reactive, so it updates when the OS setting changes)\nand mirrored as a `data-entasis-reduce-motion` attribute on `html` for CSS-only animations.\n\n```svelte\n<script>\n\timport { Theme } from 'entasis/theme';\n\n\tlet { children } = $props();\n</script>\n\n<Theme reduceMotion>{@render children()}</Theme>\n```\n\n## Build-time boundary\n\nThe Tailwind plugin still generates color palettes and registers utility names, variants,\nkeyframes, and spinner CSS. Spacing, radius, typography scale, raised borders,\n`defaultColor` and the four state roles (`focusColor`, `selectedColor`, `hoverColor`,\n`pressedColor`) belong to `Theme.designTokens`; colors remain CSS variables and can be\noverridden directly. `ThemeState.defaultColor` exposes the active role. Kit chrome should\nresolve omitted `color` props with `useDefaultColor`.\n";
|
|
1
|
+
export declare const themeDescription = "\n# Theme\n\n`Theme` owns global theme selection, runtime design tokens, shared overlay state, and theme\ntransitions. Wrap the application once and use the `ThemeState` received by the children snippet.\n\n## Runtime design tokens\n\n```svelte\n<script lang=\"ts\">\n\timport { Theme, type ThemeDesignTokenMap } from 'entasis/theme';\n\n\tlet spacing = $state<'small' | 'normal' | 'large'>('normal');\n\tconst designTokens = $derived({\n\t\tlight: {\n\t\t\tspacing,\n\t\t\tspacingScale: { xs: 1, sm: 1.5, md: 2, lg: 3, xl: 4 },\n\t\t\tradius: 'normal',\n\t\t\ttypeScale: 'default',\n\t\t\traisedWithBorder: true,\n\t\t\tdefaultColor: 'neutral'\n\t\t},\n\t\tdark: {\n\t\t\tspacing,\n\t\t\tspacingScale: { xs: 1, sm: 1.5, md: 2, lg: 3, xl: 4 },\n\t\t\tradius: 'small',\n\t\t\ttypeScale: 'compact',\n\t\t\traisedWithBorder: false,\n\t\t\tdefaultColor: 'neutral'\n\t\t}\n\t} satisfies ThemeDesignTokenMap<readonly ['light', 'dark']>);\n</script>\n\n<Theme {designTokens} transition=\"radial-top-right\">\n\t{#snippet children(theme)}\n\t\t<button onclick={() => (spacing = spacing === 'small' ? 'large' : 'small')}>\n\t\t\tChange density\n\t\t</button>\n\t\t<button onclick={() => (theme.theme = theme.resolvedTheme === 'dark' ? 'light' : 'dark')}>\n\t\t\tToggle color scheme\n\t\t</button>\n\t{/snippet}\n</Theme>\n```\n\n`designTokens` is keyed by logical theme name and respects the `attribute` and `value` props.\nEleven presets ship as `themePresets` (`dense`, `compact`, `balanced`, `comfortable`, `spacious`,\n`sharp`, `rounded`, `display`, `editorial`, `glass`, `terminal`): `designTokens={{ light: themePresets.glass.tokens, dark: themePresets.glass.tokens }}`.\nChanging the controlled object updates already-rendered Tailwind utilities without rebuilding CSS.\n\n### ThemeDesignTokens\n\n- `spacing`: `'small' | 'normal' | 'large' | number`. Globally scales density.\n- `spacingScale`: partial overrides for the strictly increasing `xs`, `sm`, `md`, `lg`,\n and `xl` spacing multipliers. Defaults to 1/1.5/2/3/4.\n- `radius`: `'none' | 'subtile' | 'small' | 'normal' | 'large' | 'round' | number`.\n Controls take the full multiplier; surface steps (`lg` and up) stop at `large` (1.5\u00D7).\n- `typeScale`: `'compact' | 'default' | 'comfortable' | 'large' | TypeScaleOptions`.\n- `raisedWithBorder`: toggles the border used by `raised-*` utilities.\n- `defaultColor`: `Colors` role kit chrome inherits when a control omits `color`.\n Defaults to `neutral`. Set `primary` to restore an accent-colored kit. Compiles the\n current-color family (`--color`, `--color-readable`, muted/contrast/light/dark variants)\n and `--default-color` onto the theme selector. Do not set `data-color` on `html`.\n- `focusColor`, `selectedColor`, `hoverColor`, `pressedColor`: the four `Colors` **state\n roles**. They pin, for the whole theme, what a focus ring, a persistent selection and the\n transient hover/pressed layer look like, independently of the role of the control the state\n lands on. They compile `--color-focus`, `--color-selected` (plus its `-contrast`,\n `-readable` and `-muted-readable` companions), `--color-hover` and\n `--color-pressed` onto the theme selector. There is no `--color-selected-muted`: the soft\n fill is a translucent tint of `--color-selected` at `--state-selected-opacity`, so it reads\n on any surface. None is declared at `:root`: every use site\n falls back to the matching current role (`ring-focus` is\n `var(--color-focus, var(--color))`, `bg-selected-muted` tints\n `var(--color-selected, var(--color))`, the state layer is\n `var(--color-hover, currentColor)` and on `:active`\n `var(--color-pressed, var(--color-hover, currentColor))`), so leaving them unset changes\n nothing and `data-color` keeps moving the states with `--color`. Theme-level only: there is\n no per-component override. An unknown role throws.\n\nComponent-level density remains a local variant. It selects utility classes whose values inherit\nthe active global spacing token.\n\nGenerated interfaces should use the public `xs | sm | md | lg | xl` vocabulary through component\nprops and named gap/padding utilities. `micro` and `layout-*` are internal recipe tokens. Prefer\nparent-owned gaps over child margins; do not emit arbitrary spacing or unsupported radius values.\n\n## Theme selection\n\nThe selection props wrap `svelte-themes`: `themes`, `defaultTheme`, `forcedTheme`,\n`systemTheme`, `syncColorScheme`, `transitionOnChange`, `storageKey`, `attribute`,\n`value`, and `colorScheme`. The default themes are light and dark, with system selection\nenabled. `systemTheme`, `syncColorScheme`, and `transitionOnChange` all default to\n`true`; they map onto the library's `enableSystem`, `enableColorScheme`, and\n`disableTransitionOnChange` options.\n\n`ThemeState` exposes `theme`, `resolvedTheme`, `themes`, and `systemTheme`. Assign\n`theme.theme` to switch themes. The optional `transition` prop applies a named view transition;\nunsupported browsers and reduced-motion users switch instantly.\n\n`spinnerVariant` sets the global default spinner animation. The children snippet is required.\n\n## Motion tokens\n\nMotion is a token scale like spacing and radius: five duration steps and four easing roles.\n`motion` retunes them app-wide; an omitted token keeps its default.\n\n| Duration | Default | | Easing role | Default |\n| ---------- | ------- | --- | ------------ | ------------- |\n| `instant` | 0ms | | `standard` | `cubicInOut` |\n| `fast` | 100ms | | `enter` | `cubicOut` |\n| `normal` | 200ms | | `exit` | `cubicIn` |\n| `slow` | 300ms | | `emphasized` | `backOut` |\n| `slower` | 500ms | | | |\n\n```svelte\n<script lang=\"ts\">\n\timport { Theme } from 'entasis/theme';\n\n\tlet { children } = $props();\n</script>\n\n<Theme motion={{ duration: { normal: 150, slow: 260 }, easing: { standard: 'quintOut' } }}>\n\t{@render children()}\n</Theme>\n```\n\nThe Tailwind plugin emits the same scale as CSS variables on `html` (`--duration-normal`,\n`--ease-standard`, ...) plus the matching `duration-*` / `ease-*` utilities, so CSS transitions\nand Svelte transitions read one set of numbers. The `motion` prop rewrites those variables on\n`html` at runtime and `designTokens.motion` rewrites them again per theme, layered over the prop;\n`ThemeState.motion` resolves through the same two rungs, so the utilities and the presets never\ndisagree. `ThemeState.transition` is a deprecated alias for its `normal` duration and `standard`\neasing. Reduced motion resolves every duration to 0 and collapses the `--duration-*` variables via\nthe `data-entasis-reduce-motion` attribute on `html`.\n\nComponents keep their own transition in a reserved `motion` slot on their theme, so the `theme`\nprop covers motion as well as classes:\n\n```svelte\n<script lang=\"ts\">\n\timport { Dialog } from 'entasis/dialog';\n</script>\n\n<Dialog theme={{ motion: { duration: 'fast', easing: 'emphasized' } }} title=\"Quick\">Body</Dialog>\n```\n\n## Component theme registry\n\n`components` sets app-wide component theme defaults without a wrapper component per component:\nit is keyed by theme name (`dialog`, `button`, ...) and each entry takes the same slots as that\ncomponent's `theme` prop, the `motion` slot included. A `set<Component>Theme` call in a subtree\nbeats the registry, and an instance `theme` prop beats both \u2014 per slot: each rung layers on the one\nbelow it, so a subtree that restyles one slot keeps the registry's others.\n\n```svelte\n<script lang=\"ts\">\n\timport { Theme } from 'entasis/theme';\n\n\tlet { children } = $props();\n</script>\n\n<Theme\n\tcomponents={{\n\t\tdialog: { motion: { duration: 'fast' }, content: { base: 'rounded-2xl' } },\n\t\tbutton: { root: { base: 'tracking-wide' } }\n\t}}\n>\n\t{@render children()}\n</Theme>\n```\n\n## Reduced motion\n\n`reduceMotion` forces reduced motion on (`true`) or off (`false`) for every entasis animation,\noverriding the OS `prefers-reduced-motion` setting; omit it to follow the OS. The live result is\nexposed as `ThemeState.preferReducesMotion` (reactive, so it updates when the OS setting changes)\nand mirrored as a `data-entasis-reduce-motion` attribute on `html` for CSS-only animations.\n\n```svelte\n<script>\n\timport { Theme } from 'entasis/theme';\n\n\tlet { children } = $props();\n</script>\n\n<Theme reduceMotion>{@render children()}</Theme>\n```\n\n## Build-time boundary\n\nThe Tailwind plugin still generates color palettes and registers utility names, variants,\nkeyframes, and spinner CSS. Spacing, radius, typography scale, raised borders,\n`defaultColor` and the four state roles (`focusColor`, `selectedColor`, `hoverColor`,\n`pressedColor`) belong to `Theme.designTokens`; colors remain CSS variables and can be\noverridden directly. `ThemeState.defaultColor` exposes the active role. Kit chrome should\nresolve omitted `color` props with `useDefaultColor`.\n";
|
|
@@ -54,6 +54,7 @@ Changing the controlled object updates already-rendered Tailwind utilities witho
|
|
|
54
54
|
- \`spacingScale\`: partial overrides for the strictly increasing \`xs\`, \`sm\`, \`md\`, \`lg\`,
|
|
55
55
|
and \`xl\` spacing multipliers. Defaults to 1/1.5/2/3/4.
|
|
56
56
|
- \`radius\`: \`'none' | 'subtile' | 'small' | 'normal' | 'large' | 'round' | number\`.
|
|
57
|
+
Controls take the full multiplier; surface steps (\`lg\` and up) stop at \`large\` (1.5×).
|
|
57
58
|
- \`typeScale\`: \`'compact' | 'default' | 'comfortable' | 'large' | TypeScaleOptions\`.
|
|
58
59
|
- \`raisedWithBorder\`: toggles the border used by \`raised-*\` utilities.
|
|
59
60
|
- \`defaultColor\`: \`Colors\` role kit chrome inherits when a control omits \`color\`.
|
|
@@ -361,7 +361,7 @@ export declare const componentInventory: readonly [{
|
|
|
361
361
|
readonly id: "sidebar";
|
|
362
362
|
readonly subpath: "entasis/sidebar";
|
|
363
363
|
readonly sourceIndex: "src/lib/components/Sidebar/index.ts";
|
|
364
|
-
readonly exportedSymbols: readonly ["Sidebar", "SidebarActiveVariant", "SidebarActivityBar", "SidebarActivityBarItem", "SidebarActivityBarSelectPayload", "SidebarApi", "SidebarCollapsible", "SidebarDensity", "SidebarDisplayState", "SidebarFrame", "SidebarGroup", "SidebarIcon", "SidebarIconVariant", "SidebarMenuActionDescriptor", "SidebarMenuAlign", "SidebarMenuButton", "SidebarMenuButtonItem", "SidebarMenuButtonSize", "SidebarMenuButtonVariant", "SidebarMenuEntry", "SidebarMenuSide", "SidebarMenuSubEntry", "SidebarMode", "SidebarProps", "SidebarRail", "SidebarResizable", "SidebarResizableOptions", "SidebarSearch", "SidebarSide", "SidebarSize", "SidebarState", "SidebarTheme", "SidebarThemeProps", "SidebarTooltipMode", "SidebarTreeNode", "SidebarVariant", "SidebarWidthChangePayload", "setSidebarTheme", "sidebarDescription", "sidebarTheme", "useSidebarTheme"];
|
|
364
|
+
readonly exportedSymbols: readonly ["Sidebar", "SidebarActiveVariant", "SidebarActivityBar", "SidebarActivityBarItem", "SidebarActivityBarSelectPayload", "SidebarApi", "SidebarCollapsible", "SidebarDensity", "SidebarDisplayState", "SidebarFrame", "SidebarGroup", "SidebarIcon", "SidebarIconVariant", "SidebarMenuActionDescriptor", "SidebarMenuAlign", "SidebarMenuButton", "SidebarMenuButtonItem", "SidebarMenuButtonSize", "SidebarMenuButtonVariant", "SidebarMenuEntry", "SidebarMenuSide", "SidebarMenuSubEntry", "SidebarMode", "SidebarProps", "SidebarRail", "SidebarResizable", "SidebarResizableOptions", "SidebarSearch", "SidebarSide", "SidebarSize", "SidebarState", "SidebarTheme", "SidebarThemeProps", "SidebarTooltipMode", "SidebarTreeNode", "SidebarVariant", "SidebarView", "SidebarWidthChangePayload", "setSidebarTheme", "sidebarDescription", "sidebarTheme", "useSidebarTheme"];
|
|
365
365
|
readonly docs: readonly [{
|
|
366
366
|
readonly id: "sidebar";
|
|
367
367
|
readonly route: "/components/sidebar";
|