@dorsk/tsumikit 0.21.0 → 0.22.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 CHANGED
@@ -83,11 +83,26 @@ Dropzone, CodeBlock, Callout, EmptyState, Toaster, ThemePicker, FontScalePicker.
83
83
  **Organisms:** DataTable (generic `<T>`, typed columns + cell snippets).
84
84
 
85
85
  **Layouts:** AppShell (responsive header/sidebar/main/footer — persistent
86
- sidebar on desktop, overlay drawer on mobile, optionally resizable), NavItem
86
+ sidebar on desktop, overlay drawer on mobile, optionally resizable;
87
+ `layout="sidebar-full"` runs the sidebar the full height with the header over
88
+ the content column only, `stickySidebar` pins it to the viewport, and
89
+ `sidebarPadding="none" | "sm" | "md"` sets the aside gutter — the header and its
90
+ children are `min-width: 0` so a wide title/actions row can't widen the grid on
91
+ mobile), NavItem
87
92
  (collapses to an icon rail when the sidebar is narrow), Container, Stack
88
93
  (vertical), Cluster (wrapping row), AutoGrid (intrinsically responsive columns —
89
94
  no media/container query needed).
90
95
 
96
+ ### Container
97
+
98
+ `size` caps the column (`--content-max` by default; `size="none"` fills the parent),
99
+ `gutter` overrides the `--sp-4` inline padding (safe-area insets still win),
100
+ `align="start"` drops the centering. `fullWidth` is a *viewport* breakout for
101
+ edge-to-edge sections — inside AppShell's main column use `size="none"` instead;
102
+ `inset="<left> <right>"` keeps a `fullWidth` container clear of docked panels.
103
+ Children can bleed to the column edge with
104
+ `margin-inline: calc(-1 * var(--container-gutter))`.
105
+
91
106
  ### Stacked distribution + legend
92
107
 
93
108
  `SegmentedProgress mode="stacked"` turns the bar into one shared track whose slice
@@ -24,7 +24,10 @@
24
24
  resizableSidebar = false,
25
25
  minSidebar = 64,
26
26
  maxSidebar = 360,
27
- sidebarWidthKey
27
+ sidebarWidthKey,
28
+ layout = 'header-top',
29
+ stickySidebar = false,
30
+ sidebarPadding = 'md'
28
31
  }: {
29
32
  header?: Snippet;
30
33
  sidebar?: Snippet;
@@ -41,6 +44,14 @@
41
44
  maxSidebar?: number;
42
45
  /** localStorage key to persist the resized width. */
43
46
  sidebarWidthKey?: string;
47
+ /** `'sidebar-full'`: the sidebar spans the whole height on desktop/tablet
48
+ * and the header only covers the content column (brand sits top-left). */
49
+ layout?: 'header-top' | 'sidebar-full';
50
+ /** Pin the desktop sidebar to the viewport (100dvh, own scroll) so its
51
+ * footer stays visible on long pages. */
52
+ stickySidebar?: boolean;
53
+ /** Inner padding of the sidebar; `'none'` when the nav owns its gutters. */
54
+ sidebarPadding?: 'none' | 'sm' | 'md';
44
55
  } = $props();
45
56
 
46
57
  let open = $state(false);
@@ -115,7 +126,14 @@
115
126
 
116
127
  <svelte:window onkeydown={(e) => e.key === 'Escape' && (open = false)} />
117
128
 
118
- <div class="shell" class:dragging style="--shell-sidebar-w: {widthCss}" data-tsu="AppShell">
129
+ <div
130
+ class="shell"
131
+ class:dragging
132
+ class:sidebar-full={layout === 'sidebar-full'}
133
+ class:sticky-sidebar={stickySidebar}
134
+ style="--shell-sidebar-w: {widthCss}"
135
+ data-tsu="AppShell"
136
+ >
119
137
  <header class="shell-header">
