lutra 0.1.68 → 0.1.70

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 (84) hide show
  1. package/dist/components/AspectRatio.svelte +19 -9
  2. package/dist/components/AspectRatio.svelte.d.ts +2 -1
  3. package/dist/components/Avatar.svelte +5 -8
  4. package/dist/components/Close.svelte +24 -27
  5. package/dist/components/Close.svelte.d.ts +2 -0
  6. package/dist/components/ContextTip.svelte +3 -2
  7. package/dist/components/DataList.svelte +111 -0
  8. package/dist/components/DataList.svelte.d.ts +10 -0
  9. package/dist/components/DataListTypes.d.ts +14 -0
  10. package/dist/components/DataListTypes.js +1 -0
  11. package/dist/components/Dialog.svelte +38 -0
  12. package/dist/components/Icon.svelte +2 -2
  13. package/dist/components/IconButton.svelte +10 -22
  14. package/dist/components/Image.svelte +2 -2
  15. package/dist/components/Indicator.svelte +2 -1
  16. package/dist/components/Inset.svelte +13 -0
  17. package/dist/components/Layout.svelte +7 -3
  18. package/dist/components/Layout.svelte.d.ts +3 -2
  19. package/dist/components/MenuDropdown.svelte +12 -2
  20. package/dist/components/MenuItem.svelte +30 -14
  21. package/dist/components/MenuItem.svelte.d.ts +6 -0
  22. package/dist/components/Modal.svelte +36 -20
  23. package/dist/components/Popover.svelte +43 -13
  24. package/dist/components/TabbedContent.svelte +1 -1
  25. package/dist/components/TabbedContentItem.svelte +14 -0
  26. package/dist/components/TabbedContentItem.svelte.d.ts +4 -0
  27. package/dist/components/Table.svelte +69 -0
  28. package/dist/components/Table.svelte.d.ts +7 -0
  29. package/dist/components/Tabs.svelte +44 -36
  30. package/dist/components/Tag.svelte +53 -13
  31. package/dist/components/Tag.svelte.d.ts +4 -0
  32. package/dist/components/Theme.svelte +121 -94
  33. package/dist/components/Theme.svelte.d.ts +7 -6
  34. package/dist/components/Toast.svelte +11 -8
  35. package/dist/components/Tooltip.svelte +17 -10
  36. package/dist/components/index.d.ts +2 -0
  37. package/dist/components/index.js +2 -0
  38. package/dist/css/1-props.css +197 -163
  39. package/dist/css/2-init.css +519 -0
  40. package/dist/css/{2-base.css → 3-base.css} +42 -131
  41. package/dist/css/{3-typo.css → 4-typo.css} +3 -1
  42. package/dist/css/lutra.css +7 -6
  43. package/dist/css/themes/DefaultTheme.css +26 -4
  44. package/dist/form/Button.svelte +20 -0
  45. package/dist/form/Button.svelte.d.ts +9 -0
  46. package/dist/form/Datepicker.svelte +13 -0
  47. package/dist/form/Datepicker.svelte.d.ts +3 -0
  48. package/dist/form/FieldContent.svelte +20 -11
  49. package/dist/form/FieldError.svelte +1 -1
  50. package/dist/form/FieldGroup.svelte +84 -0
  51. package/dist/form/FieldGroup.svelte.d.ts +20 -0
  52. package/dist/form/Fieldset.svelte +19 -11
  53. package/dist/form/Form.svelte +137 -63
  54. package/dist/form/Form.svelte.d.ts +21 -0
  55. package/dist/form/FormActions.svelte +21 -3
  56. package/dist/form/FormActions.svelte.d.ts +3 -0
  57. package/dist/form/FormSection.svelte +22 -20
  58. package/dist/form/ImageUpload.svelte +50 -30
  59. package/dist/form/ImageUpload.svelte.d.ts +14 -0
  60. package/dist/form/Input.svelte +62 -30
  61. package/dist/form/Input.svelte.d.ts +0 -1
  62. package/dist/form/InputLength.svelte +5 -5
  63. package/dist/form/Label.svelte +6 -6
  64. package/dist/form/LogoUpload.svelte +24 -10
  65. package/dist/form/Select.svelte +23 -10
  66. package/dist/form/Select.svelte.d.ts +6 -6
  67. package/dist/form/Textarea.svelte +11 -1
  68. package/dist/form/Toggle.svelte +162 -0
  69. package/dist/form/Toggle.svelte.d.ts +31 -17
  70. package/dist/form/client.svelte.js +0 -2
  71. package/dist/form/index.d.ts +1 -0
  72. package/dist/form/index.js +1 -0
  73. package/dist/state/Persisted.svelte.d.ts +6 -0
  74. package/dist/state/Persisted.svelte.js +29 -0
  75. package/dist/state/theme.svelte.d.ts +7 -0
  76. package/dist/state/theme.svelte.js +14 -0
  77. package/dist/types.d.ts +6 -23
  78. package/dist/types.js +0 -17
  79. package/dist/util/color.js +2 -2
  80. package/package.json +5 -4
  81. package/dist/config.d.ts +0 -30
  82. package/dist/config.js +0 -18
  83. /package/dist/css/{4-layout.css → 5-layout.css} +0 -0
  84. /package/dist/css/{5-media.css → 6-media.css} +0 -0
