@dorsk/tsumikit 0.2.6 → 0.2.8

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.
@@ -3,24 +3,64 @@
3
3
  // background/border/radius/padding from theme tokens. `tap` adds the
4
4
  // interactive hover/active affordance for tappable list items (e.g. session
5
5
  // rows); `as` lets it be a button/anchor when the whole surface is clickable.
6
+ // `padding` dials the inner spacing (none/sm/md/lg) for denser cards.
7
+ //
8
+ // `stacked` fakes a pile of cards by drawing two layers peeking out below
9
+ // (and optionally to the right) via pseudo-elements. `stackTone` tints those
10
+ // back layers with a semantic hue (e.g. `info` for a blue stack); `neutral`
11
+ // keeps them on the plain border colour. `stackY`/`stackX` set the per-layer
12
+ // vertical / horizontal offset in px (vertical spacing stays even across the
13
+ // 3 borders); horizontal defaults to a tiny 2px peek.
6
14
  import type { Snippet } from 'svelte';
7
15
 
16
+ type Tone = 'neutral' | 'ok' | 'warn' | 'danger' | 'info';
17
+
8
18
  let {
9
19
  tap = false,
10
20
  as = 'div',
21
+ padding = 'md',
22
+ stacked = false,
23
+ stackTone = 'neutral',
24
+ stackY = 8,
25
+ stackX = 2,
11
26
  class: klass = '',
27
+ style = '',
12
28
  children,
13
29
  ...rest
14
30
  }: {
15
31
  tap?: boolean;
16
32
  as?: 'div' | 'button' | 'a' | 'li' | 'section' | 'form';
33
+ padding?: 'none' | 'sm' | 'md' | 'lg';
34
+ stacked?: boolean;
35
+ stackTone?: Tone;
36
+ stackY?: number;
37
+ stackX?: number;
17
38
  class?: string;
39
+ style?: string;
18
40
  children?: Snippet;
19
41
  [key: string]: unknown;
20
42
  } = $props();
43
+
44
+ let stackStyle = $derived(
45
+ stacked ? `--stack-y:${stackY}px;--stack-x:${stackX}px;` : ''
46
+ );
21
47
  </script>
22
48
 
23
- <svelte:element this={as} class="card {klass}" class:card-tap={tap} {...rest}>
49
+ <svelte:element
50
+ this={as}
51
+ class="card {klass}"
52
+ class:pad-none={padding === 'none'}
53
+ class:pad-sm={padding === 'sm'}
54
+ class:pad-lg={padding === 'lg'}
55
+ class:card-tap={tap}
56
+ class:card-stacked={stacked}
57
+ class:stack-ok={stacked && stackTone === 'ok'}
58
+ class:stack-warn={stacked && stackTone === 'warn'}
59
+ class:stack-danger={stacked && stackTone === 'danger'}
60
+ class:stack-info={stacked && stackTone === 'info'}
61
+ style={`${stackStyle}${style}`}
62
+ {...rest}
63
+ >
24
64
  {@render children?.()}
25
65
  </svelte:element>
26
66
 
@@ -31,6 +71,15 @@
31
71
  border-radius: var(--r-lg);
32
72
  padding: var(--sp-4);
33
73
  }