120
138
  {#if sidebar}
121
139
  <!-- Wrapper owned here so the responsive hide is a scoped rule on our own
@@ -143,6 +161,7 @@
143
161
  <aside
144
162
  class="shell-sidebar"
145
163
  class:open
164
+ data-padding={sidebarPadding}
146
165
  aria-label={navLabel}
147
166
  inert={isMobile && !open ? true : undefined}
148
167
  >
@@ -190,6 +209,7 @@
190
209
  display: flex;
191
210
  align-items: center;
192
211
  gap: var(--sp-3);
212
+ min-width: 0;
193
213
  height: var(--header-h);
194
214
  padding-inline: max(var(--sp-4), var(--safe-left)) max(var(--sp-4), var(--safe-right));
195
215
  padding-top: var(--safe-top);
@@ -197,6 +217,9 @@
197
217
  backdrop-filter: blur(8px);
198
218
  border-bottom: 1px solid var(--border);
199
219
  }
220
+ .shell-header > :global(*) {
221
+ min-width: 0;
222
+ }
200
223
  .shell-menu-btn {
201
224
  display: inline-flex;
202
225
  align-items: center;
@@ -229,15 +252,22 @@
229
252
  -webkit-overflow-scrolling: touch;
230
253
  background: var(--bg-elevated);
231
254
  border-right: 1px solid var(--border);
232
- padding: var(--sp-3);
233
- padding-top: max(var(--sp-3), var(--safe-top));
234
- padding-bottom: max(var(--sp-3), var(--safe-bottom));
255
+ --shell-sidebar-pad: var(--sp-3);
256
+ padding: var(--shell-sidebar-pad);
257
+ padding-top: max(var(--shell-sidebar-pad), var(--safe-top));
258
+ padding-bottom: max(var(--shell-sidebar-pad), var(--safe-bottom));
235
259
  /* A query container named `sidebar` so nav items collapse to an icon rail
236
260
  based on the sidebar's own width (see NavItem). */
237
261
  container: sidebar / inline-size;
238
262
  transform: translateX(-100%);
239
263
  transition: transform 0.2s var(--ease);
240
264
  }
265
+ .shell-sidebar[data-padding='sm'] {
266
+ --shell-sidebar-pad: var(--sp-2);
267
+ }
268
+ .shell-sidebar[data-padding='none'] {
269
+ --shell-sidebar-pad: 0px;
270
+ }
241
271
  .shell-sidebar.open {
242
272
  transform: translateX(0);
243
273
  box-shadow: var(--shadow-lg);
@@ -273,6 +303,12 @@
273
303
  'sidebar main'
274
304
  'footer footer';
275
305
  }
306
+ .shell.sidebar-full {
307
+ grid-template-areas:
308
+ 'sidebar header'
309
+ 'sidebar main'
310
+ 'sidebar footer';
311
+ }
276
312
  .shell-sidebar {
277
313
  position: relative; /* anchor the absolute resize handle */
278
314
  grid-area: sidebar;
@@ -281,6 +317,12 @@
281
317
  box-shadow: none;
282
318
  border-right: 1px solid var(--border);
283
319
  }
320
+ .shell.sticky-sidebar .shell-sidebar {
321
+ position: sticky;
322
+ top: 0;
323
+ height: 100dvh;
324
+ align-self: start;
325
+ }
284
326
  .shell-scrim,
