@dorsk/tsumikit 0.1.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.
Files changed (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +165 -0
  3. package/dist/autoresize.d.ts +11 -0
  4. package/dist/autoresize.js +24 -0
  5. package/dist/components/atoms/Badge.svelte +72 -0
  6. package/dist/components/atoms/Badge.svelte.d.ts +12 -0
  7. package/dist/components/atoms/Button.svelte +156 -0
  8. package/dist/components/atoms/Button.svelte.d.ts +13 -0
  9. package/dist/components/atoms/Card.svelte +46 -0
  10. package/dist/components/atoms/Card.svelte.d.ts +11 -0
  11. package/dist/components/atoms/Checkbox.svelte +99 -0
  12. package/dist/components/atoms/Checkbox.svelte.d.ts +10 -0
  13. package/dist/components/atoms/Chip.svelte +53 -0
  14. package/dist/components/atoms/Chip.svelte.d.ts +11 -0
  15. package/dist/components/atoms/Heading.svelte +66 -0
  16. package/dist/components/atoms/Heading.svelte.d.ts +13 -0
  17. package/dist/components/atoms/Icon.svelte +151 -0
  18. package/dist/components/atoms/Icon.svelte.d.ts +18 -0
  19. package/dist/components/atoms/Input.svelte +42 -0
  20. package/dist/components/atoms/Input.svelte.d.ts +10 -0
  21. package/dist/components/atoms/Link.svelte +31 -0
  22. package/dist/components/atoms/Link.svelte.d.ts +10 -0
  23. package/dist/components/atoms/Progress.svelte +59 -0
  24. package/dist/components/atoms/Progress.svelte.d.ts +9 -0
  25. package/dist/components/atoms/Select.svelte +95 -0
  26. package/dist/components/atoms/Select.svelte.d.ts +11 -0
  27. package/dist/components/atoms/Slider.svelte +136 -0
  28. package/dist/components/atoms/Slider.svelte.d.ts +14 -0
  29. package/dist/components/atoms/Switch.svelte +64 -0
  30. package/dist/components/atoms/Switch.svelte.d.ts +8 -0
  31. package/dist/components/atoms/Text.svelte +127 -0
  32. package/dist/components/atoms/Text.svelte.d.ts +16 -0
  33. package/dist/components/atoms/Textarea.svelte +62 -0
  34. package/dist/components/atoms/Textarea.svelte.d.ts +11 -0
  35. package/dist/components/layouts/AppShell.svelte +304 -0
  36. package/dist/components/layouts/AppShell.svelte.d.ts +21 -0
  37. package/dist/components/layouts/AutoGrid.svelte +36 -0
  38. package/dist/components/layouts/AutoGrid.svelte.d.ts +12 -0
  39. package/dist/components/layouts/Cluster.svelte +45 -0
  40. package/dist/components/layouts/Cluster.svelte.d.ts +14 -0
  41. package/dist/components/layouts/Container.svelte +40 -0
  42. package/dist/components/layouts/Container.svelte.d.ts +13 -0
  43. package/dist/components/layouts/NavItem.svelte +95 -0
  44. package/dist/components/layouts/NavItem.svelte.d.ts +14 -0
  45. package/dist/components/layouts/Stack.svelte +44 -0
  46. package/dist/components/layouts/Stack.svelte.d.ts +13 -0
  47. package/dist/components/molecules/Accordion.svelte +94 -0
  48. package/dist/components/molecules/Accordion.svelte.d.ts +16 -0
  49. package/dist/components/molecules/CodeBlock.svelte +119 -0
  50. package/dist/components/molecules/CodeBlock.svelte.d.ts +17 -0
  51. package/dist/components/molecules/CopyButton.svelte +80 -0
  52. package/dist/components/molecules/CopyButton.svelte.d.ts +13 -0
  53. package/dist/components/molecules/Dropzone.svelte +140 -0
  54. package/dist/components/molecules/Dropzone.svelte.d.ts +13 -0
  55. package/dist/components/molecules/Field.svelte +57 -0
  56. package/dist/components/molecules/Field.svelte.d.ts +12 -0
  57. package/dist/components/molecules/FileButton.svelte +68 -0
  58. package/dist/components/molecules/FileButton.svelte.d.ts +14 -0
  59. package/dist/components/molecules/FontScalePicker.svelte +21 -0
  60. package/dist/components/molecules/FontScalePicker.svelte.d.ts +6 -0
  61. package/dist/components/molecules/IconButton.svelte +36 -0
  62. package/dist/components/molecules/IconButton.svelte.d.ts +13 -0
  63. package/dist/components/molecules/Menu.svelte +120 -0
  64. package/dist/components/molecules/Menu.svelte.d.ts +17 -0
  65. package/dist/components/molecules/Modal.svelte +263 -0
  66. package/dist/components/molecules/Modal.svelte.d.ts +13 -0
  67. package/dist/components/molecules/OptionButton.svelte +76 -0
  68. package/dist/components/molecules/OptionButton.svelte.d.ts +10 -0
  69. package/dist/components/molecules/Popover.svelte +125 -0
  70. package/dist/components/molecules/Popover.svelte.d.ts +18 -0
  71. package/dist/components/molecules/RadioGroup.svelte +110 -0
  72. package/dist/components/molecules/RadioGroup.svelte.d.ts +16 -0
  73. package/dist/components/molecules/SelectButton.svelte +52 -0
  74. package/dist/components/molecules/SelectButton.svelte.d.ts +15 -0
  75. package/dist/components/molecules/Tabs.svelte +119 -0
  76. package/dist/components/molecules/Tabs.svelte.d.ts +15 -0
  77. package/dist/components/molecules/ThemePicker.svelte +22 -0
  78. package/dist/components/molecules/ThemePicker.svelte.d.ts +6 -0
  79. package/dist/components/molecules/Toaster.svelte +73 -0
  80. package/dist/components/molecules/Toaster.svelte.d.ts +18 -0
  81. package/dist/components/molecules/Toggle.svelte +68 -0
  82. package/dist/components/molecules/Toggle.svelte.d.ts +11 -0
  83. package/dist/components/molecules/Tooltip.svelte +106 -0
  84. package/dist/components/molecules/Tooltip.svelte.d.ts +10 -0
  85. package/dist/components/organisms/DataTable.svelte +145 -0
  86. package/dist/components/organisms/DataTable.svelte.d.ts +43 -0
  87. package/dist/env.d.ts +1 -0
  88. package/dist/env.js +4 -0
  89. package/dist/index.d.ts +46 -0
  90. package/dist/index.js +56 -0
  91. package/dist/stores/fontscale.svelte.d.ts +15 -0
  92. package/dist/stores/fontscale.svelte.js +49 -0
  93. package/dist/stores/theme.svelte.d.ts +96 -0
  94. package/dist/stores/theme.svelte.js +71 -0
  95. package/dist/stores/toast.svelte.d.ts +19 -0
  96. package/dist/stores/toast.svelte.js +26 -0
  97. package/dist/styles/app.css +522 -0
  98. package/dist/styles/variables.css +651 -0
  99. package/package.json +71 -0
@@ -0,0 +1,304 @@
1
+ <script lang="ts">
2
+ // Application frame: sticky header, optional sidebar, main content, optional
3
+ // footer — all supplied as snippets. Responsive by default:
4
+ // • desktop (≥ 48rem): the sidebar is a persistent grid column.
5
+ // • mobile: the sidebar becomes an off-canvas drawer that slides in over a
6
+ // scrim; AppShell renders the hamburger toggle for you (shown only on
7
+ // mobile), locks body scroll while open, closes on Escape / scrim click,
8
+ // and marks the rest of the page `inert` so focus stays in the drawer.
9
+ // Switching to desktop auto-closes the drawer. Safe-area insets are honored.
10
+ import type { Snippet } from 'svelte';
11
+ import { browser } from '../../env';
12
+ import IconButton from '../molecules/IconButton.svelte';
13
+
14
+ let {
15
+ header,
16
+ sidebar,
17
+ footer,
18
+ children,
19
+ sidebarWidth = '16rem',
20
+ navLabel = 'Main navigation',
21
+ resizableSidebar = false,
22
+ minSidebar = 64,
23
+ maxSidebar = 360,
24
+ sidebarWidthKey
25
+ }: {
26
+ header?: Snippet;
27
+ sidebar?: Snippet;
28
+ footer?: Snippet;
29
+ children: Snippet;
30
+ /** Initial / non-resizable desktop sidebar width (any CSS length). */
31
+ sidebarWidth?: string;
32
+ navLabel?: string;
33
+ /** Drag the sidebar's right edge on desktop to resize it. Below ~8rem the
34
+ * nav collapses to an icon rail automatically (see NavItem, driven by the
35
+ * `sidebar` container query — no JS state needed). */
36
+ resizableSidebar?: boolean;
37
+ minSidebar?: number;
38
+ maxSidebar?: number;
39
+ /** localStorage key to persist the resized width. */
40
+ sidebarWidthKey?: string;
41
+ } = $props();
42
+
43
+ let open = $state(false);
44
+ let isMobile = $state(false);
45
+
46
+ // --- resizable sidebar (desktop) ---
47
+ function loadW(): number | null {
48
+ if (!browser || !sidebarWidthKey) return null;
49
+ const n = Number(localStorage.getItem(sidebarWidthKey));
50
+ return Number.isFinite(n) && n >= minSidebar ? Math.min(n, maxSidebar) : null;
51
+ }
52
+ let sideW = $state<number | null>(loadW());
53
+ let dragging = $state(false);
54
+ let rafId = 0;
55
+ let lastX = 0;
56
+ function startDrag(e: PointerEvent) {
57
+ dragging = true;
58
+ lastX = e.clientX;
59
+ (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
60
+ e.preventDefault();
61
+ }
62
+ function onDrag(e: PointerEvent) {
63
+ if (!dragging) return;
64
+ lastX = e.clientX;
65
+ if (rafId) return;
66
+ rafId = requestAnimationFrame(() => {
67
+ rafId = 0;
68
+ sideW = Math.round(Math.max(minSidebar, Math.min(lastX, maxSidebar)));
69
+ });
70
+ }
71
+ function endDrag(e: PointerEvent) {
72
+ if (!dragging) return;
73
+ dragging = false;
74
+ if (rafId) {
75
+ cancelAnimationFrame(rafId);
76
+ rafId = 0;
77
+ }
78
+ try {
79
+ (e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId);
80
+ } catch {
81
+ /* already released */
82
+ }
83
+ if (browser && sidebarWidthKey && sideW != null) localStorage.setItem(sidebarWidthKey, String(sideW));
84
+ }
85
+ // The effective desktop width: a resized px value wins over the prop default.
86
+ const widthCss = $derived(resizableSidebar && sideW != null ? `${sideW}px` : sidebarWidth);
87
+
88
+ $effect(() => {
89
+ if (!browser) return;
90
+ const mq = window.matchMedia('(max-width: 47.999rem)');
91
+ const sync = () => {
92
+ isMobile = mq.matches;
93
+ if (!isMobile) open = false; // desktop: drawer concept doesn't apply
94
+ };
95
+ sync();
96
+ mq.addEventListener('change', sync);
97
+ return () => mq.removeEventListener('change', sync);
98
+ });
99
+
100
+ // Lock body scroll while the mobile drawer is open.
101
+ $effect(() => {
102
+ if (!browser) return;
103
+ document.body.style.overflow = open && isMobile ? 'hidden' : '';
104
+ return () => {
105
+ document.body.style.overflow = '';
106
+ };
107
+ });
108
+
109
+ // Background is inert (untabbable, hidden from AT) when the drawer is open.
110
+ const bgInert = $derived(open && isMobile);
111
+ </script>
112
+
113
+ <svelte:window onkeydown={(e) => e.key === 'Escape' && (open = false)} />
114
+
115
+ <div class="shell" class:dragging style="--shell-sidebar-w: {widthCss}">
116
+ <header class="shell-header">
117
+ {#if sidebar}
118
+ <IconButton
119
+ class="shell-menu-btn"
120
+ icon="menu"
121
+ label="Toggle navigation"
122
+ aria-expanded={open}
123
+ onclick={() => (open = !open)}
124
+ />
125
+ {/if}
126
+ {@render header?.()}
127
+ </header>
128
+
129
+ {#if sidebar}
130
+ <button
131
+ class="shell-scrim"
132
+ class:show={open && isMobile}
133
+ tabindex="-1"
134
+ aria-hidden="true"
135
+ onclick={() => (open = false)}
136
+ ></button>
137
+ <aside
138
+ class="shell-sidebar"
139
+ class:open
140
+ aria-label={navLabel}
141
+ inert={isMobile && !open ? true : undefined}
142
+ >
143
+ {@render sidebar()}
144
+ {#if resizableSidebar}
145
+ <div
146
+ class="shell-sidebar-resize"
147
+ role="separator"
148
+ aria-label="Resize sidebar"
149
+ aria-orientation="vertical"
150
+ onpointerdown={startDrag}
151
+ onpointermove={onDrag}
152
+ onpointerup={endDrag}
153
+ onpointercancel={endDrag}
154
+ ></div>
155
+ {/if}
156
+ </aside>
157
+ {/if}
158
+
159
+ <main class="shell-main" inert={bgInert || undefined}>
160
+ {@render children()}
161
+ </main>
162
+
163
+ {#if footer}
164
+ <footer class="shell-footer" inert={bgInert || undefined}>{@render footer()}</footer>
165
+ {/if}
166
+ </div>
167
+
168
+ <style>
169
+ .shell {
170
+ min-height: 100dvh;
171
+ display: grid;
172
+ grid-template-columns: 1fr;
173
+ grid-template-rows: auto 1fr auto;
174
+ grid-template-areas:
175
+ 'header'
176
+ 'main'
177
+ 'footer';
178
+ }
179
+ .shell-header {
180
+ grid-area: header;
181
+ position: sticky;
182
+ top: 0;
183
+ z-index: var(--z-header);
184
+ display: flex;
185
+ align-items: center;
186
+ gap: var(--sp-3);
187
+ height: var(--header-h);
188
+ padding-inline: max(var(--sp-4), var(--safe-left)) max(var(--sp-4), var(--safe-right));
189
+ padding-top: var(--safe-top);
190
+ background: color-mix(in srgb, var(--bg) 88%, transparent);
191
+ backdrop-filter: blur(8px);
192
+ border-bottom: 1px solid var(--border);
193
+ }
194
+ .shell-main {
195
+ grid-area: main;
196
+ min-width: 0; /* let content shrink instead of overflowing the grid */
197
+ /* A query container named `main` so content components can respond to the
198
+ content width (which changes when the sidebar is resized), not the
199
+ viewport. */
200
+ container: main / inline-size;
201
+ }
202
+ .shell-footer {
203
+ grid-area: footer;
204
+ border-top: 1px solid var(--border);
205
+ padding: var(--sp-4) max(var(--sp-4), var(--safe-right)) calc(var(--sp-4) + var(--safe-bottom))
206
+ max(var(--sp-4), var(--safe-left));
207
+ }
208
+
209
+ /* Mobile: sidebar is an off-canvas drawer. */
210
+ .shell-sidebar {
211
+ position: fixed;
212
+ top: 0;
213
+ bottom: 0;
214
+ left: 0;
215
+ width: min(var(--shell-sidebar-w), 82vw);
216
+ z-index: var(--z-drawer);
217
+ overflow-y: auto;
218
+ overflow-x: hidden; /* overflow-y:auto would otherwise make x auto too */
219
+ -webkit-overflow-scrolling: touch;
220
+ background: var(--bg-elevated);
221
+ border-right: 1px solid var(--border);
222
+ padding: var(--sp-3);
223
+ padding-top: max(var(--sp-3), var(--safe-top));
224
+ padding-bottom: max(var(--sp-3), var(--safe-bottom));
225
+ /* A query container named `sidebar` so nav items collapse to an icon rail
226
+ based on the sidebar's own width (see NavItem). */
227
+ container: sidebar / inline-size;
228
+ transform: translateX(-100%);
229
+ transition: transform 0.2s var(--ease);
230
+ }
231
+ .shell-sidebar.open {
232
+ transform: translateX(0);
233
+ box-shadow: var(--shadow-lg);
234
+ }
235
+ .shell-scrim {
236
+ display: none;
237
+ position: fixed;
238
+ inset: 0;
239
+ z-index: calc(var(--z-drawer) - 1);
240
+ background: rgba(0, 0, 0, 0.5);
241
+ border: 0;
242
+ cursor: pointer;
243
+ }
244
+ .shell-scrim.show {
245
+ display: block;
246
+ }
247
+
248
+ /* Resize handle lives only on the desktop persistent column. */
249
+ .shell-sidebar-resize {
250
+ display: none;
251
+ }
252
+ .shell.dragging {
253
+ user-select: none;
254
+ cursor: ew-resize;
255
+ }
256
+
257
+ /* Desktop: persistent sidebar column; hide drawer chrome. */
258
+ @media (min-width: 48rem) {
259
+ .shell {
260
+ grid-template-columns: var(--shell-sidebar-w) 1fr;
261
+ grid-template-areas:
262
+ 'header header'
263
+ 'sidebar main'
264
+ 'footer footer';
265
+ }
266
+ .shell-sidebar {
267
+ position: relative; /* anchor the absolute resize handle */
268
+ grid-area: sidebar;
269
+ transform: none;
270
+ z-index: auto;
271
+ box-shadow: none;
272
+ border-right: 1px solid var(--border);
273
+ }
274
+ .shell-scrim,
275
+ :global(.shell-menu-btn) {
276
+ display: none !important;
277
+ }
278
+ .shell-sidebar-resize {
279
+ display: block;
280
+ position: absolute;
281
+ top: 0;
282
+ bottom: 0;
283
+ right: 0;
284
+ width: 10px;
285
+ cursor: ew-resize;
286
+ touch-action: none;
287
+ z-index: 1;
288
+ }
289
+ .shell-sidebar-resize::after {
290
+ content: '';
291
+ position: absolute;
292
+ top: 0;
293
+ bottom: 0;
294
+ right: 0;
295
+ width: 1px;
296
+ background: transparent;
297
+ transition: background 0.12s var(--ease);
298
+ }
299
+ .shell-sidebar-resize:hover::after,
300
+ .shell.dragging .shell-sidebar-resize::after {
301
+ background: var(--accent);
302
+ }
303
+ }
304
+ </style>
@@ -0,0 +1,21 @@
1
+ import type { Snippet } from 'svelte';
2
+ type $$ComponentProps = {
3
+ header?: Snippet;
4
+ sidebar?: Snippet;
5
+ footer?: Snippet;
6
+ children: Snippet;
7
+ /** Initial / non-resizable desktop sidebar width (any CSS length). */
8
+ sidebarWidth?: string;
9
+ navLabel?: string;
10
+ /** Drag the sidebar's right edge on desktop to resize it. Below ~8rem the
11
+ * nav collapses to an icon rail automatically (see NavItem, driven by the
12
+ * `sidebar` container query — no JS state needed). */
13
+ resizableSidebar?: boolean;
14
+ minSidebar?: number;
15
+ maxSidebar?: number;
16
+ /** localStorage key to persist the resized width. */
17
+ sidebarWidthKey?: string;
18
+ };
19
+ declare const AppShell: import("svelte").Component<$$ComponentProps, {}, "">;
20
+ type AppShell = ReturnType<typeof AppShell>;
21
+ export default AppShell;
@@ -0,0 +1,36 @@
1
+ <script lang="ts">
2
+ // Intrinsically responsive grid: columns are at least `min` wide and fill the
3
+ // row, wrapping when they can't — no media or container queries needed, so it
4
+ // adapts to whatever space it's given (including inside a narrow column). This
5
+ // is the "columns just adapt to available space" layout from the AppShell
6
+ // demo, as a named component. `min` is the minimum column width, `gap` the
7
+ // gutter.
8
+ import type { Snippet } from 'svelte';
9
+
10
+ let {
11
+ as = 'div',
12
+ min = '14rem',
13
+ gap = 'var(--sp-4)',
14
+ class: klass = '',
15
+ children,
16
+ ...rest
17
+ }: {
18
+ as?: 'div' | 'section' | 'ul' | 'ol';
19
+ min?: string;
20
+ gap?: string;
21
+ class?: string;
22
+ children?: Snippet;
23
+ [key: string]: unknown;
24
+ } = $props();
25
+ </script>
26
+
27
+ <svelte:element this={as} class="autogrid-c {klass}" style="--ag-min: {min}; gap: {gap}" {...rest}>
28
+ {@render children?.()}
29
+ </svelte:element>
30
+
31
+ <style>
32
+ .autogrid-c {
33
+ display: grid;
34
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--ag-min)), 1fr));
35
+ }
36
+ </style>
@@ -0,0 +1,12 @@
1
+ import type { Snippet } from 'svelte';
2
+ type $$ComponentProps = {
3
+ as?: 'div' | 'section' | 'ul' | 'ol';
4
+ min?: string;
5
+ gap?: string;
6
+ class?: string;
7
+ children?: Snippet;
8
+ [key: string]: unknown;
9
+ };
10
+ declare const AutoGrid: import("svelte").Component<$$ComponentProps, {}, "">;
11
+ type AutoGrid = ReturnType<typeof AutoGrid>;
12
+ export default AutoGrid;
@@ -0,0 +1,45 @@
1
+ <script lang="ts">
2
+ // Horizontal layout: children in a row with a consistent gap, wrapping onto
3
+ // new lines by default (so it never overflows). Ideal for toolbars, tag
4
+ // rows, button groups. `gap`/`align`/`justify` are any CSS value; set
5
+ // `wrap={false}` to keep one line. Polymorphic via `as`.
6
+ import type { Snippet } from 'svelte';
7
+
8
+ let {
9
+ as = 'div',
10
+ gap = 'var(--sp-2)',
11
+ align = 'center',
12
+ justify,
13
+ wrap = true,
14
+ class: klass = '',
15
+ children,
16
+ ...rest
17
+ }: {
18
+ as?: 'div' | 'section' | 'ul' | 'ol' | 'nav' | 'header' | 'footer';
19
+ gap?: string;
20
+ align?: string;
21
+ justify?: string;
22
+ wrap?: boolean;
23
+ class?: string;
24
+ children?: Snippet;
25
+ [key: string]: unknown;
26
+ } = $props();
27
+ </script>
28
+
29
+ <svelte:element
30
+ this={as}
31
+ class="cluster-c {klass}"
32
+ style:gap
33
+ style:align-items={align}
34
+ style:justify-content={justify}
35
+ style:flex-wrap={wrap ? 'wrap' : 'nowrap'}
36
+ {...rest}
37
+ >
38
+ {@render children?.()}
39
+ </svelte:element>
40
+
41
+ <style>
42
+ .cluster-c {
43
+ display: flex;
44
+ }
45
+ </style>
@@ -0,0 +1,14 @@
1
+ import type { Snippet } from 'svelte';
2
+ type $$ComponentProps = {
3
+ as?: 'div' | 'section' | 'ul' | 'ol' | 'nav' | 'header' | 'footer';
4
+ gap?: string;
5
+ align?: string;
6
+ justify?: string;
7
+ wrap?: boolean;
8
+ class?: string;
9
+ children?: Snippet;
10
+ [key: string]: unknown;
11
+ };
12
+ declare const Cluster: import("svelte").Component<$$ComponentProps, {}, "">;
13
+ type Cluster = ReturnType<typeof Cluster>;
14
+ export default Cluster;
@@ -0,0 +1,40 @@
1
+ <script lang="ts">
2
+ // Centered, max-width content column with token gutters that respect safe-area
3
+ // insets. `size` overrides the default --content-max; `pad` toggles vertical
4
+ // padding. Polymorphic via `as` so it can be a <main>, <section>, etc.
5
+ import type { Snippet } from 'svelte';
6
+
7
+ let {
8
+ as = 'div',
9
+ size,
10
+ pad = false,
11
+ class: klass = '',
12
+ children,
13
+ ...rest
14
+ }: {
15
+ as?: 'div' | 'main' | 'section' | 'article';
16
+ /** Max width (any CSS length). Defaults to --content-max. */
17
+ size?: string;
18
+ pad?: boolean;
19
+ class?: string;
20
+ children?: Snippet;
21
+ [key: string]: unknown;
22
+ } = $props();
23
+ </script>
24
+
25
+ <svelte:element
26
+ this={as}
27
+ class="container ct {klass}"
28
+ class:pad
29
+ style={size ? `max-width: ${size}` : undefined}
30
+ {...rest}
31
+ >
32
+ {@render children?.()}
33
+ </svelte:element>
34
+
35
+ <style>
36
+ .ct.pad {
37
+ padding-top: var(--sp-6);
38
+ padding-bottom: var(--sp-12);
39
+ }
40
+ </style>
@@ -0,0 +1,13 @@
1
+ import type { Snippet } from 'svelte';
2
+ type $$ComponentProps = {
3
+ as?: 'div' | 'main' | 'section' | 'article';
4
+ /** Max width (any CSS length). Defaults to --content-max. */
5
+ size?: string;
6
+ pad?: boolean;
7
+ class?: string;
8
+ children?: Snippet;
9
+ [key: string]: unknown;
10
+ };
11
+ declare const Container: import("svelte").Component<$$ComponentProps, {}, "">;
12
+ type Container = ReturnType<typeof Container>;
13
+ export default Container;
@@ -0,0 +1,95 @@
1
+ <script lang="ts">
2
+ // Sidebar navigation item: icon + label. When the surrounding `sidebar`
3
+ // query container gets narrow (≤ 8rem — e.g. the user drags AppShell's
4
+ // resizable sidebar down to a rail) the label hides and the icon centers,
5
+ // purely in CSS. `title`/`aria-label` carry the name so the icon-only state
6
+ // stays accessible and shows a tooltip. Renders an <a> when `href` is set,
7
+ // else a <button>.
8
+ import type { Snippet } from 'svelte';
9
+ import Icon from '../atoms/Icon.svelte';
10
+ import type { IconName } from '../atoms/Icon.svelte';
11
+
12
+ let {
13
+ icon,
14
+ label,
15
+ href,
16
+ active = false,
17
+ onclick,
18
+ children,
19
+ ...rest
20
+ }: {
21
+ icon: IconName;
22
+ label: string;
23
+ href?: string;
24
+ active?: boolean;
25
+ onclick?: (e: MouseEvent) => void;
26
+ children?: Snippet;
27
+ [key: string]: unknown;
28
+ } = $props();
29
+ </script>
30
+
31
+ <svelte:element
32
+ this={href ? 'a' : 'button'}
33
+ {href}
34
+ type={href ? undefined : 'button'}
35
+ class="nav-item"
36
+ class:active
37
+ title={label}
38
+ aria-label={label}
39
+ aria-current={active ? 'page' : undefined}
40
+ {onclick}
41
+ {...rest}
42
+ >
43
+ <Icon name={icon} size={18} />
44
+ <span class="nav-label">{label}</span>
45
+ {#if children}<span class="nav-trail">{@render children()}</span>{/if}
46
+ </svelte:element>
47
+
48
+ <style>
49
+ .nav-item {
50
+ display: flex;
51
+ align-items: center;
52
+ gap: var(--sp-2);
53
+ width: 100%;
54
+ text-align: left;
55
+ padding: var(--sp-2) var(--sp-3);
56
+ border: none;
57
+ background: none;
58
+ color: var(--text-muted);
59
+ border-radius: var(--r-md);
60
+ font-size: var(--fs-sm);
61
+ font-weight: var(--fw-medium);
62
+ text-decoration: none;
63
+ white-space: nowrap;
64
+ transition:
65
+ background 0.12s var(--ease),
66
+ color 0.12s var(--ease);
67
+ }
68
+ .nav-item:hover {
69
+ background: var(--bg-elevated-2);
70
+ color: var(--text);
71
+ text-decoration: none;
72
+ }
73
+ .nav-item.active {
74
+ background: color-mix(in srgb, var(--accent) 14%, transparent);
75
+ color: var(--accent);
76
+ }
77
+ .nav-label {
78
+ flex: 1;
79
+ min-width: 0; /* allow the label to shrink/ellipsis instead of overflowing */
80
+ overflow: hidden;
81
+ text-overflow: ellipsis;
82
+ }
83
+ /* Icon-rail: when the sidebar container is narrow, drop the label + trailing
84
+ slot and center the icon. Driven by the sidebar's width, not the viewport. */
85
+ @container sidebar (max-width: 8rem) {
86
+ .nav-item {
87
+ justify-content: center;
88
+ padding-inline: 0;
89
+ }
90
+ .nav-label,
91
+ .nav-trail {
92
+ display: none;
93
+ }
94
+ }
95
+ </style>
@@ -0,0 +1,14 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { IconName } from '../atoms/Icon.svelte';
3
+ type $$ComponentProps = {
4
+ icon: IconName;
5
+ label: string;
6
+ href?: string;
7
+ active?: boolean;
8
+ onclick?: (e: MouseEvent) => void;
9
+ children?: Snippet;
10
+ [key: string]: unknown;
11
+ };
12
+ declare const NavItem: import("svelte").Component<$$ComponentProps, {}, "">;
13
+ type NavItem = ReturnType<typeof NavItem>;
14
+ export default NavItem;
@@ -0,0 +1,44 @@
1
+ <script lang="ts">
2
+ // Vertical layout: children stacked in a column with a consistent gap.
3
+ // `gap`/`align`/`justify` accept any CSS length/value; `gap` defaults to a
4
+ // spacing token. Polymorphic via `as`. The one-dimensional layout every app
5
+ // rebuilds, in one intention-revealing component.
6
+ import type { Snippet } from 'svelte';
7
+
8
+ let {
9
+ as = 'div',
10
+ gap = 'var(--sp-3)',
11
+ align,
12
+ justify,
13
+ class: klass = '',
14
+ children,
15
+ ...rest
16
+ }: {
17
+ as?: 'div' | 'section' | 'ul' | 'ol' | 'li' | 'form' | 'nav';
18
+ gap?: string;
19
+ align?: string;
20
+ justify?: string;
21
+ class?: string;
22
+ children?: Snippet;
23
+ [key: string]: unknown;
24
+ } = $props();
25
+ </script>
26
+
27
+ <svelte:element
28
+ this={as}
29
+ class="stack-c {klass}"
30
+ style:gap
31
+ style:align-items={align}
32
+ style:justify-content={justify}
33
+ {...rest}
34
+ >
35
+ {@render children?.()}
36
+ </svelte:element>
37
+
38
+ <style>
39
+ .stack-c {
40
+ display: flex;
41
+ flex-direction: column;
42
+ min-width: 0;
43
+ }
44
+ </style>
@@ -0,0 +1,13 @@
1
+ import type { Snippet } from 'svelte';
2
+ type $$ComponentProps = {
3
+ as?: 'div' | 'section' | 'ul' | 'ol' | 'li' | 'form' | 'nav';
4
+ gap?: string;
5
+ align?: string;
6
+ justify?: string;
7
+ class?: string;
8
+ children?: Snippet;
9
+ [key: string]: unknown;
10
+ };
11
+ declare const Stack: import("svelte").Component<$$ComponentProps, {}, "">;
12
+ type Stack = ReturnType<typeof Stack>;
13
+ export default Stack;