74
+ .pad-none {
75
+ padding: 0;
76
+ }
77
+ .pad-sm {
78
+ padding: var(--sp-2);
79
+ }
80
+ .pad-lg {
81
+ padding: var(--sp-6);
82
+ }
34
83
  .card-tap {
35
84
  cursor: pointer;
36
85
  transition:
@@ -43,4 +92,59 @@
43
92
  .card-tap:hover {
44
93
  border-color: var(--border-strong);
45
94
  }
95
+
96
+ /* Stacked effect — two back layers peeking out bottom-right. The front
97
+ surface keeps its own background so the layers only show at the edges. */
98
+ .card-stacked {
99
+ position: relative;
100
+ /* Defaults; overridden inline by the stackY/stackX props. */
101
+ --stack-y: 8px;
102
+ --stack-x: 2px;
103
+ --stack-bg: var(--bg-elevated-2);
104
+ --stack-border: var(--border);
105
+ /* Reserve room for the two peeking layers so they aren't clipped. */
106
+ margin-right: calc(var(--stack-x) * 2);
107
+ margin-bottom: calc(var(--stack-y) * 2);
108
+ }
109
+ .card-stacked::before,
110
+ .card-stacked::after {
111
+ content: '';
112
+ position: absolute;
113
+ inset: 0;
114
+ border-radius: inherit;
115
+ /* Opaque fill so each layer fully hides the one behind it — only the
116
+ bottom-right peek (and its single border line) stays visible. */
117
+ background: var(--stack-bg);
118
+ border: 1px solid var(--stack-border);
119
+ }
120
+ /* Nearest back layer — sits just under the front surface. */
121
+ .card-stacked::before {
122
+ z-index: -1;
123
+ transform: translate(var(--stack-x), var(--stack-y));
124
+ }
125
+ /* Furthest back layer — behind the nearest one. */
126
+ .card-stacked::after {
127
+ z-index: -2;
128
+ transform: translate(
129
+ calc(var(--stack-x) * 2),
130
+ calc(var(--stack-y) * 2)
131
+ );
132
+ }
133
+
134
+ .stack-ok {
135
+ --stack-border: color-mix(in srgb, var(--ok) 45%, transparent);
136
+ --stack-bg: color-mix(in srgb, var(--ok) 12%, var(--bg-elevated));
137
+ }
138
+ .stack-warn {
139
+ --stack-border: color-mix(in srgb, var(--warn) 45%, transparent);
140
+ --stack-bg: color-mix(in srgb, var(--warn) 12%, var(--bg-elevated));
141
+ }
142
+ .stack-danger {
143
+ --stack-border: color-mix(in srgb, var(--danger) 45%, transparent);
144
+ --stack-bg: color-mix(in srgb, var(--danger) 12%, var(--bg-elevated));
145
+ }
146
+ .stack-info {
147
+ --stack-border: color-mix(in srgb, var(--info) 45%, transparent);
148
+ --stack-bg: color-mix(in srgb, var(--info) 12%, var(--bg-elevated));
149
+ }
46
150
  </style>
@@ -1,8 +1,15 @@
1
1
  import type { Snippet } from 'svelte';
2
+ type Tone = 'neutral' | 'ok' | 'warn' | 'danger' | 'info';
2
3
  type $$ComponentProps = {
3
4
  tap?: boolean;
4
5
  as?: 'div' | 'button' | 'a' | 'li' | 'section' | 'form';
6
+ padding?: 'none' | 'sm' | 'md' | 'lg';
7
+ stacked?: boolean;
8
+ stackTone?: Tone;
9
+ stackY?: number;
10
+ stackX?: number;
5
11
  class?: string;
12
+ style?: string;
6
13
  children?: Snippet;
7
14
  [key: string]: unknown;
8
15
  };
@@ -293,14 +293,18 @@
293
293
  touch-action: none;
294
294
  z-index: 1;
295
295
  }
296
+ /* Persistent grip hint (mirrors the Modal): a small pill centered on the
297
+ handle, brightening to the accent on hover / while dragging. */
296
298
  .shell-sidebar-resize::after {
297
299
  content: '';
298
300
  position: absolute;
299
- top: 0;
300
- bottom: 0;
301
- right: 0;
302
- width: 1px;
303
- background: transparent;
301
+ top: 50%;
302
+ right: 1px;
303
+ transform: translateY(-50%);
304
+ width: 3px;
305
+ height: 28px;
306
+ border-radius: 999px;
307
+ background: var(--border-strong);
304
308
  transition: background 0.12s var(--ease);
305
309
  }
306
310
  .shell-sidebar-resize:hover::after,
@@ -7,30 +7,65 @@
7
7
  // gutter. `maxCols` optionally caps how many columns the grid will ever show:
