@dorsk/tsumikit 0.38.1 → 0.40.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 (145) hide show
  1. package/dist/autoresize.d.ts +2 -0
  2. package/dist/autoresize.js +15 -1
  3. package/dist/components/atoms/Artwork.svelte +19 -18
  4. package/dist/components/atoms/Artwork.svelte.d.ts +3 -2
  5. package/dist/components/atoms/Badge.svelte +60 -26
  6. package/dist/components/atoms/Badge.svelte.d.ts +14 -3
  7. package/dist/components/atoms/Button.svelte +30 -12
  8. package/dist/components/atoms/Button.svelte.d.ts +4 -2
  9. package/dist/components/atoms/Card.svelte +47 -30
  10. package/dist/components/atoms/Card.svelte.d.ts +12 -3
  11. package/dist/components/atoms/Checkbox.svelte +16 -2
  12. package/dist/components/atoms/Checkbox.svelte.d.ts +2 -0
  13. package/dist/components/atoms/Dot.svelte +17 -11
  14. package/dist/components/atoms/Dot.svelte.d.ts +5 -2
  15. package/dist/components/atoms/Gauge.svelte +19 -18
  16. package/dist/components/atoms/Gauge.svelte.d.ts +3 -2
  17. package/dist/components/atoms/Heading.svelte +20 -19
  18. package/dist/components/atoms/Heading.svelte.d.ts +3 -2
  19. package/dist/components/atoms/Icon.svelte +36 -13
  20. package/dist/components/atoms/Icon.svelte.d.ts +20 -2
  21. package/dist/components/atoms/Input.svelte +17 -3
  22. package/dist/components/atoms/Input.svelte.d.ts +2 -1
  23. package/dist/components/atoms/Kbd.svelte +25 -3
  24. package/dist/components/atoms/Kbd.svelte.d.ts +5 -1
  25. package/dist/components/atoms/Link.svelte +16 -12
  26. package/dist/components/atoms/Link.svelte.d.ts +6 -2
  27. package/dist/components/atoms/Progress.svelte +119 -4
  28. package/dist/components/atoms/Progress.svelte.d.ts +28 -2
  29. package/dist/components/atoms/Scrim.svelte +7 -2
  30. package/dist/components/atoms/Scrim.svelte.d.ts +2 -0
  31. package/dist/components/atoms/SegmentedProgress.svelte +14 -2
  32. package/dist/components/atoms/SegmentedProgress.svelte.d.ts +5 -2
  33. package/dist/components/atoms/Select.svelte +18 -6
  34. package/dist/components/atoms/Select.svelte.d.ts +3 -3
  35. package/dist/components/atoms/Skeleton.svelte +12 -11
  36. package/dist/components/atoms/Skeleton.svelte.d.ts +3 -2
  37. package/dist/components/atoms/Slider.svelte +45 -3
  38. package/dist/components/atoms/Slider.svelte.d.ts +7 -0
  39. package/dist/components/atoms/Spinner.svelte +2 -0
  40. package/dist/components/atoms/Spinner.svelte.d.ts +2 -0
  41. package/dist/components/atoms/Switch.svelte +70 -18
  42. package/dist/components/atoms/Switch.svelte.d.ts +5 -1
  43. package/dist/components/atoms/Text.svelte +39 -26
  44. package/dist/components/atoms/Text.svelte.d.ts +7 -3
  45. package/dist/components/atoms/Textarea.svelte +47 -10
  46. package/dist/components/atoms/Textarea.svelte.d.ts +10 -3
  47. package/dist/components/layouts/AppShell.svelte +7 -3
  48. package/dist/components/layouts/AppShell.svelte.d.ts +2 -0
  49. package/dist/components/layouts/AutoGrid.svelte +8 -0
  50. package/dist/components/layouts/AutoGrid.svelte.d.ts +2 -0
  51. package/dist/components/layouts/Cluster.svelte +62 -1
  52. package/dist/components/layouts/Cluster.svelte.d.ts +9 -0
  53. package/dist/components/layouts/NavItem.svelte +22 -1
  54. package/dist/components/layouts/NavItem.svelte.d.ts +5 -0
  55. package/dist/components/layouts/NavSection.svelte +4 -1
  56. package/dist/components/layouts/NavSection.svelte.d.ts +2 -0
  57. package/dist/components/layouts/ResizablePanel.svelte +14 -3
  58. package/dist/components/layouts/ResizablePanel.svelte.d.ts +5 -0
  59. package/dist/components/layouts/Stack.svelte +41 -0
  60. package/dist/components/layouts/Stack.svelte.d.ts +9 -0
  61. package/dist/components/layouts/Toolbar.svelte +122 -0
  62. package/dist/components/layouts/Toolbar.svelte.d.ts +22 -0
  63. package/dist/components/molecules/AttachmentList.svelte +59 -0
  64. package/dist/components/molecules/AttachmentList.svelte.d.ts +13 -0
  65. package/dist/components/molecules/Breadcrumb.svelte +6 -2
  66. package/dist/components/molecules/Breadcrumb.svelte.d.ts +2 -0
  67. package/dist/components/molecules/Callout.svelte +10 -4
  68. package/dist/components/molecules/Callout.svelte.d.ts +1 -1
  69. package/dist/components/molecules/CapBar.svelte +5 -1
  70. package/dist/components/molecules/ChatBubble.svelte +158 -0
  71. package/dist/components/molecules/ChatBubble.svelte.d.ts +26 -0
  72. package/dist/components/molecules/CodeBlock.svelte +36 -3
  73. package/dist/components/molecules/CodeBlock.svelte.d.ts +8 -0
  74. package/dist/components/molecules/Composer.svelte +195 -0
  75. package/dist/components/molecules/Composer.svelte.d.ts +27 -0
  76. package/dist/components/molecules/ConfirmModal.svelte +15 -5
  77. package/dist/components/molecules/ConfirmModal.svelte.d.ts +5 -1
  78. package/dist/components/molecules/Drawer.svelte +9 -6
  79. package/dist/components/molecules/Dropzone.svelte +9 -3
  80. package/dist/components/molecules/Dropzone.svelte.d.ts +2 -0
  81. package/dist/components/molecules/EmptyState.svelte +9 -5
  82. package/dist/components/molecules/EmptyState.svelte.d.ts +3 -1
  83. package/dist/components/molecules/Field.svelte +7 -1
  84. package/dist/components/molecules/Field.svelte.d.ts +2 -0
  85. package/dist/components/molecules/FileButton.svelte +2 -1
  86. package/dist/components/molecules/FileButton.svelte.d.ts +2 -1
  87. package/dist/components/molecules/FilterInput.svelte +13 -3
  88. package/dist/components/molecules/FilterInput.svelte.d.ts +4 -1
  89. package/dist/components/molecules/FontScalePicker.svelte +8 -1
  90. package/dist/components/molecules/IconButton.svelte +32 -7
  91. package/dist/components/molecules/IconButton.svelte.d.ts +9 -1
  92. package/dist/components/molecules/KeyValue.svelte +4 -1
  93. package/dist/components/molecules/KeyValue.svelte.d.ts +2 -1
  94. package/dist/components/molecules/Menu.svelte +50 -5
  95. package/dist/components/molecules/Menu.svelte.d.ts +10 -1
  96. package/dist/components/molecules/Meter.svelte +91 -0
  97. package/dist/components/molecules/Meter.svelte.d.ts +29 -0
  98. package/dist/components/molecules/Metric.svelte +134 -8
  99. package/dist/components/molecules/Metric.svelte.d.ts +20 -2
  100. package/dist/components/molecules/Modal.svelte +26 -12
  101. package/dist/components/molecules/Modal.svelte.d.ts +7 -1
  102. package/dist/components/molecules/Pagination.svelte +25 -1
  103. package/dist/components/molecules/Pagination.svelte.d.ts +9 -2
  104. package/dist/components/molecules/Popover.svelte +16 -5
  105. package/dist/components/molecules/Popover.svelte.d.ts +5 -0
  106. package/dist/components/molecules/Prose.svelte +140 -0
  107. package/dist/components/molecules/Prose.svelte.d.ts +13 -0
  108. package/dist/components/molecules/RadioGroup.svelte +42 -2
  109. package/dist/components/molecules/RadioGroup.svelte.d.ts +4 -1
  110. package/dist/components/molecules/SectionHeader.svelte +5 -2
  111. package/dist/components/molecules/SectionHeader.svelte.d.ts +1 -1
  112. package/dist/components/molecules/SegmentedControl.svelte +36 -1
  113. package/dist/components/molecules/SegmentedControl.svelte.d.ts +11 -1
  114. package/dist/components/molecules/SelectButton.svelte +64 -4
  115. package/dist/components/molecules/SelectButton.svelte.d.ts +18 -1
  116. package/dist/components/molecules/Tabs.svelte +21 -3
  117. package/dist/components/molecules/Tabs.svelte.d.ts +6 -0
  118. package/dist/components/molecules/Timestamp.svelte +13 -6
  119. package/dist/components/molecules/Timestamp.svelte.d.ts +4 -0
  120. package/dist/components/molecules/Toaster.svelte +54 -38
  121. package/dist/components/molecules/Toaster.svelte.d.ts +5 -1
  122. package/dist/components/molecules/Toggle.svelte +2 -1
  123. package/dist/components/molecules/Toggle.svelte.d.ts +2 -1
  124. package/dist/components/molecules/Tooltip.svelte +32 -5
  125. package/dist/components/molecules/Tooltip.svelte.d.ts +9 -0
  126. package/dist/components/molecules/Truncate.svelte +156 -18
  127. package/dist/components/molecules/Truncate.svelte.d.ts +17 -1
  128. package/dist/components/molecules/WorkingDir.svelte +6 -0
  129. package/dist/components/molecules/WorkingDir.svelte.d.ts +2 -0
  130. package/dist/components/organisms/DataTable.svelte +38 -10
  131. package/dist/components/organisms/DataTable.svelte.d.ts +4 -0
  132. package/dist/components/organisms/FilterSearchBar.svelte +9 -2
  133. package/dist/components/organisms/FilterSearchBar.svelte.d.ts +4 -1
  134. package/dist/index.d.ts +10 -2
  135. package/dist/index.js +8 -1
  136. package/dist/size.d.ts +2 -0
  137. package/dist/size.js +1 -0
  138. package/dist/styles/tokens.css +3 -0
  139. package/dist/timestamp.d.ts +2 -2
  140. package/dist/timestamp.js +4 -4
  141. package/dist/tone.d.ts +1 -1
  142. package/dist/tone.js +1 -1
  143. package/dist/truncate.d.ts +13 -1
  144. package/dist/truncate.js +39 -0
  145. package/package.json +5 -3