@@ -1,130 +1,6 @@
1
1
  /**
2
2
  * Base DOM setup
3
- * Initializes shorthand variables and foundational sizing
4
3
  */
5
- :root {
6
- /* Base size for all sizing */
7
- --base-size: 16px;
8
-
9
- /* Spacing scale */
10
- --space-base: 1em;
11
- --space-025: calc(var(--space-base) * 0.25);
12
- --space-050: calc(var(--space-base) * 0.5);
13
- --space-075: calc(var(--space-base) * 0.75);
14
- --space-100: calc(var(--space-base) * 1);
15
- --space-125: calc(var(--space-base) * 1.25);
16
- --space-150: calc(var(--space-base) * 1.5);
17
- --space-175: calc(var(--space-base) * 1.75);
18
- --space-200: calc(var(--space-base) * 2);
19
- --space-225: calc(var(--space-base) * 2.25);
20
- --space-250: calc(var(--space-base) * 2.5);
21
- --space-300: calc(var(--space-base) * 3);
22
- --space-350: calc(var(--space-base) * 3.5);
23
- --space-400: calc(var(--space-base) * 4);
24
- --space-450: calc(var(--space-base) * 4.5);
25
- --space-500: calc(var(--space-base) * 5);
26
- --space-550: calc(var(--space-base) * 5.5);
27
- --space-600: calc(var(--space-base) * 6);
28
- --space-700: calc(var(--space-base) * 7);
29
- --space-800: calc(var(--space-base) * 8);
30
- --space-900: calc(var(--space-base) * 9);
31
- --space-1000: calc(var(--space-base) * 10);
32
-
33
- --space-xxs: var(--space-025);
34
- --space-xs: var(--space-050);
35
- --space-sm: var(--space-075);
36
- --space-md: var(--space-100);
37
- --space-lg: var(--space-150);
38
- --space-xl: var(--space-200);
39
- --space-xxl: var(--space-300);
40
- --space-xxxl: var(--space-400);
41
-
42
- --font-scale: 1.175;
43
- --font-size-base: 1em;
44
-
45
- --font-size-xs: calc(var(--font-size-base) * pow(var(--font-scale), -2));
46
- --font-size-sm: calc(var(--font-size-base) * pow(var(--font-scale), -1));
47
- --font-size-p: var(--font-size-base);
48
- --font-size-h6: var(--font-size-base);
49
- --font-size-h5: calc(var(--font-size-base) * pow(var(--font-scale), 1));
50
- --font-size-h4: calc(var(--font-size-base) * pow(var(--font-scale), 2));
51
- --font-size-h3: calc(var(--font-size-base) * pow(var(--font-scale), 3));
52
- --font-size-h2: calc(var(--font-size-base) * pow(var(--font-scale), 4));
53
- --font-size-h1: calc(var(--font-size-base) * pow(var(--font-scale), 5));
54
- --font-size-hero: calc(var(--font-size-base) * pow(var(--font-scale), 6));
55
- --font-size-jumbo: calc(var(--font-size-base) * pow(var(--font-scale), 7));
56
-
57
- --text-color-h1: var(--text-color-heading);
58
- --text-color-h2: var(--text-color-heading);
59
- --text-color-h3: var(--text-color-heading);
60
- --text-color-h4: var(--text-color-heading);
61
- --text-color-h5: var(--text-color-heading);
62
- --text-color-h6: var(--text-color-heading);
63
- --field-color: var(--text-color-p);
64
-
65
- --border-radius-scale: 1.2;
66
- --border-size-thin: 1px;
67
- --border-size-thick: 2px;
68
-
69
- --border-radius-base: calc(var(--border-radius-scale) * 0.25em);
70
- --border-radius-sm: calc(var(--border-radius-scale) * 0.5em);
71
- --border-radius-lg: calc(var(--border-radius-scale) * 2em);
72
-
73
- --form-border-radius: var(--border-radius-base);
74
- --form-border-size: var(--border-size-thin);
75
- --form-border-style: var(--border-style);
76
- --form-border-color: var(--border-color);
77
-
78
- --field-border-radius: var(--border-radius-base);
79
- --field-border-size: var(--border-size-thin);
80
- --field-border-style: var(--border-style);
81
- --field-border-color: var(--border-color);
82
-
83
- --button-border-radius: var(--border-radius-base);
84
- --button-border-size: var(--border-size-thin);
85
- --button-border-style: var(--border-style);
86
- --button-border-color: var(--border-color);
87
-
88
- --link-color-visited: var(--link-color);
89
- --link-color-active: var(--link-color);
90
-
91
- /* Shorthand compound variables */
92
- --field-padding: var(--field-padding-block) var(--field-padding-inline);
93
- --field-border: var(--field-border-size) var(--field-border-style) var(--field-border-color);
94
-
95
- --button-padding: var(--button-padding-block) var(--button-padding-inline);
96
- --button-border: var(--button-border-size) var(--button-border-style) var(--button-submit-border-color);
97
- --button-icon-size: 1em;
98
-
99
- /* Focus ring compound */
100
- --focus-ring: var(--focus-ring-size) solid var(--focus-ring-color);
101
-
102
- /* Shadows */
103
- --shadow-base: var(--shadow-base);
104
- --shadow-sm: var(--shadow-sm);
105
- --shadow-md: var(--shadow-md);
106
- --shadow-lg: var(--shadow-lg);
107
- --shadow-xl: var(--shadow-xl);
108
- --shadow-2xl: var(--shadow-2xl);
109
-
110
- --tooltip-background: var(--surface-background);
111
- --tooltip-color: var(--text-color-p);
112
- --tooltip-border: var(--surface-border);
113
- --tooltip-border-radius: var(--surface-border-radius);
114
- --tooltip-shadow: var(--surface-shadow);
115
-
116
- /* Table compound variables */
117
- --table-border: var(--table-border-size) var(--table-border-style) var(--table-border-color);
118
- --table-cell-padding: var(--table-cell-padding-block) var(--table-cell-padding-inline);
119
-
120
- /* Modal compound variables */
121
- --modal-border: var(--modal-border-size) var(--modal-border-style) var(--modal-border-color);
122
- --modal-padding: var(--modal-padding-block) var(--modal-padding-inline);
123
- --modal-actions-padding: var(--modal-actions-padding-block) var(--modal-actions-padding-inline);
124
-
125
- --mix-target: light-dark(black, white);
126
- }
127
-
128
4
  body {
129
5
  text-rendering: optimizeLegibility;
130
6
  -webkit-font-smoothing: antialiased;
@@ -141,16 +17,53 @@ body {
141
17
  width: 100%;
142
18
  min-height: 0;
143
19
  scrollbar-gutter: stable;
20
+ -webkit-text-size-adjust: 100%;
21
+ -ms-overflow-style: -ms-autohiding-scrollbar;
22
+ box-sizing: border-box;
23
+ border-collapse: collapse;
24
+ }
25
+
26
+ html {
27
+ color-scheme: light dark;
28
+ &:not(.light) {
29
+ @media (prefers-color-scheme: dark) {
30
+ color-scheme: only dark;
31
+ }
32
+ }
33
+ }
34
+
35
+ html.light {
36
+ color-scheme: only light;
37
+ }
38
+
39
+ html.dark {
40
+ color-scheme: only dark;
41
+ }
42
+
43
+ html,
44
+ body {
45
+ height: 100%;
46
+ width: 100%;
144
47
  }
145
48
 
146
- body * {
49
+ * {
147
50
  margin: 0;
148
51
  padding: 0;
149
52
  box-sizing: border-box;
53
+ scrollbar-width: thin;
54
+ scrollbar-color: var(--scrollbar-color) transparent;
150
55
  }
151
56
 
152
- html {
153
- color-scheme: light dark;
57
+ .visually-hidden {
58
+ border: 0;
59
+ clip: rect(0 0 0 0);
60
+ height: auto;
61
+ margin: 0;
62
+ overflow: hidden;
63
+ padding: 0;
64
+ position: absolute;
65
+ width: 1px;
66
+ white-space: nowrap;
154
67
  }
155
68
 
156
69
  input, textarea, select, button {
@@ -229,12 +142,10 @@ input, textarea, select, .button, ::file-selector-button, .Field {
229
142
  background-image var(--transition-duration-fast) var(--transition-timing-function),
230
143
  background-color var(--transition-duration-fast) var(--transition-timing-function),
231
144
  color var(--transition-duration-fast) var(--transition-timing-function),
232
- box-shadow var(--transition-duration-fast) var(--transition-timing-function),
233
- outline var(--transition-duration-fast) var(--transition-timing-function),
234
- outline-offset var(--transition-duration-fast) var(--transition-timing-function);
145
+ box-shadow var(--transition-duration-fast) var(--transition-timing-function);
235
146
  }
236
147
 
237
- input:focus-visible, textarea:focus-visible, select:focus-visible, button:focus-visible {
148
+ input:focus-visible, textarea:focus-visible, select:focus-visible, button:focus-visible, a:focus-visible {
238
149
  outline: var(--focus-ring-size) solid var(--focus-ring-color);
239
150
  outline-offset: var(--focus-ring-offset);
240
151
  border-color: var(--focus-ring-color);
@@ -16,13 +16,15 @@ small {
16
16
  line-height: var(--font-line-height-tight);
17
17
  }
18
18
 
19
- small.smaller {
19
+ small.smaller,
20
+ small small {
20
21
  font-size: var(--font-size-xs);
21
22
  }
22
23
 
23
24
  /* Form elements inherit base typography */
24
25
  input, textarea, select, button {
25
26
  font-family: var(--font-family);
27
+ font-size: inherit;
26
28
  line-height: var(--font-line-height);
27
29
  }
28
30
 
@@ -1,7 +1,8 @@
1
- @layer l-base, base, l-typo, typo, l-layout, layout, theme, l-media, media, misc;
1
+ @layer l-props, l-init, l-base, l-typo, typo, l-layout, l-media, l-default, theme, media, misc;
2
2
 
3
- @import "./1-props.css";
4
- @import "./2-base.css" layer(l-base);
5
- @import "./3-typo.css" layer(l-typo);
6
- @import "./4-layout.css" layer(l-layout);
7
- @import "./5-media.css" layer(l-media);
3
+ @import "./1-props.css" layer(l-props);
4
+ @import "./2-init.css" layer(l-init);
5
+ @import "./3-base.css" layer(l-base);
6
+ @import "./4-typo.css" layer(l-typo);
7
+ @import "./5-layout.css" layer(l-layout);
8
+ @import "./6-media.css" layer(l-media);
@@ -1,4 +1,6 @@
1
- @layer theme {
1
+ @layer l-props, l-init, l-base, l-typo, typo, l-layout, l-media, l-default, theme, media, misc;
2
+
3
+ @layer l-default {
2
4
  :root {
3
5
  --hue: 240deg;
4
6
  --chroma: 0.5;
@@ -170,9 +172,20 @@
170
172
  --checkbox-border-color-checked: var(--button-submit-base-color);
171
173
  --checkbox-indicator-color: light-dark(#ffffff, #ffffff);
172
174
 
175
+ /**
176
+ * Toggle
177
+ */
178
+
179
+ --toggle-background: var(--border-color);
180
+ --toggle-background-checked: var(--checkbox-background-checked);
181
+ --toggle-border-color: var(--toggle-background);
182
+ --toggle-border-color-checked: var(--toggle-background-checked);
183
+ --toggle-thumb-color: var(--checkbox-indicator-color);
184
+
173
185
  --field-label-color: var(--text-color-p);
174
186
 
175
- --form-background-actions: color-mix(in srgb, var(--theme-surface-interactive) 35%, transparent);
187
+ --form-background: var(--background-body);
188
+ --form-background-actions: color-mix(in oklch, var(--theme-surface-interactive) 35%, transparent);
176
189
 
177
190
  /**
178
191
  * Borders
@@ -201,12 +214,20 @@
201
214
  --link-color-hover: var(--button-submit-base-color-hover);
202
215
  --link-color-active: var(--button-submit-base-color-hover);
203
216
 
217
+ /**
218
+ * Status Colors
219
+ */
220
+
221
+ --status-default-color: light-dark(#111111, #e0e0e0);
222
+
204
223
  /**
205
224
  * Menus
206
225
  */
207
226
 
208
227
  --menu-background-color: var(--surface-background);
209
- --menu-background-color-hover: color-mix(in srgb, var(--theme-surface-interactive) 35%, transparent);
228
+ --menu-background-color-hover: color-mix(in oklch, var(--theme-surface-interactive) 35%, transparent);
229
+ --menu-text-color: var(--text-color-p);
230
+ --menu-text-color-subtle: var(--text-color-p-subtle);
210
231
  --menu-border-color: var(--border-color-subtle);
211
232
  --menu-border-size: var(--border-size-thin);
212
233
  --menu-border-style: var(--border-style);
@@ -221,7 +242,7 @@
221
242
  --table-header-color: var(--text-color-heading);
222
243
  --table-row-background: transparent;
223
244
  --table-row-background-even: transparent;
224
- --table-row-background-hover: color-mix(in srgb, var(--theme-surface-interactive) 60%, transparent);
245
+ --table-row-background-hover: color-mix(in oklch, var(--theme-surface-interactive) 60%, transparent);
225
246
  --table-cell-color: var(--text-color-p);
226
247
 
227
248
  /**
@@ -270,6 +291,7 @@
270
291
  --toast-border: var(--surface-border);
271
292
  --toast-border-radius: var(--surface-border-radius);
272
293
  --toast-shadow: var(--surface-shadow);
294
+ --toast-title-color: var(--text-color-heading);
273
295
 
274
296
  /**
275
297
  * Scrim/Backdrop
@@ -3,6 +3,17 @@
3
3
  import { getContext, type Component, type Snippet } from 'svelte';
4
4
  import type { LutraForm } from './types.js';
5
5
 
6
+ /**
7
+ * @description
8
+ * A polymorphic form button that renders as a `<button>` or `<a>` depending on whether `href` is set.
9
+ * Automatically disables itself while its parent form is in a loading state.
10
+ * Inherits the alignment context set by a parent `FormActions` component.
11
+ *
12
+ * @example
13
+ * <Button type="submit" kind="default">Save</Button>
14
+ * <Button href="/cancel" kind="secondary">Cancel</Button>
15
+ * <Button kind="warn" icon={TrashIcon}>Delete</Button>
16
+ */
6
17
  let {
7
18
  href,
8
19
  type = 'button',
@@ -14,14 +25,23 @@
14
25
  onclick,
15
26
  children,
16
27
  }: {
28
+ /** When set, renders as an `<a>` link instead of a `<button>`. */
17
29
  href?: string;
30
+ /** The button type attribute. */
18
31
  type?: 'button' | 'submit' | 'reset';
32
+ /** Visual style variant. */
19
33
  kind?: 'default' | 'outlined' | 'secondary' | 'warn';
34
+ /** Size variant. */
20
35
  size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
36
+ /** Additional CSS class names. */
21
37
  class?: string;
38
+ /** Whether the button is disabled. Also disabled automatically when the parent form is loading. */
22
39
  disabled?: boolean;
40
+ /** Icon component or string to render before the label. */
23
41
  icon?: string | Component;
42
+ /** Click event handler. */
24
43
  onclick?: (event: MouseEvent) => void;
44
+ /** Button label content. */
25
45
  children: Snippet;
26
46
  } = $props();
27
47
 
@@ -1,13 +1,22 @@
1
1
  import { type Component, type Snippet } from 'svelte';
2
2
  type $$ComponentProps = {
3
+ /** When set, renders as an `<a>` link instead of a `<button>`. */
3
4
  href?: string;
5
+ /** The button type attribute. */
4
6
  type?: 'button' | 'submit' | 'reset';
7
+ /** Visual style variant. */
5
8
  kind?: 'default' | 'outlined' | 'secondary' | 'warn';
9
+ /** Size variant. */
6
10
  size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
11
+ /** Additional CSS class names. */
7
12
  class?: string;
13
+ /** Whether the button is disabled. Also disabled automatically when the parent form is loading. */
8
14
  disabled?: boolean;
15
+ /** Icon component or string to render before the label. */
9
16
  icon?: string | Component;
17
+ /** Click event handler. */
10
18
  onclick?: (event: MouseEvent) => void;
19
+ /** Button label content. */
11
20
  children: Snippet;
12
21
  };
13
22
  declare const Button: Component<$$ComponentProps, {}, "">;
@@ -4,11 +4,24 @@
4
4
  import UiContent from "../components/UIContent.svelte";
5
5
  import { getLocaleFirstDayOfWeek } from "../util/locale.js";
6
6
 
7
+ /**
8
+ * @description
9
+ * A custom date/time range picker with year, month and day grids.
10
+ * Renders start and end `datetime-local` inputs alongside visual selection components
11
+ * for year, month and day. Respects the user's locale for first day of week.
12
+ *
13
+ * @example
14
+ * <Datepicker />
15
+ * <Datepicker range={{ min: new Date('2024-01-01'), max: new Date('2025-12-31') }} />
16
+ */
7
17
  let {
8
18
  range
9
19
  }: {
20
+ /** Optional date range constraints. */
10
21
  range?: {
22
+ /** Minimum selectable date. */
11
23
  min?: Date;
24
+ /** Maximum selectable date. */
12
25
  max?: Date;
13
26
  }
14
27
  } = $props();
@@ -1,6 +1,9 @@
1
1
  type $$ComponentProps = {
2
+ /** Optional date range constraints. */
2
3
  range?: {
4
+ /** Minimum selectable date. */
3
5
  min?: Date;
6
+ /** Maximum selectable date. */
4
7
  max?: Date;
5
8
  };
6
9
  };
@@ -7,7 +7,14 @@
7
7
 
8
8
  /**
9
9
  * @description
10
- * This component is used internally by the Input, Select, etc. components to render the label and the input field in a consistent way.
10
+ * Internal wrapper used by Input, Select and Textarea to render the label, field
11
+ * border, prefix/suffix decorations, help text and error messages in a consistent layout.
12
+ *
13
+ * @cssprop --field-background -- Background color of the field container.
14
+ * @cssprop --field-border-size -- Border width of the field container.
15
+ * @cssprop --field-border-style -- Border style of the field container.
16
+ * @cssprop --field-border-color -- Border color of the field container.
17
+ * @cssprop --field-border-radius -- Border radius of the field container.
11
18
  */
12
19
  let {
13
20
  id,
@@ -62,8 +69,7 @@
62
69
  {#if contained}
63
70
  <div
64
71
  class="Field"
65
- class:hasPrefix={prefix}
66
- class:hasSuffix={suffix}
72
+
67
73
  class:invalid={field?.tainted && issue?.code}
68
74
  >
69
75
  {#if prefix}
@@ -105,13 +111,15 @@
105
111
  .FieldContentContainer {
106
112
  display: flex;
107
113
  flex-direction: column;
108
- gap: var(--form-field-gap, var(--space-xs));
114
+ gap: var(--form-field-inside-gap);
115
+ min-width: 0;
109
116
  }
110
117
  .FieldContent {
111
118
  display: flex;
112
119
  gap: var(--form-label-gap, var(--space-xs));
113
120
  flex-direction: column;
114
121
  font-size: 1em;
122
+ min-width: 0;
115
123
  }
116
124
  .FieldContent.row {
117
125
  flex-direction: row-reverse;
@@ -124,6 +132,7 @@
124
132
  border: var(--field-border-size) var(--field-border-style) var(--field-border-color);
125
133
  border-radius: var(--field-border-radius);
126
134
  display: flex;
135
+ min-width: 0;
127
136
  }
128
137
  .Field.invalid {
129
138
  border-color: var(--field-border-color-invalid);
@@ -138,7 +147,7 @@
138
147
  padding-inline: var(--form-field-gap, var(--space-md));
139
148
  font-size: 1em;
140
149
  text-box: trim-both cap alphabetic;
141
- color: var(--text-subtle);
150
+ color: var(--text-color-p-subtle);
142
151
  }
143
152
  .Suffix {
144
153
  padding-inline-start: 0;
@@ -158,21 +167,21 @@
158
167
  outline-color: var(--focus-ring-color-invalid);
159
168
  border-color: var(--focus-ring-color-invalid);
160
169
  }
161
- .Field.hasPrefix :global(input) {
170
+ .Field:has(.Prefix) :global(input) {
162
171
  padding-inline-start: var(--space-xxs);
163
172
  }
164
173
  .Field :global(button) {
165
- margin-right: var(--space-xxs);
174
+ margin-inline-end: var(--space-xxs);
166
175
  }
167
176
  .Field :global(button:focus-visible) {
168
- outline: var(--focus-outline);
177
+ outline: var(--focus-ring);
169
178
  outline-offset: 3px;
170
- border-radius: calc(var(--field-radius) - 2px);
179
+ border-radius: calc(var(--field-border-radius) - 2px);
171
180
  }
172
181
  .Help {
173
- font-size: min(11px, var(--font-size-075));
182
+ font-size: var(--font-size-sm);
174
183
  line-height: var(--font-line-height-tight);
175
184
  color: var(--text-color-p-subtle);
176
- font-weight: var(--font-weight-normal);
185
+ font-weight: var(--font-weight-light);
177
186
  }
178
187
  </style>
@@ -16,7 +16,7 @@
16
16
 
17
17
  <style>
18
18
  p.Error {
19
- color: var(--field-color-danger, light-dark(red, red));
19
+ color: var(--field-color-danger);
20
20
  font-size: max(0.85em, 11px);
21
21
  font-weight: var(--font-weight-normal);
22
22
  text-wrap: balance;
@@ -0,0 +1,84 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from "svelte";
3
+ import Label from "./Label.svelte";
4
+ import StringOrSnippet from "../util/StringOrSnippet.svelte";
5
+
6
+ /**
7
+ * @description
8
+ * Groups related form fields (checkboxes, radios, toggles) under a shared label
9
+ * with a tighter gap than the default form field spacing.
10
+ *
11
+ * @cssprop --field-group-gap -- Gap between items within the group.
12
+ * @cssprop --form-label-gap -- Gap between the group label and the items.
13
+ *
14
+ * @example
15
+ * <FieldGroup label="Permissions">
16
+ * <Input name="read" type="checkbox" label="Read" />
17
+ * <Input name="write" type="checkbox" label="Write" />
18
+ * <Input name="admin" type="checkbox" label="Admin" />
19
+ * </FieldGroup>
20
+ */
21
+ let {
22
+ id = crypto.randomUUID(),
23
+ label,
24
+ labelHelp,
25
+ labelTip,
26
+ help,
27
+ required,
28
+ children,
29
+ }: {
30
+ /** A unique id for the group. Auto-generated if not provided. */
31
+ id?: string;
32
+ /** The group label. */
33
+ label?: string | Snippet;
34
+ /** Help text to display alongside the label. */
35
+ labelHelp?: string | Snippet;
36
+ /** Context tooltip for the label. */
37
+ labelTip?: string | Snippet;
38
+ /** Help text to display below the group. */
39
+ help?: string | Snippet;
40
+ /** Whether the group is required. */
41
+ required?: boolean;
42
+ /** The grouped fields to render. */
43
+ children: Snippet;
44
+ } = $props();
45
+ </script>
46
+
47
+ <div
48
+ class="FieldGroup"
49
+ role="group"
50
+ aria-labelledby={label ? `fg-${id}` : undefined}
51
+ >
52
+ {#if label}
53
+ <div id="fg-{id}">
54
+ <Label {label} tip={labelTip} help={labelHelp} {required} />
55
+ </div>
56
+ {/if}
57
+ <div class="FieldGroupContent">
58
+ {@render children()}
59
+ </div>
60
+ {#if help}
61
+ <div class="Help">
62
+ <StringOrSnippet content={help} />
63
+ </div>
64
+ {/if}
65
+ </div>
66
+
67
+ <style>
68
+ .FieldGroup {
69
+ display: flex;
70
+ flex-direction: column;
71
+ gap: var(--form-label-gap, var(--space-xs));
72
+ }
73
+ .FieldGroupContent {
74
+ display: flex;
75
+ flex-direction: column;
76
+ gap: var(--field-group-gap);
77
+ }
78
+ .Help {
79
+ font-size: var(--font-size-sm);
80
+ line-height: var(--font-line-height-tight);
81
+ color: var(--text-color-p-subtle);
82
+ font-weight: var(--font-weight-light);
83
+ }
84
+ </style>
@@ -0,0 +1,20 @@
1
+ import type { Snippet } from "svelte";
2
+ type $$ComponentProps = {
3
+ /** A unique id for the group. Auto-generated if not provided. */
4
+ id?: string;
5
+ /** The group label. */
6
+ label?: string | Snippet;
7
+ /** Help text to display alongside the label. */
8
+ labelHelp?: string | Snippet;
9
+ /** Context tooltip for the label. */
10
+ labelTip?: string | Snippet;
11
+ /** Help text to display below the group. */
12
+ help?: string | Snippet;
13
+ /** Whether the group is required. */
14
+ required?: boolean;
15
+ /** The grouped fields to render. */
16
+ children: Snippet;
17
+ };
18
+ declare const FieldGroup: import("svelte").Component<$$ComponentProps, {}, "">;
19
+ type FieldGroup = ReturnType<typeof FieldGroup>;
20
+ export default FieldGroup;
@@ -3,14 +3,19 @@
3
3
 
4
4
  /**
5
5
  * @description
6
- * A fieldset is a group of related form elements that can be disabled or enabled.
6
+ * A fieldset is a group of related form elements. Supports responsive column
7
+ * layouts via container queries, optional contained styling with padding,
8
+ * and rounded borders.
9
+ *
10
+ * @cssprop --form-field-gap -- Gap between fields within the fieldset.
11
+ * @cssprop --form-padding-block -- Block padding when contained.
12
+ * @cssprop --form-padding-inline -- Inline padding when contained.
13
+ * @cssprop --field-label-font-size -- Font size for the legend.
14
+ *
7
15
  * @example
8
- * <script>
9
- * import Input from "lutra/Input.svelte";
10
- * <\/script>
11
- * <Fieldset legend="Personal Information">
12
- * <Input label="First Name" />
13
- * <Input label="Last Name" />
16
+ * <Fieldset legend="Personal Information" columns={2}>
17
+ * <Input label="First Name" name="first" />
18
+ * <Input label="Last Name" name="last" />
14
19
  * </Fieldset>
15
20
  */
16
21
 
@@ -76,16 +81,19 @@
76
81
  }
77
82
  fieldset {
78
83
  display: grid;
84
+ border: none;
85
+ margin: 0;
86
+ padding: 0;
79
87
  width: var(--width, fit-content);
80
88
  grid-template-columns: repeat(var(--lg-cols), 1fr);
81
- gap: var(--gap, 1.5em);
89
+ gap: var(--form-field-gap);
82
90
  }
83
91
  legend {
84
- font-weight: 500;
85
- font-size: var(--font-size, 1em);
92
+ font-weight: var(--font-weight-normal);
93
+ font-size: var(--field-label-font-size);
86
94
  }
87
95
  fieldset.contained {
88
- padding: 1.5em;
96
+ padding: var(--form-padding-block) var(--form-padding-inline);
89
97
  }
90
98
  fieldset.fullWidth {
91
99
  width: 100%;