8
8
  // it raises the effective per-column minimum to the width N columns would
9
9
  // need, so auto-fit can never pack more than N across.
10
+ //
11
+ // `max` caps each column's width: instead of growing to fill the row (the
12
+ // default `1fr`), columns top out at `max` and the grid left-packs them
13
+ // (auto-fill + justify-content:start) so a 3-item and a 4-item section both
14
+ // show uniform fixed-width columns rather than stretching to fill. `align`
15
+ // controls cross-axis alignment of items within their row; it defaults to
16
+ // `start` so cards don't stretch to the tallest sibling.
10
17
  import type { Snippet } from 'svelte';
11
18
 
12
19
  let {
13
20
  as = 'div',
14
21
  min = '14rem',
22
+ max,
15
23
  gap = 'var(--sp-4)',
16
24
  maxCols,
25
+ align = 'start',
26
+ justify,
17
27
  class: klass = '',
18
28
  children,
19
29
  ...rest
20
30
  }: {
21
31
  as?: 'div' | 'section' | 'ul' | 'ol';
22
32
  min?: string;
33
+ /** Maximum column width. When set, columns stop growing at this width and
34
+ * the grid left-packs uniform tracks instead of stretching to fill. */
35
+ max?: string;
23
36
  gap?: string;
24
37
  maxCols?: number;
38
+ /** Cross-axis alignment of items in their row (align-items). Defaults to `start`. */
39
+ align?: 'start' | 'center' | 'end' | 'stretch';
40
+ /** Inline (main-axis) distribution of tracks (justify-content). Defaults to
41
+ * `start` when `max` is set, otherwise the grid's natural `stretch`. */
42
+ justify?: 'start' | 'center' | 'end' | 'space-between' | 'space-around' | 'space-evenly' | 'stretch';
25
43
  class?: string;
26
44
  children?: Snippet;
27
45
  [key: string]: unknown;
28
46
  } = $props();
29
47
 
48
+ // Columns grow to fill (`1fr`) by default; when `max` is given they cap there.
49
+ let track = $derived(max != null ? max : '1fr');
50
+ // auto-fill keeps capped tracks from stretching to fill the row; auto-fit
51
+ // (the default) collapses empty tracks so columns expand to use the space.
52
+ let mode = $derived(max != null ? 'auto-fill' : 'auto-fit');
53
+ // Left-pack capped grids by default so columns don't drift to fill the row.
54
+ let justifyValue = $derived(justify ?? (max != null ? 'start' : null));
55
+
30
56
  let style = $derived(
31
- maxCols != null
32
- ? `--ag-min: ${min}; --ag-gap: ${gap}; --ag-cols: ${maxCols}; gap: ${gap}`
33
- : `--ag-min: ${min}; gap: ${gap}`
57
+ [
58
+ `--ag-min: ${min}`,
59
+ `--ag-track: ${track}`,
60
+ `--ag-mode: ${mode}`,
61
+ maxCols != null ? `--ag-gap: ${gap}` : null,
62
+ maxCols != null ? `--ag-cols: ${maxCols}` : null,
63
+ `gap: ${gap}`,
64
+ `align-items: ${align}`,
65
+ justifyValue ? `justify-content: ${justifyValue}` : null
66
+ ]
67
+ .filter(Boolean)
68
+ .join('; ')
34
69
  );
35
70
  </script>
36
71
 
@@ -47,7 +82,7 @@
47
82
  <style>
48
83
  .autogrid-c {
49
84
  display: grid;
50
- grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--ag-min)), 1fr));
85
+ grid-template-columns: repeat(var(--ag-mode), minmax(min(100%, var(--ag-min)), var(--ag-track)));
51
86
  }
52
87
 
53
88
  /* Cap at N columns: the per-column floor becomes the larger of `min` and the
@@ -56,10 +91,10 @@
56
91
  overflowing when the container is narrower than a single `min` track. */