@@ -1,14 +1,18 @@
1
1
  <script lang="ts">
2
+ import type { ControlSize } from '../../size';
2
3
  // Canonical on/off toggle (role="switch") — a pill track with a sliding knob.
3
- // Owns its sizing/colors from theme tokens; the parent supplies `checked`
4
- // and an `onclick` handler plus an accessible label/title.
4
+ // `checked` is bindable (clicking toggles it) and `onclick` still fires.
5
+ // `label` is the accessible name; `labelVisible` renders it beside the track.
5
6
  import type { HTMLButtonAttributes } from 'svelte/elements';
6
7
  import { getFieldContext } from '../../field-context';
7
8
 
8
9
  let {
9
- checked = false,
10
+ checked = $bindable(false),
10
11
  invalid = false,
11
12
  label,
13
+ labelVisible = false,
14
+ size = 'md',
15
+ onclick,
12
16
  id,
13
17
  'aria-describedby': ariaDescribedby,
14
18
  'aria-invalid': ariaInvalid,
@@ -18,6 +22,9 @@
18
22
  checked?: boolean;
19
23
  invalid?: boolean;
20
24
  label: string;
25
+ /** Render `label` as visible text beside the track. */
26
+ labelVisible?: boolean;
27
+ size?: ControlSize;
21
28
  id?: string;
22
29
  'aria-describedby'?: string | null;
23
30
  'aria-invalid'?: HTMLButtonAttributes['aria-invalid'];
@@ -25,23 +32,42 @@
25
32
 
26
33
  const field = getFieldContext();
27
34
  const isInvalid = $derived(invalid || !!field?.invalid);
35
+
36
+ function toggle(e: MouseEvent & { currentTarget: EventTarget & HTMLButtonElement }) {
37
+ onclick?.(e);
38
+ if (e.defaultPrevented) return;
39
+ checked = !checked;
40
+ }
28
41
  </script>
29
42
 
30
- <button
31
- data-tsu="Switch"
32
- {...rest}
33
- type="button"
34
- class="switch {klass}"
35
- class:on={checked}
36
- role="switch"
37
- aria-checked={checked}
38
- id={id ?? field?.id}
39
- aria-describedby={ariaDescribedby ?? field?.describedBy}
40
- aria-invalid={ariaInvalid ?? (isInvalid ? 'true' : undefined)}
41
- aria-label={label}
42
- >
43
- <span class="knob"></span>
44
- </button>
43
+ {#snippet control()}
44
+ <button
45
+ data-tsu="Switch"
46
+ {...rest}
47
+ type="button"
48
+ class="switch {klass}"
49
+ class:on={checked}
50
+ class:switch-sm={size === 'sm'}
51
+ role="switch"
52
+ aria-checked={checked}
53
+ id={id ?? field?.id}
54
+ aria-describedby={ariaDescribedby ?? field?.describedBy}
55
+ aria-invalid={ariaInvalid ?? (isInvalid ? 'true' : undefined)}
56
+ aria-label={labelVisible ? undefined : label}
57
+ onclick={toggle}
58
+ >
59
+ <span class="knob"></span>
60
+ </button>
61
+ {/snippet}
62
+
63
+ {#if labelVisible}
64
+ <label class="switch-row" class:switch-row-sm={size === 'sm'}>
65
+ {@render control()}
66
+ <span class="switch-label">{label}</span>
67
+ </label>
68
+ {:else}
69
+ {@render control()}
70
+ {/if}
45
71
 
46
72
  <style>
47
73
  .switch {
@@ -76,6 +102,10 @@
76
102
  transform: translateX(1.15rem);
77
103
  background: var(--text-on-accent);
78
104
  }
105
+ .switch:focus-visible {
106
+ outline: var(--focus-ring);
107
+ outline-offset: var(--focus-ring-offset);
108
+ }
79
109
  .switch:disabled {
80
110
  opacity: 0.45;
81
111
  cursor: not-allowed;
@@ -83,4 +113,26 @@
83
113
  .switch[aria-invalid='true']:not(.on) {
84
114
  border-color: var(--danger);
85
115
  }
116
+ .switch-sm {
117
+ width: 2rem;
118
+ height: 1.2rem;
119
+ }
120
+ .switch-sm .knob {
121
+ width: 0.9rem;
122
+ height: 0.9rem;
123
+ }
124
+ .switch-sm.on .knob {
125
+ transform: translateX(0.8rem);
126
+ }
127
+ .switch-row {
128
+ display: inline-flex;
129
+ align-items: center;
130
+ gap: var(--sp-2);
131
+ cursor: pointer;
132
+ font-size: var(--fs-sm);
133
+ color: var(--text);
134
+ }
135
+ .switch-row-sm {
136
+ font-size: var(--fs-xs);
137
+ }
86
138
  </style>
@@ -1,12 +1,16 @@
1
+ import type { ControlSize } from '../../size';
1
2
  import type { HTMLButtonAttributes } from 'svelte/elements';
2
3
  type $$ComponentProps = HTMLButtonAttributes & {
3
4
  checked?: boolean;
4
5
  invalid?: boolean;
5
6
  label: string;
7
+ /** Render `label` as visible text beside the track. */
8
+ labelVisible?: boolean;
9
+ size?: ControlSize;
6
10
  id?: string;
7
11
  'aria-describedby'?: string | null;
8
12
  'aria-invalid'?: HTMLButtonAttributes['aria-invalid'];
9
13
  };
10
- declare const Switch: import("svelte").Component<$$ComponentProps, {}, "">;
14
+ declare const Switch: import("svelte").Component<$$ComponentProps, {}, "checked">;
11
15
  type Switch = ReturnType<typeof Switch>;
12
16
  export default Switch;
@@ -1,4 +1,6 @@
1
1
  <script lang="ts">
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import { canonicalTone, type Tone } from '../../tone';
2
4
  // Text primitive: the ONLY place body/label/caption/code text and its bearing
3
5
  // elements (<p>/<span>/<label>/<div> of pure text) are emitted. `variant` picks
4
6
  // a token preset; `tone`/`weight`/`size` override individual axes; bare <Text>
@@ -9,39 +11,21 @@
9
11
 
10
12
  type Size = 'xs' | 'sm' | 'base' | 'md' | 'lg' | 'xl' | '2xl';
11
13
 
12
- let {
13
- as = 'span',
14
- variant,
15
- tone = 'inherit',
16
- weight,
17
- size,
18
- numeric = false,
19
- truncate = false,
20
- italic = false,
21
- nowrap = false,
22
- wrap = 'normal',
23
- uppercase = false,
24
- leading,
25
- measure,
26
- grow = false,
27
- scale = true,
28
- block = false,
29
- class: klass = '',
30
- children,
31
- ...rest
32
- }: {
14
+ type Own = {
33
15
  as?: 'span' | 'p' | 'div' | 'label';
34
16
  // body: default reading text · label: form-label · caption: small meta ·
35
17
  // code: monospace · eyebrow: small uppercase muted kicker. Omit for inline
36
18
  // glue that should inherit its parent.
37
19
  variant?: 'body' | 'label' | 'caption' | 'code' | 'eyebrow';
38
20
  // `ok` and `success` are aliases.
39
- tone?: 'inherit' | 'default' | 'muted' | 'faint' | 'ok' | 'success' | 'warn' | 'danger' | 'accent';
21
+ tone?: 'inherit' | 'default' | 'muted' | 'faint' | Tone;
40
22
  weight?: 'normal' | 'medium' | 'semibold' | 'bold';
41
23
  size?: Size;
42
24
  // Tabular figures: digits share a fixed advance width so counts/percentages
43
25
  // don't jitter as they change. Use for counters, timers, metrics.
44
26
  numeric?: boolean;
27
+ /** Monospace family without the `code` variant's chip styling. */
28
+ mono?: boolean;
45
29
  truncate?: boolean;
46
30
  italic?: boolean;
47
31
  nowrap?: boolean;
@@ -56,10 +40,33 @@
56
40
  block?: boolean;
57
41
  class?: string;
58
42
  children?: Snippet;
59
- [key: string]: unknown;
60
- } = $props();
43
+ };
44
+ let {
45
+ as = 'span',
46
+ variant,
47
+ tone = 'inherit',
48
+ weight,
49
+ size,
50
+ numeric = false,
51
+ mono = false,
52
+ truncate = false,
53
+ italic = false,
54
+ nowrap = false,
55
+ wrap = 'normal',
56
+ uppercase = false,
57
+ leading,
58
+ measure,
59
+ grow = false,
60
+ scale = true,
61
+ block = false,
62
+ class: klass = '',
63
+ children,
64
+ ...rest
65
+ }: Omit<HTMLAttributes<HTMLElement>, keyof Own> & Own = $props();
61
66
 
62
- const toneClass = $derived(tone === 'ok' ? 'success' : tone);
67
+ const toneClass = $derived(
68
+ tone === 'neutral' ? 'default' : canonicalTone(tone) === 'ok' ? 'success' : canonicalTone(tone)
69
+ );
63
70
  const styleAttr = $derived(measure ? `max-width: ${measure}` : undefined);
64
71
  </script>
65
72
 
@@ -68,7 +75,7 @@
68
75
  data-tsu="Text"
69
76
  class="text {variant ? `v-${variant}` : ''} tone-{toneClass} {weight ? `fw-${weight}` : ''} {size
70
77
  ? `fs-${size}`
71
- : ''} {numeric ? 'numeric' : ''} {truncate ? 'truncate' : ''} {leading ? `lh-${leading}` : ''} {wrap !==
78
+ : ''} {numeric ? 'numeric' : ''} {mono ? 'mono' : ''} {truncate ? 'truncate' : ''} {leading ? `lh-${leading}` : ''} {wrap !==
72
79
  'normal'
73
80
  ? `wrap-${wrap}`
74
81
  : ''} {klass}"
@@ -116,6 +123,9 @@
116
123
  letter-spacing: 0.04em;
117
124
  }
118
125
  /* Tone (colour) — overrides variant colour. */
126
+ .mono {
127
+ font-family: var(--font-mono);
128
+ }
119
129
  .tone-default {
120
130
  color: var(--text);
121
131
  }
@@ -134,6 +144,9 @@
134
144
  .tone-danger {
135
145
  color: var(--danger);
136
146
  }
147
+ .tone-info {
148
+ color: var(--info);
149
+ }
137
150
  .tone-accent {
138
151
  color: var(--accent);
139
152
  }
@@ -1,12 +1,16 @@
1
+ import type { HTMLAttributes } from 'svelte/elements';
2
+ import { type Tone } from '../../tone';
1
3
  import type { Snippet } from 'svelte';
2
4
  type Size = 'xs' | 'sm' | 'base' | 'md' | 'lg' | 'xl' | '2xl';
3
- type $$ComponentProps = {
5
+ type Own = {
4
6
  as?: 'span' | 'p' | 'div' | 'label';
5
7
  variant?: 'body' | 'label' | 'caption' | 'code' | 'eyebrow';
6
- tone?: 'inherit' | 'default' | 'muted' | 'faint' | 'ok' | 'success' | 'warn' | 'danger' | 'accent';
8
+ tone?: 'inherit' | 'default' | 'muted' | 'faint' | Tone;
7
9
  weight?: 'normal' | 'medium' | 'semibold' | 'bold';
8
10
  size?: Size;
9
11
  numeric?: boolean;
12
+ /** Monospace family without the `code` variant's chip styling. */
13
+ mono?: boolean;
10
14
  truncate?: boolean;
11
15
  italic?: boolean;
12
16
  nowrap?: boolean;
@@ -19,8 +23,8 @@ type $$ComponentProps = {
19
23
  block?: boolean;
20
24
  class?: string;
21
25
  children?: Snippet;
22
- [key: string]: unknown;
23
26
  };
27
+ type $$ComponentProps = Omit<HTMLAttributes<HTMLElement>, keyof Own> & Own;
24
28
  declare const Text: import("svelte").Component<$$ComponentProps, {}, "">;
25
29
  type Text = ReturnType<typeof Text>;
26
30
  export default Text;
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import type { ControlSize } from '../../size';
2
3
  // Multi-line text input primitive. Owns its styling from theme tokens;
3
4
  // supports `bind:value` and passes through every native <textarea> attribute
4
5
  // and event (Svelte 5 events are props, so `...rest` forwards them). `mono`
@@ -10,10 +11,9 @@
10
11
  // handle on the top or bottom edge, styled like the Modal/AppShell grips: a
11
12
  // centered pill that's a thicker portion of the border.
12
13
  //
13
- // With `autoresize`, only a `top` handle is offered and it sets a manual
14
- // *floor* (min-height) rather than a fixed height: the textarea still grows
15
- // with content, but dragging up reserves extra space. (A bottom handle makes
16
- // no sense alongside content-driven sizing, so it's suppressed.)
14
+ // With `autoresize`, the handle (either edge) sets a manual *floor*
15
+ // (min-height) rather than a fixed height: the textarea still grows with
16
+ // content, but dragging reserves extra space. `rows` is the initial floor.
17
17
  //
18
18
  // `onsubmit` fires with the value on Enter (`submitOn="enter"`, Shift+Enter
19
19
  // still inserts a newline) or Ctrl/Meta+Enter (`submitOn="mod-enter"`, the
@@ -25,10 +25,16 @@
25
25
  type Props = HTMLTextareaAttributes & {
26
26
  mono?: boolean;
27
27
  autoresize?: boolean;
28
- size?: 'sm' | 'md';
28
+ size?: ControlSize;
29
+ /** Fill the free space of a flex row (`flex: 1 1 0`). */
30
+ grow?: boolean;
31
+ /** `false` pins the box (`flex: none`). */
32
+ shrink?: boolean;
33
+ /** Full-width block. */
34
+ block?: boolean;
29
35
  /** Manual resize handle edge, or 'none' to disable. Defaults to a bottom
30
- * handle. With `autoresize`, only `top` is honored and it drags a
31
- * min-height floor (the textarea still grows with content). */
36
+ * handle. With `autoresize` it drags a min-height floor (the textarea
37
+ * still grows with content). */
32
38
  resize?: 'none' | 'top' | 'bottom';
33
39
  /** Error state: danger border + aria-invalid (also styles if a consumer
34
40
  * sets aria-invalid directly). */
@@ -50,6 +56,9 @@
50
56
  mono = false,
51
57
  autoresize = false,
52
58
  size = 'md',
59
+ grow = false,
60
+ shrink = true,
61
+ block = false,
53
62
  resize = 'bottom',
54
63
  invalid = false,
55
64
  maxHeight,
@@ -70,8 +79,7 @@
70
79
 
71
80
  $effect(() => warnUnlabelled(el, 'Textarea'));
72
81
 
73
- // With autoresize, only the top handle (a min-height floor) is meaningful.
74
- const handleEdge = $derived(autoresize ? (resize === 'top' ? 'top' : 'none') : resize);
82
+ const handleEdge = $derived(resize);
75
83
  const showHandle = $derived(handleEdge !== 'none');
76
84
  const submitMode = $derived(submitOn === 'none' && onsubmit ? 'mod-enter' : submitOn);
77
85
 
@@ -136,13 +144,21 @@
136
144
  }
137
145
  </script>
138
146
 
139
- <div class="textarea-wrap" class:dragging data-tsu="Textarea">
147
+ <div
148
+ class="textarea-wrap"
149
+ class:dragging
150
+ class:grow={grow}
151
+ class:no-shrink={!shrink}
152
+ class:block={block}
153
+ data-tsu="Textarea"
154
+ >
140
155
  {#if autoresize}
141
156
  <textarea
142
157
  bind:this={el}
143
158
  class="textarea {klass}"
144
159
  class:mono
145
160
  class:textarea-sm={size === 'sm'}
161
+ class:textarea-lg={size === 'lg'}
146
162
  class:capped={!!maxHeight}
147
163
  style:max-height={maxHeight}
148
164
  bind:value
@@ -159,6 +175,7 @@
159
175
  class="textarea {klass}"
160
176
  class:mono
161
177
  class:textarea-sm={size === 'sm'}
178
+ class:textarea-lg={size === 'lg'}
162
179
  class:capped={!!maxHeight}
163
180
  style:max-height={maxHeight}
164
181
  bind:value
@@ -184,6 +201,17 @@
184
201
  </div>
185
202
 
186
203
  <style>
204
+ .grow {
205
+ flex: 1 1 0;
206
+ min-width: 0;
207
+ }
208
+ .no-shrink {
209
+ flex: none;
210
+ }
211
+ .block {
212
+ display: flex;
213
+ width: 100%;
214
+ }
187
215
  .textarea-wrap {
188
216
  position: relative;
189
217
  display: flex;
@@ -212,6 +240,10 @@
212
240
  outline: none;
213
241
  border-color: var(--accent);
214
242
  }
243
+ .textarea:focus-visible {
244
+ outline: var(--focus-ring);
245
+ outline-offset: var(--focus-ring-offset);
246
+ }
215
247
  .textarea.capped {
216
248
  overflow-y: auto;
217
249
  }
@@ -220,6 +252,11 @@
220
252
  font-size: var(--fs-sm);
221
253
  min-height: 2rem;
222
254
  }
255
+ .textarea-lg {
256
+ padding: var(--sp-3) var(--sp-4);
257
+ font-size: var(--fs-base);
258
+ min-height: var(--control-height-large);
259
+ }
223
260
  .textarea[aria-invalid='true'],
224
261
  .textarea[aria-invalid='true']:focus {
225
262
  border-color: var(--danger);
@@ -1,11 +1,18 @@
1
+ import type { ControlSize } from '../../size';
1
2
  import type { HTMLTextareaAttributes } from 'svelte/elements';
2
3
  type Props = HTMLTextareaAttributes & {
3
4
  mono?: boolean;
4
5
  autoresize?: boolean;
5
- size?: 'sm' | 'md';
6
+ size?: ControlSize;
7
+ /** Fill the free space of a flex row (`flex: 1 1 0`). */
8
+ grow?: boolean;
9
+ /** `false` pins the box (`flex: none`). */
10
+ shrink?: boolean;
11
+ /** Full-width block. */
12
+ block?: boolean;
6
13
  /** Manual resize handle edge, or 'none' to disable. Defaults to a bottom
7
- * handle. With `autoresize`, only `top` is honored and it drags a
8
- * min-height floor (the textarea still grows with content). */
14
+ * handle. With `autoresize` it drags a min-height floor (the textarea
15
+ * still grows with content). */
9
16
  resize?: 'none' | 'top' | 'bottom';
10
17
  /** Error state: danger border + aria-invalid (also styles if a consumer
11
18
  * sets aria-invalid directly). */
@@ -27,7 +27,9 @@
27
27
  sidebarWidthKey,
28
28
  layout = 'header-top',
29
29
  stickySidebar = false,
30
- sidebarPadding = 'md'
30
+ sidebarPadding = 'md',
31
+ class: klass = '',
32
+ style: styleProp = '',
31
33
  }: {
32
34
  header?: Snippet;
33
35
  sidebar?: Snippet;
@@ -52,6 +54,8 @@
52
54
  stickySidebar?: boolean;
53
55
  /** Inner padding of the sidebar; `'none'` when the nav owns its gutters. */
54
56
  sidebarPadding?: 'none' | 'sm' | 'md';
57
+ class?: string;
58
+ style?: string;
55
59
  } = $props();
56
60
 
57
61
  let open = $state(false);
@@ -127,11 +131,11 @@
127
131
  <svelte:window onkeydown={(e) => e.key === 'Escape' && (open = false)} />
128
132
 
129
133
  <div
130
- class="shell"
134
+ class="shell {klass}"
131
135
  class:dragging
132
136
  class:sidebar-full={layout === 'sidebar-full'}
133
137
  class:sticky-sidebar={stickySidebar}
134
- style="--shell-sidebar-w: {widthCss}"
138
+ style="--shell-sidebar-w: {widthCss}; {styleProp}"
135
139
  data-tsu="AppShell"
136
140
  >
137
141
  <header class="shell-header">
@@ -23,6 +23,8 @@ type $$ComponentProps = {
23
23
  stickySidebar?: boolean;
24
24
  /** Inner padding of the sidebar; `'none'` when the nav owns its gutters. */
25
25
  sidebarPadding?: 'none' | 'sm' | 'md';
26
+ class?: string;
27
+ style?: string;
26
28
  };
27
29
  declare const AppShell: import("svelte").Component<$$ComponentProps, {}, "">;
28
30
  type AppShell = ReturnType<typeof AppShell>;
@@ -28,6 +28,7 @@
28
28
  fill = false,
29
29
  gap = 'var(--sp-4)',
30
30
  maxCols,
31
+ template,
31
32
  align = 'start',
32
33
  justify,
33
34
  class: klass = '',
@@ -45,6 +46,8 @@
45
46
  fill?: boolean;
46
47
  gap?: string;
47
48
  maxCols?: number;
49
+ /** Explicit `grid-template-columns` (e.g. `1fr 1fr auto`); bypasses `min`/`max`. */
50
+ template?: string;
48
51
  /** Cross-axis alignment of items in their row (align-items). Defaults to `start`. */
49
52
  align?: 'start' | 'center' | 'end' | 'stretch';
50
53
  /** Inline (main-axis) distribution of tracks (justify-content). Defaults to
@@ -70,6 +73,11 @@
70
73
  `--ag-mode: ${mode}`,
71
74
  maxCols != null ? `--ag-gap: ${gap}` : null,
72
75
  maxCols != null ? `--ag-cols: ${maxCols}` : null,
76
+ template
77
+ ? `grid-template-columns: ${template}`
78
+ : max != null && maxCols == null
79
+ ? `grid-template-columns: repeat(auto-fill, minmax(min(100%, ${min}), ${max}))`
80
+ : null,
73
81
  `gap: ${gap}`,
74
82
  `align-items: ${align}`,
75
83
  justifyValue ? `justify-content: ${justifyValue}` : null
@@ -11,6 +11,8 @@ type $$ComponentProps = {
11
11
  fill?: boolean;
12
12
  gap?: string;
13
13
  maxCols?: number;
14
+ /** Explicit `grid-template-columns` (e.g. `1fr 1fr auto`); bypasses `min`/`max`. */
15
+ template?: string;
14
16
  /** Cross-axis alignment of items in their row (align-items). Defaults to `start`. */
15
17
  align?: 'start' | 'center' | 'end' | 'stretch';
16
18
  /** Inline (main-axis) distribution of tracks (justify-content). Defaults to
@@ -27,6 +27,10 @@
27
27
  wrap = true,
28
28
  size,
29
29
  grow = false,
30
+ shrink = true,
31
+ fill = false,
32
+ push,
33
+ wrapAt,
30
34
  stackAt,
31
35
  class: klass = '',
32
36
  children,
@@ -39,18 +43,56 @@
39
43
  wrap?: boolean;
40
44
  size?: 'sm' | 'md' | 'lg';
41
45
  grow?: boolean;
46
+ /** `false` pins the cluster (`flex: none`) inside a parent row. */
47
+ shrink?: boolean;
48
+ /** `height: 100%` to fill a flex parent. */
49
+ fill?: boolean;
50
+ /** Auto margin that pushes the cluster to the start/end of its parent. */
51
+ push?: 'start' | 'end';
52
+ /** Width (px/em/rem) under which the row starts wrapping (wraps always when unset and `wrap`). */
53
+ wrapAt?: string;
42
54
  stackAt?: 'xs' | 'sm' | 'md' | 'lg';
43
55
  class?: string;
56
+ style?: string;
44
57
  children?: Snippet;
45
58
  [key: string]: unknown;
46
59
  } = $props();
60
+
61
+ let el = $state<HTMLElement | null>(null);
62
+ let narrow = $state(false);
63
+ function toPx(length: string, node: HTMLElement): number {
64
+ const n = Number.parseFloat(length);
65
+ const fontSize = (x: Element) => Number.parseFloat(getComputedStyle(x).fontSize);
66
+ if (length.endsWith('rem')) return n * fontSize(document.documentElement);
67
+ if (length.endsWith('em')) return n * fontSize(node);
68
+ return n;
69
+ }
70
+ $effect(() => {
71
+ if (!wrapAt || !el) {
72
+ narrow = false;
73
+ return;
74
+ }
75
+ const node = el;
76
+ const limit = toPx(wrapAt, node);
77
+ const ro = new ResizeObserver(([entry]) => {
78
+ narrow = entry.contentRect.width < limit;
79
+ });
80
+ ro.observe(node);
81
+ return () => ro.disconnect();
82
+ });
83
+ const wrapping = $derived(wrapAt ? narrow : wrap);
47
84
  </script>
48
85
 
49
86
  <svelte:element
50
87
  this={as}
88
+ bind:this={el}
51
89
  data-tsu="Cluster"
52
90
  class="cluster-c {klass}"
53
91
  class:cluster-grow={grow}
92
+ class:no-shrink={!shrink}
93
+ class:fill
94
+ class:push-start={push === 'start'}
95
+ class:push-end={push === 'end'}
54
96
  class:cluster-stack={stackAt !== undefined}
55
97
  class:stack-xs={stackAt === 'xs'}
56
98
  class:stack-sm={stackAt === 'sm'}
@@ -59,7 +101,7 @@
59
101
  style:gap
60
102
  style:align-items={align}
61
103
  style:justify-content={justify}
62
- style:flex-wrap={wrap ? 'wrap' : 'nowrap'}
104
+ style:flex-wrap={wrapping ? 'wrap' : 'nowrap'}
63
105
  style:--control-height={size ? CONTROL_TIER[size] : undefined}
64
106
  {...rest}
65
107
  >
@@ -74,6 +116,25 @@
74
116
  flex: 1 1 0;
75
117
  min-width: 0;
76
118
  }
119
+ .cluster-c > :global([data-grow]) {
120
+ flex: 1 1 0;
121
+ min-width: 0;
122
+ }
123
+ .cluster-c > :global([data-shrink='false']) {
124
+ flex: none;
125
+ }
126
+ .no-shrink {
127
+ flex: none;
128
+ }
129
+ .fill {
130
+ height: 100%;
131
+ }
132
+ .push-start {
133
+ margin-inline-end: auto;
134
+ }
135
+ .push-end {
136
+ margin-inline-start: auto;
137
+ }
77
138
  .cluster-stack {
78
139
  container-type: inline-size;
79
140
  }
@@ -7,8 +7,17 @@ type $$ComponentProps = {
7
7
  wrap?: boolean;
8
8
  size?: 'sm' | 'md' | 'lg';
9
9
  grow?: boolean;
10
+ /** `false` pins the cluster (`flex: none`) inside a parent row. */
11
+ shrink?: boolean;
12
+ /** `height: 100%` to fill a flex parent. */
13
+ fill?: boolean;
14
+ /** Auto margin that pushes the cluster to the start/end of its parent. */
15
+ push?: 'start' | 'end';
16
+ /** Width (px/em/rem) under which the row starts wrapping (wraps always when unset and `wrap`). */
17
+ wrapAt?: string;
10
18
  stackAt?: 'xs' | 'sm' | 'md' | 'lg';
11
19
  class?: string;
20
+ style?: string;
12
21
  children?: Snippet;
13
22
  [key: string]: unknown;
14
23
  };