285
327
  .shell-menu-btn {
286
328
  display: none !important;
@@ -15,6 +15,14 @@ type $$ComponentProps = {
15
15
  maxSidebar?: number;
16
16
  /** localStorage key to persist the resized width. */
17
17
  sidebarWidthKey?: string;
18
+ /** `'sidebar-full'`: the sidebar spans the whole height on desktop/tablet
19
+ * and the header only covers the content column (brand sits top-left). */
20
+ layout?: 'header-top' | 'sidebar-full';
21
+ /** Pin the desktop sidebar to the viewport (100dvh, own scroll) so its
22
+ * footer stays visible on long pages. */
23
+ stickySidebar?: boolean;
24
+ /** Inner padding of the sidebar; `'none'` when the nav owns its gutters. */
25
+ sidebarPadding?: 'none' | 'sm' | 'md';
18
26
  };
19
27
  declare const AppShell: import("svelte").Component<$$ComponentProps, {}, "">;
20
28
  type AppShell = ReturnType<typeof AppShell>;
@@ -1,31 +1,51 @@
1
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. `fullWidth` releases the max-width constraint and lets the content
5
- // bleed to the full viewport width even when nested inside a centered ancestor
6
- // (the `margin-inline: calc(50% - 50vw)` trick), for edge-to-edge sections.
7
- // Polymorphic via `as` so it can be a <main>, <section>, etc.
2
+ // Max-width content column with token gutters that respect safe-area insets.
3
+ // Self-contained: does not depend on the global `.container` utility. Exposes
4
+ // `--container-gutter` so children can bleed to the column edge with
5
+ // `margin-inline: calc(-1 * var(--container-gutter))`.
8
6
  import type { Snippet } from 'svelte';
9
7
 
10
8
  let {
11
9
  as = 'div',
12
10
  size,
11
+ gutter,
12
+ align = 'center',
13
13
  pad = false,
14
14
  fullWidth = false,
15
+ inset,
15
16
  class: klass = '',
16
17
  children,
17
18
  ...rest
18
19
  }: {
19
20
  as?: 'div' | 'main' | 'section' | 'article';
20
- /** Max width (any CSS length). Defaults to --content-max. Ignored when `fullWidth`. */
21
- size?: string;
21
+ /** Max width (any CSS length), or `'none'` to fill the parent. Defaults to --content-max. Ignored when `fullWidth`. */
22
+ size?: string | 'none';
23
+ /** Inline gutter (any CSS length). Defaults to --sp-4; safe-area insets still win when larger. */
24
+ gutter?: string;
25
+ /** `'center'` (margin-inline auto) or `'start'` (flush with the parent's start edge). */
26
+ align?: 'center' | 'start';
22
27
  pad?: boolean;
23
- /** Break out to the full viewport width, ignoring `size`/--content-max. */
28
+ /** Break out to the full *viewport* width, ignoring `size`/--content-max and any
29
+ * centered ancestor. Inside a sidebar layout prefer `size="none"` (fills the column). */
24
30
  fullWidth?: boolean;
31
+ /** Space reserved at the viewport edges when `fullWidth` (`'left right'` or one value
32
+ * for both), e.g. `"var(--dock-left-w) var(--dock-right-w)"` for docked panels. */
33
+ inset?: string;
25
34
  class?: string;
26
35
  children?: Snippet;
27
36
  [key: string]: unknown;
28
37
  } = $props();
38
+
39
+ const style = $derived.by(() => {
40
+ const vars: string[] = [];
41
+ if (!fullWidth && size && size !== 'none') vars.push(`--ct-max: ${size}`);
42
+ if (gutter) vars.push(`--container-gutter: ${gutter}`);
43
+ if (fullWidth && inset) {
44
+ const [left, right = left] = inset.trim().split(/\s+/);
45
+ vars.push(`--ct-inset-l: ${left}`, `--ct-inset-r: ${right}`);
46
+ }
47
+ return vars.length ? vars.join('; ') : undefined;
48
+ });
29
49
  </script>
30
50
 
31
51
  <svelte:element
@@ -33,25 +53,46 @@
33
53
  data-tsu="Container"
34
54
  class="container ct {klass}"
35
55
  class:pad
56
+ class:none={size === 'none'}
57
+ class:start={align === 'start'}
36
58
  class:full={fullWidth}
37
- style={!fullWidth && size ? `max-width: ${size}` : undefined}
59
+ {style}
38
60
  {...rest}
39
61
  >
40
62
  {@render children?.()}
41
63
  </svelte:element>
42
64
 
43
65
  <style>
66
+ .ct {
67
+ --container-gutter: var(--sp-4);
68
+ width: 100%;
69
+ max-width: var(--ct-max, var(--content-max));
70
+ margin-inline: auto;
71
+ padding-inline: max(var(--container-gutter), var(--safe-left))
72
+ max(var(--container-gutter), var(--safe-right));
73
+ }
74
+
75
+ .ct.none {
76
+ max-width: none;
77
+ }
78
+
79
+ .ct.start {
80
+ margin-inline: 0;
81
+ }
82
+
44
83
  .ct.pad {
45
84
  padding-top: var(--sp-6);
46
85
  padding-bottom: var(--sp-12);
47
86
  }
48
87
 
49
- /* Break out of any centered ancestor to span the full viewport width.
88
+ /* Break out of any centered ancestor to span the viewport (minus `inset`).
50
89
  `margin-inline: calc(50% - 50vw)` pulls each edge out to the viewport,
51
90
  keeping the element in normal flow (no transform/overflow side-effects). */
52
91
  .ct.full {
92
+ --ct-inset-l: 0px;
93
+ --ct-inset-r: 0px;
53
94
  max-width: none;
54
- width: 100vw;
55
- margin-inline: calc(50% - 50vw);
95
+ width: calc(100vw - var(--ct-inset-l) - var(--ct-inset-r));
96
+ margin-inline: calc(50% - 50vw + var(--ct-inset-l)) calc(50% - 50vw + var(--ct-inset-r));
56
97
  }
57
98
  </style>
@@ -1,11 +1,19 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  type $$ComponentProps = {
3
3
  as?: 'div' | 'main' | 'section' | 'article';
4
- /** Max width (any CSS length). Defaults to --content-max. Ignored when `fullWidth`. */
5
- size?: string;
4
+ /** Max width (any CSS length), or `'none'` to fill the parent. Defaults to --content-max. Ignored when `fullWidth`. */
5
+ size?: string | 'none';
6
+ /** Inline gutter (any CSS length). Defaults to --sp-4; safe-area insets still win when larger. */
7
+ gutter?: string;
8
+ /** `'center'` (margin-inline auto) or `'start'` (flush with the parent's start edge). */
9
+ align?: 'center' | 'start';
6
10
  pad?: boolean;
7
- /** Break out to the full viewport width, ignoring `size`/--content-max. */
11
+ /** Break out to the full *viewport* width, ignoring `size`/--content-max and any
12
+ * centered ancestor. Inside a sidebar layout prefer `size="none"` (fills the column). */
8
13
  fullWidth?: boolean;
14
+ /** Space reserved at the viewport edges when `fullWidth` (`'left right'` or one value
15
+ * for both), e.g. `"var(--dock-left-w) var(--dock-right-w)"` for docked panels. */
16
+ inset?: string;
9
17
  class?: string;
10
18
  children?: Snippet;
11
19
  [key: string]: unknown;
@@ -106,9 +106,7 @@ pre {
106
106
  width: 100%;
107
107
  max-width: var(--content-max);
108
108
  margin-inline: auto;
109
- padding-inline: var(--sp-4);
110
- padding-left: max(var(--sp-4), var(--safe-left));
111
- padding-right: max(var(--sp-4), var(--safe-right));
109
+ padding-inline: max(var(--sp-4), var(--safe-left)) max(var(--sp-4), var(--safe-right));
112
110
  }
113
111
  .stack {
114
112
  display: flex;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dorsk/tsumikit",
3
- "version": "0.21.0",
3
+ "version": "0.22.0",
4
4
  "description": "Minimal, dependency-free Svelte 5 + pure-CSS UI kit. Token-driven atoms, molecules & layouts with theming out of the box.",
5
5
  "type": "module",
6
6
  "license": "MIT",