57
92
  .autogrid-c.capped {
58
93
  grid-template-columns: repeat(
59
- auto-fit,
94
+ var(--ag-mode),
60
95
  minmax(
61
96
  min(100%, max(var(--ag-min), (100% - (var(--ag-cols) - 1) * var(--ag-gap)) / var(--ag-cols))),
62
- 1fr
97
+ var(--ag-track)
63
98
  )
64
99
  );
65
100
  }
@@ -2,8 +2,16 @@ import type { Snippet } from 'svelte';
2
2
  type $$ComponentProps = {
3
3
  as?: 'div' | 'section' | 'ul' | 'ol';
4
4
  min?: string;
5
+ /** Maximum column width. When set, columns stop growing at this width and
6
+ * the grid left-packs uniform tracks instead of stretching to fill. */
7
+ max?: string;
5
8
  gap?: string;
6
9
  maxCols?: number;
10
+ /** Cross-axis alignment of items in their row (align-items). Defaults to `start`. */
11
+ align?: 'start' | 'center' | 'end' | 'stretch';
12
+ /** Inline (main-axis) distribution of tracks (justify-content). Defaults to
13
+ * `start` when `max` is set, otherwise the grid's natural `stretch`. */
14
+ justify?: 'start' | 'center' | 'end' | 'space-between' | 'space-around' | 'space-evenly' | 'stretch';
7
15
  class?: string;
8
16
  children?: Snippet;
9
17
  [key: string]: unknown;
@@ -1,21 +1,27 @@
1
1
  <script lang="ts">
2
2
  // Centered, max-width content column with token gutters that respect safe-area
3
3
  // insets. `size` overrides the default --content-max; `pad` toggles vertical
4
- // padding. Polymorphic via `as` so it can be a <main>, <section>, etc.
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.
5
8
  import type { Snippet } from 'svelte';
6
9
 
7
10
  let {
8
11
  as = 'div',
9
12
  size,
10
13
  pad = false,
14
+ fullWidth = false,
11
15
  class: klass = '',
12
16
  children,
13
17
  ...rest
14
18
  }: {
15
19
  as?: 'div' | 'main' | 'section' | 'article';
16
- /** Max width (any CSS length). Defaults to --content-max. */
20
+ /** Max width (any CSS length). Defaults to --content-max. Ignored when `fullWidth`. */
17
21
  size?: string;
18
22
  pad?: boolean;
23
+ /** Break out to the full viewport width, ignoring `size`/--content-max. */
24
+ fullWidth?: boolean;
19
25
  class?: string;
20
26
  children?: Snippet;
21
27
  [key: string]: unknown;
@@ -26,7 +32,8 @@
26
32
  this={as}
27
33
  class="container ct {klass}"
28
34
  class:pad
29
- style={size ? `max-width: ${size}` : undefined}
35
+ class:full={fullWidth}
36
+ style={!fullWidth && size ? `max-width: ${size}` : undefined}
30
37
  {...rest}
31
38
  >
32
39
  {@render children?.()}
@@ -37,4 +44,13 @@
37
44
  padding-top: var(--sp-6);
38
45
  padding-bottom: var(--sp-12);
39
46
  }
47
+
48
+ /* Break out of any centered ancestor to span the full viewport width.
49
+ `margin-inline: calc(50% - 50vw)` pulls each edge out to the viewport,
50
+ keeping the element in normal flow (no transform/overflow side-effects). */
51
+ .ct.full {
52
+ max-width: none;
53
+ width: 100vw;
54
+ margin-inline: calc(50% - 50vw);
55
+ }
40
56
  </style>
@@ -1,9 +1,11 @@
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. */
4
+ /** Max width (any CSS length). Defaults to --content-max. Ignored when `fullWidth`. */
5
5
  size?: string;
6
6
  pad?: boolean;
7
+ /** Break out to the full viewport width, ignoring `size`/--content-max. */
8
+ fullWidth?: boolean;
7
9
  class?: string;
8
10
  children?: Snippet;
9
11
  [key: string]: unknown;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dorsk/tsumikit",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
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",