lutra 0.1.68 → 0.1.69

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 (72) 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/Dialog.svelte +38 -0
  8. package/dist/components/Icon.svelte +2 -2
  9. package/dist/components/IconButton.svelte +10 -22
  10. package/dist/components/Image.svelte +2 -2
  11. package/dist/components/Indicator.svelte +2 -1
  12. package/dist/components/Inset.svelte +13 -0
  13. package/dist/components/Layout.svelte +7 -3
  14. package/dist/components/Layout.svelte.d.ts +3 -2
  15. package/dist/components/MenuDropdown.svelte +12 -2
  16. package/dist/components/MenuItem.svelte +30 -14
  17. package/dist/components/MenuItem.svelte.d.ts +6 -0
  18. package/dist/components/Modal.svelte +36 -20
  19. package/dist/components/Popover.svelte +39 -12
  20. package/dist/components/TabbedContent.svelte +1 -1
  21. package/dist/components/TabbedContentItem.svelte +14 -0
  22. package/dist/components/TabbedContentItem.svelte.d.ts +4 -0
  23. package/dist/components/Table.svelte +69 -0
  24. package/dist/components/Table.svelte.d.ts +7 -0
  25. package/dist/components/Tabs.svelte +44 -36
  26. package/dist/components/Tag.svelte +53 -13
  27. package/dist/components/Tag.svelte.d.ts +4 -0
  28. package/dist/components/Theme.svelte +121 -94
  29. package/dist/components/Theme.svelte.d.ts +7 -6
  30. package/dist/components/Toast.svelte +11 -8
  31. package/dist/components/Tooltip.svelte +17 -10
  32. package/dist/css/1-props.css +64 -51
  33. package/dist/css/2-init.css +503 -0
  34. package/dist/css/{2-base.css → 3-base.css} +42 -131
  35. package/dist/css/{3-typo.css → 4-typo.css} +3 -1
  36. package/dist/css/lutra.css +7 -6
  37. package/dist/css/themes/DefaultTheme.css +16 -4
  38. package/dist/form/Button.svelte +20 -0
  39. package/dist/form/Button.svelte.d.ts +9 -0
  40. package/dist/form/Datepicker.svelte +13 -0
  41. package/dist/form/Datepicker.svelte.d.ts +3 -0
  42. package/dist/form/FieldContent.svelte +18 -9
  43. package/dist/form/FieldError.svelte +1 -1
  44. package/dist/form/Fieldset.svelte +19 -11
  45. package/dist/form/Form.svelte +137 -63
  46. package/dist/form/Form.svelte.d.ts +21 -0
  47. package/dist/form/FormActions.svelte +21 -3
  48. package/dist/form/FormActions.svelte.d.ts +3 -0
  49. package/dist/form/FormSection.svelte +22 -20
  50. package/dist/form/ImageUpload.svelte +50 -30
  51. package/dist/form/ImageUpload.svelte.d.ts +14 -0
  52. package/dist/form/Input.svelte +62 -30
  53. package/dist/form/Input.svelte.d.ts +0 -1
  54. package/dist/form/InputLength.svelte +5 -5
  55. package/dist/form/Label.svelte +6 -6
  56. package/dist/form/LogoUpload.svelte +24 -10
  57. package/dist/form/Select.svelte +23 -10
  58. package/dist/form/Select.svelte.d.ts +6 -6
  59. package/dist/form/Textarea.svelte +11 -1
  60. package/dist/form/client.svelte.js +0 -2
  61. package/dist/state/Persisted.svelte.d.ts +6 -0
  62. package/dist/state/Persisted.svelte.js +29 -0
  63. package/dist/state/theme.svelte.d.ts +7 -0
  64. package/dist/state/theme.svelte.js +14 -0
  65. package/dist/types.d.ts +6 -23
  66. package/dist/types.js +0 -17
  67. package/dist/util/color.js +2 -2
  68. package/package.json +5 -4
  69. package/dist/config.d.ts +0 -30
  70. package/dist/config.js +0 -18
  71. /package/dist/css/{4-layout.css → 5-layout.css} +0 -0
  72. /package/dist/css/{5-media.css → 6-media.css} +0 -0
@@ -2,20 +2,38 @@
2
2
  import StringOrSnippet from "../util/StringOrSnippet.svelte";
3
3
  import { setContext, type Snippet } from "svelte";
4
4
 
5
+ /**
6
+ * @description
7
+ * Action bar for form buttons. Renders at the bottom of a form with configurable
8
+ * alignment. When the form is contained, inherits background and padding from
9
+ * `--form-background-actions` via the `--fcc` multiplier pattern.
10
+ *
11
+ * @example
12
+ * <FormActions>
13
+ * <Button type="submit">Save</Button>
14
+ * </FormActions>
15
+ * <FormActions align="justified" info="Changes are saved automatically.">
16
+ * <Button type="submit">Save</Button>
17
+ * <Button kind="secondary" href="/cancel">Cancel</Button>
18
+ * </FormActions>
19
+ */
5
20
  let {
6
21
  align = 'end',
7
22
  children,
8
23
  info,
9
24
  }: {
25
+ /** Alignment of the action buttons. */
10
26
  align?: 'justified' | 'start' | 'center' | 'end' | 'full';
27
+ /** Action buttons and other content. */
11
28
  children: Snippet;
29
+ /** Optional info text displayed alongside the action buttons. */
12
30
  info?: string | Snippet;
13
31
  } = $props();
14
32
 
15
33
  setContext('form.actions.align', align);
16
34
  </script>
17
35
 
18
- <div class="FormActions {align}" class:hasInfo={!!info}>
36
+ <div class="FormActions {align}">
19
37
  {#if info}
20
38
  <div class="Info">
21
39
  <StringOrSnippet content={info} />
@@ -29,7 +47,7 @@
29
47
  <style>
30
48
  .FormActions {
31
49
  display: grid;
32
- background: color-mix(in srgb, var(--form-background-actions) calc(var(--fcc) * 100%), transparent);
50
+ background: color-mix(in oklch, var(--form-background-actions) calc(var(--fcc) * 100%), transparent);
33
51
  padding: calc(var(--space-md) * var(--fcc)) calc(var(--space-xl) * var(--fcc));
34
52
  grid-column: 1 / -1;
35
53
  grid-template-columns: subgrid;
@@ -57,7 +75,7 @@
57
75
  justify-content: end;
58
76
  grid-column: 1 / -1;
59
77
  }
60
- .FormActions.hasInfo .Actions {
78
+ .FormActions:has(.Info) .Actions {
61
79
  grid-column: 2 / -1;
62
80
  }
63
81
  .FormActions.full .Actions {
@@ -1,7 +1,10 @@
1
1
  import { type Snippet } from "svelte";
2
2
  type $$ComponentProps = {
3
+ /** Alignment of the action buttons. */
3
4
  align?: 'justified' | 'start' | 'center' | 'end' | 'full';
5
+ /** Action buttons and other content. */
4
6
  children: Snippet;
7
+ /** Optional info text displayed alongside the action buttons. */
5
8
  info?: string | Snippet;
6
9
  };
7
10
  declare const FormActions: import("svelte").Component<$$ComponentProps, {}, "">;
@@ -3,7 +3,21 @@
3
3
 
4
4
  /**
5
5
  * @description
6
- * A field section is a group of related form elements.
6
+ * A section within a form, grouping related fields under an optional title and
7
+ * description. Uses CSS subgrid for alignment with the parent form layout and
8
+ * responds to `@container` queries to collapse into a single column at narrow widths.
9
+ *
10
+ * @cssprop --form-section-gap -- Gap between the title and content areas.
11
+ * @cssprop --form-title-gap -- Gap within the section title (title + description).
12
+ * @cssprop --form-field-gap -- Gap between fields in the content area.
13
+ * @cssprop --form-padding-block -- Block padding when the form is contained.
14
+ * @cssprop --form-padding-inline -- Inline padding when the form is contained.
15
+ *
16
+ * @example
17
+ * <FormSection title="Personal Information" description="Enter your details.">
18
+ * <Input name="name" label="Name" />
19
+ * <Input name="email" type="email" label="Email" />
20
+ * </FormSection>
7
21
  */
8
22
  let {
9
23
  title,
@@ -53,12 +67,11 @@
53
67
  .FormSectionTitle {
54
68
  display: flex;
55
69
  flex-direction: column;
56
- background-color: var(--base);
57
70
  gap: var(--form-title-gap, var(--space-md));
58
71
  text-wrap: balance;
59
72
  }
60
73
  .FormSection:not(:first-child) {
61
- border-top: calc(var(--fcc) * var(--border-size)) var(--border-style) var(--border-color);
74
+ border-top: calc(var(--fcc) * var(--form-border-size)) var(--form-border-style) var(--form-border-color);
62
75
  }
63
76
  .FormSectionContent {
64
77
  display: grid;
@@ -67,27 +80,16 @@
67
80
  .FormSection.noTitle .FormSectionContent {
68
81
  grid-column: 1 / -1;
69
82
  }
70
- @media(max-width: 1280px) {
71
- .FormSection {
72
- padding: var(--space-xl);
73
- gap: var(--space-md);
74
- }
75
- .FormSectionTitle {
76
- gap: var(--space-xs);
77
- padding-block-end: var(--space-md);
78
- }
79
- }
80
- @media(max-width: 640px) {
83
+ @container (max-width: 800px) {
81
84
  .FormSection {
82
- padding: var(--space-xl);
83
- gap: var(--space-md);
85
+ padding-block: calc(var(--fcc) * var(--form-padding-block));
86
+ padding-inline: calc(var(--fcc) * var(--form-padding-inline));
87
+ gap: var(--form-section-gap, var(--space-md));
84
88
  }
85
89
  .FormSectionTitle {
86
- gap: var(--space-xs);
87
- padding-block-end: var(--space-md);
90
+ gap: var(--form-title-gap);
91
+ padding-block-end: var(--form-title-padding-block-end);
88
92
  }
89
- }
90
- @container (max-width: 800px) {
91
93
  .FormSectionTitle,
92
94
  .FormSectionContent {
93
95
  grid-column: 1 / -1;
@@ -9,6 +9,23 @@
9
9
  const imageCompression: ImageCompressionFn = (imageCompressionModule as unknown as { default?: ImageCompressionFn }).default ?? imageCompressionModule as unknown as ImageCompressionFn;
10
10
  import { addToast } from "../components/toasts.svelte.js";
11
11
 
12
+ /**
13
+ * @description
14
+ * An image upload component with client-side compression, progress indicator and
15
+ * optional remove button. Supports square, circle and banner display shapes.
16
+ * Uploads to a presigned URL via `PUT`.
17
+ *
18
+ * @cssprop --max-image-width -- Maximum width of the image preview (used in the grid layout).
19
+ *
20
+ * @example
21
+ * <ImageUpload
22
+ * uploadUrl="/api/upload"
23
+ * name="avatar"
24
+ * button="Upload"
25
+ * shape="circle"
26
+ * src={user.avatarUrl}
27
+ * />
28
+ */
12
29
  let {
13
30
  src,
14
31
  uploadUrl,
@@ -25,19 +42,33 @@
25
42
  onFile,
26
43
  children,
27
44
  }: {
45
+ /** Current image source URL. */
28
46
  src?: string | null;
47
+ /** The presigned URL to upload the image to. */
29
48
  uploadUrl: string;
49
+ /** The form field name for the resulting URL value. */
30
50
  name: string;
51
+ /** HTML id for the file input. */
31
52
  id?: string;
53
+ /** Title used in the alt text of the preview image. */
32
54
  title?: string;
55
+ /** Label for the upload button. */
33
56
  button: string;
57
+ /** Maximum file size in bytes before compression is applied. */
34
58
  maxSize?: number;
59
+ /** Maximum compressed file size in megabytes. */
35
60
  compressSize?: number;
61
+ /** Maximum compressed image width in pixels. */
36
62
  compressMaxWidth?: number;
63
+ /** Accepted MIME types for the file input. */
37
64
  accept?: string;
65
+ /** Whether a remove button is shown. */
38
66
  removable?: boolean;
67
+ /** Display shape of the image preview. */
39
68
  shape?: 'square' | 'circle' | 'banner';
69
+ /** Callback invoked with the selected file before upload. */
40
70
  onFile?: (file: File) => void;
71
+ /** Optional additional content rendered below the actions. */
41
72
  children?: Snippet;
42
73
  } = $props();
43
74
 
@@ -63,8 +94,6 @@
63
94
  useWebWorker: true,
64
95
  onProgress: (progress: number) => resizeProgress = progress,
65
96
  });
66
- console.log('compressedFile instanceof Blob', compressedFile instanceof Blob); // true
67
- console.log(`compressedFile size ${compressedFile.size / 1024 / 1024} MB`); // smaller than maxSizeMB
68
97
  uploadState = 'uploading';
69
98
 
70
99
  const response = await fetch(uploadUrl, {
@@ -79,15 +108,10 @@
79
108
  addToast({ content: 'Something went wrong. Please try again.', autoClose: 3000 })
80
109
  cancel();
81
110
  }
82
- //src = new URL(response.url).href.split('?')[0];
83
- console.log('setting', name, new URL(response.url).href.split('?')[0]);
84
111
  const imgUrl = new URL(response.url).href.split('?')[0];
85
112
  data.set(name, imgUrl);
86
113
  src = imgUrl;
87
- console.log('response', response, response.url);
88
- console.log('data', data.forEach((v, k) => console.log(k, v)));
89
114
  } catch(err) {
90
- console.error('error', err);
91
115
  addToast({ content: 'Something went wrong. Please try again.', autoClose: 3000 })
92
116
  cancel();
93
117
  }
@@ -141,7 +165,7 @@
141
165
  </script>
142
166
 
143
167
  <div class="ImageUpload {shape}">
144
- <div class="Image" class:hasSrc={!!src}>
168
+ <div class="Image">
145
169
  {#if src}
146
170
  <img src={src} alt="{title} logo" />
147
171
  {/if}
@@ -194,16 +218,16 @@
194
218
  display: grid;
195
219
  grid-template-columns: 1fr;
196
220
  align-items: center;
197
- gap: 1rem;
221
+ gap: var(--space-md);
198
222
  container-type: inline-size;
199
223
  }
200
224
  .Image {
201
- border: 1px solid var(--border-light);
225
+ border: var(--border-size-thin) solid var(--border-color-subtle);
202
226
  pointer-events: none;
203
227
  position: relative;
204
228
  display: none;
205
229
  }
206
- .Image.hasSrc {
230
+ .Image:has(img) {
207
231
  display: block;
208
232
  }
209
233
  .Image img {
@@ -229,32 +253,29 @@
229
253
  height: 100%;
230
254
  width: 100%;
231
255
  object-fit: contain;
232
- padding: 1rem;
256
+ padding: var(--space-md);
233
257
  text-align: center;
234
- border: 1px dashed var(--border-color);
235
- border-radius: var(--border-radius);
258
+ border: var(--border-size-thin) dashed var(--border-color);
259
+ border-radius: var(--border-radius-base);
236
260
  }
237
261
  .Loading {
238
262
  position: absolute;
239
- top: 0;
240
- left: 0;
241
- right: 0;
242
- bottom: 0;
263
+ inset: 0;
243
264
  z-index: 5;
244
- backdrop-filter: blur(5px);
245
- background: var(--bg-overlay);
265
+ backdrop-filter: var(--scrim-backdrop-filter);
266
+ background: var(--scrim-background);
246
267
  display: flex;
247
268
  align-items: center;
248
269
  justify-content: center;
249
270
  flex-direction: column;
250
- gap: 1rem;
251
- padding: 1rem;
271
+ gap: var(--space-md);
272
+ padding: var(--space-md);
252
273
  }
253
274
  input {
254
275
  display: none;
255
276
  }
256
277
  .error {
257
- margin-top: 0.75rem;
278
+ margin-block-start: var(--space-sm);
258
279
  }
259
280
  .ImageUpload.banner {
260
281
  grid-template-areas: "image";
@@ -266,7 +287,6 @@
266
287
  overflow: hidden;
267
288
  object-fit: cover;
268
289
  height: 15rem;
269
- aspect-ratio: initial !important;
270
290
  }
271
291
  .ImageUpload.banner .Image img {
272
292
  object-fit: cover;
@@ -276,28 +296,28 @@
276
296
  grid-area: image;
277
297
  flex-direction: column;
278
298
  justify-content: center;
279
- padding: 1rem;
299
+ padding: var(--space-md);
280
300
  }
281
301
  .ImageUpload.banner:hover .ImageUploadActions {
282
- background: var(--bg-overlay);
302
+ background: var(--scrim-background);
283
303
  }
284
304
  .ImageUploadActions {
285
305
  display: grid;
286
306
  grid-template-columns: 1fr 1fr;
287
- gap: 0.75rem;
307
+ gap: var(--space-sm);
288
308
  align-items: start;
289
309
  }
290
310
  @container (min-width: 320px) {
291
311
  .ImageUpload:not(.banner) {
292
312
  grid-template-columns: minmax(auto, var(--max-image-width)) 1fr;
293
- gap: 2rem;
313
+ gap: var(--space-xl);
294
314
  }
295
315
  .ImageUpload.banner .ImageUploadActions {
296
- padding: 3rem;
316
+ padding: var(--space-xxl);
297
317
  }
298
318
  .ImageUploadActions {
299
319
  grid-template-columns: 1fr;
300
- gap: 1rem;
320
+ gap: var(--space-md);
301
321
  }
302
322
  }
303
323
  </style>
@@ -1,18 +1,32 @@
1
1
  import { type Snippet } from 'svelte';
2
2
  type $$ComponentProps = {
3
+ /** Current image source URL. */
3
4
  src?: string | null;
5
+ /** The presigned URL to upload the image to. */
4
6
  uploadUrl: string;
7
+ /** The form field name for the resulting URL value. */
5
8
  name: string;
9
+ /** HTML id for the file input. */
6
10
  id?: string;
11
+ /** Title used in the alt text of the preview image. */
7
12
  title?: string;
13
+ /** Label for the upload button. */
8
14
  button: string;
15
+ /** Maximum file size in bytes before compression is applied. */
9
16
  maxSize?: number;
17
+ /** Maximum compressed file size in megabytes. */
10
18
  compressSize?: number;
19
+ /** Maximum compressed image width in pixels. */
11
20
  compressMaxWidth?: number;
21
+ /** Accepted MIME types for the file input. */
12
22
  accept?: string;
23
+ /** Whether a remove button is shown. */
13
24
  removable?: boolean;
25
+ /** Display shape of the image preview. */
14
26
  shape?: 'square' | 'circle' | 'banner';
27
+ /** Callback invoked with the selected file before upload. */
15
28
  onFile?: (file: File) => void;
29
+ /** Optional additional content rendered below the actions. */
16
30
  children?: Snippet;
17
31
  };
18
32
  declare const ImageUpload: import("svelte").Component<$$ComponentProps, {}, "">;
@@ -14,9 +14,19 @@
14
14
  import { ZodType } from "zod";
15
15
  import FieldContent from "./FieldContent.svelte";
16
16
  import { on } from "svelte/events";
17
- import { fade } from "svelte/transition";
18
- import Theme from "../components/Theme.svelte";
19
17
 
18
+ /**
19
+ * @description
20
+ * A versatile input component supporting all standard HTML input types. Integrates
21
+ * with the Lutra form system for validation, error display and field state tracking.
22
+ * Supports prefix/suffix content, copy-to-clipboard, password visibility toggle,
23
+ * range slider with value tooltip, and auto-growing file inputs.
24
+ *
25
+ * @example
26
+ * <Input name="email" type="email" label="Email" placeholder="you@example.com" />
27
+ * <Input name="age" type="range" min={0} max={100} unit="years" label="Age" />
28
+ * <Input name="token" type="password" viewable copyable label="API Token" />
29
+ */
20
30
  let {
21
31
  alt,
22
32
  autofocus,
@@ -116,7 +126,6 @@
116
126
  maxlength?: number;
117
127
  /** The minimum number of characters (as UTF-16 code units) the user can enter into the input. Valid for text, search, url, tel, email, and password. */
118
128
  minlength?: number;
119
- /** Allow multiple f
120
129
  /** The maximum value of the input element. Valid for date, month, week, time, datetime-local, number, and range. */
121
130
  max?: number
122
131
  /** The minimum value of the input element. Valid for date, month, week, time, datetime-local, number, and range. */
@@ -352,17 +361,12 @@
352
361
  {/if}
353
362
  <div class="RangeInput">
354
363
  {@render input()}
355
- {#if focused && min?.toString().length && max?.toString().length}
356
- <Theme theme="invert">
357
- <div
358
- class="RangeValue"
359
- in:fade={{ duration: 100 }}
360
- out:fade={{ duration: 100 }}
361
- style="left: {rangeValueLeft}%"
362
- >
363
- {value}{unit}
364
- </div>
365
- </Theme>
364
+ {#if min?.toString().length && max?.toString().length}
365
+ <span class="RangeTooltipAnchor" style="left: {rangeValueLeft}%">
366
+ <Tooltip tip="{value}{unit}" open={focused}>
367
+ <span></span>
368
+ </Tooltip>
369
+ </span>
366
370
  {/if}
367
371
  </div>
368
372
  {#if max?.toString().length}
@@ -396,7 +400,8 @@
396
400
  input:not([type="checkbox"]):not([type="radio"]) {
397
401
  border: none;
398
402
  flex-grow: 1;
399
- flex-shrink: 0;
403
+ flex-shrink: 1;
404
+ min-width: 0;
400
405
  }
401
406
  input:not([type="checkbox"]):not([type="radio"]):focus-visible,
402
407
  input:not([type="checkbox"]):not([type="radio"]):active {
@@ -404,29 +409,56 @@
404
409
  }
405
410
  input[type="range"] {
406
411
  display: block;
412
+ appearance: none;
413
+ background: transparent;
414
+ }
415
+ input[type="range"]::-webkit-slider-thumb {
416
+ -webkit-appearance: none;
417
+ appearance: none;
418
+ width: 1rem;
419
+ height: 1rem;
420
+ border-radius: 50%;
421
+ background: var(--focus-ring-color);
422
+ border: none;
423
+ margin-top: calc((0.25rem - 1rem) / 2);
424
+ }
425
+ input[type="range"]::-moz-range-thumb {
426
+ width: 1rem;
427
+ height: 1rem;
428
+ border-radius: 50%;
429
+ background: var(--focus-ring-color);
430
+ border: none;
407
431
  }
408
432
  .Range {
409
433
  display: flex;
410
- gap: 0.5em;
434
+ gap: var(--space-xs);
411
435
  align-items: center;
412
436
  }
413
- .RangeValue {
437
+ .RangeTooltipAnchor {
414
438
  position: absolute;
415
- top: 0;
416
- font-size: max(0.85em, 9px);
417
- color: var(--text-subtle);
418
- background: var(--bg-app);
419
- padding: 0.15em 0.35em;
420
- border-radius: var(--border-radius);
421
- transform: translate(-50%, -125%);
422
- border: var(--border);
423
- min-inline-size: 1em;
424
- box-shadow: 0 0.5rem 1rem var(--shadow);
425
- font-weight: 600;
439
+ top: 50%;
440
+ width: 0;
441
+ height: 0;
442
+ pointer-events: none;
443
+ }
444
+ .RangeTooltipAnchor :global(.TooltipContent) {
445
+ position-area: block-end center;
446
+ margin-block-end: 0;
447
+ margin-block-start: var(--tooltip-offset, var(--space-xxs));
448
+ }
449
+ input[type="range"]::-webkit-slider-runnable-track {
450
+ background: var(--border-color-subtle);
451
+ block-size: 0.25rem;
452
+ border-radius: var(--border-radius-base);
453
+ }
454
+ input[type="range"]::-moz-range-track {
455
+ background: var(--border-color-subtle);
456
+ block-size: 0.25rem;
457
+ border-radius: var(--border-radius-base);
426
458
  }
427
459
  .Range span {
428
- font-size: 0.75em;
429
- color: var(--text-subtle);
460
+ font-size: var(--font-size-xs);
461
+ color: var(--text-color-p-subtle);
430
462
  }
431
463
  .RangeInput {
432
464
  position: relative;
@@ -47,7 +47,6 @@ type $$ComponentProps = {
47
47
  maxlength?: number;
48
48
  /** The minimum number of characters (as UTF-16 code units) the user can enter into the input. Valid for text, search, url, tel, email, and password. */
49
49
  minlength?: number;
50
- /** Allow multiple f
51
50
  /** The maximum value of the input element. Valid for date, month, week, time, datetime-local, number, and range. */
52
51
  max?: number;
53
52
  /** The minimum value of the input element. Valid for date, month, week, time, datetime-local, number, and range. */
@@ -27,16 +27,16 @@
27
27
  display: flex;
28
28
  justify-content: flex-end;
29
29
  align-items: center;
30
- font-size: 0.8rem;
31
- font-weight: 500;
32
- color: var(--text-subtle);
30
+ font-size: var(--font-size-xs);
31
+ font-weight: var(--font-weight-normal);
32
+ color: var(--text-color-p-subtle);
33
33
  }
34
34
  .InputLength .Length {
35
35
  display: flex;
36
36
  align-items: center;
37
- gap: 0.25rem;
37
+ gap: var(--space-xxs);
38
38
  }
39
39
  .InputLength .Length.warn {
40
- color: var(--text-color-warn, light-dark(red, red));
40
+ color: var(--field-color-danger);
41
41
  }
42
42
  </style>
@@ -34,7 +34,7 @@
34
34
  </script>
35
35
 
36
36
  {#if label}
37
- <label for={id} class:hasTip={!!tip} class:hasHelp={!!help}>
37
+ <label for={id}>
38
38
  {#if typeof label === 'string'}
39
39
  <span>
40
40
  {label} {#if required}<span aria-hidden="true">*</span>{/if}
@@ -64,16 +64,16 @@
64
64
  gap: var(--form-label-gap, var(--space-md));
65
65
  align-items: center;
66
66
  }
67
- label.hasTip,
68
- label.hasHelp {
67
+ label:has(.Help),
68
+ label:has(:global(.ContextTip)) {
69
69
  grid-template-columns: 1fr auto;
70
70
  }
71
- label.hasHelp.hasTip {
71
+ label:has(.Help):has(:global(.ContextTip)) {
72
72
  grid-template-columns: 1fr auto auto;
73
73
  }
74
74
  label > span > span {
75
- font-weight: 600;
76
- color: var(--text-color-warn, light-dark(red, red));
75
+ font-weight: var(--font-weight-medium);
76
+ color: var(--field-color-danger);
77
77
  padding-inline: 0.175em;
78
78
  }
79
79
  .Help {
@@ -4,6 +4,20 @@
4
4
  import type { Snippet } from "svelte";
5
5
  import StringOrSnippet from "../util/StringOrSnippet.svelte";
6
6
 
7
+ /**
8
+ * @description
9
+ * A dual-theme logo upload component that provides side-by-side upload fields for
10
+ * light and dark theme logos. Each side renders an `ImageUpload` within its
11
+ * respective `Theme` wrapper so the preview matches the actual display context.
12
+ *
13
+ * @example
14
+ * <LogoUpload
15
+ * lightLogoUploadUrl="/api/upload/logo-light"
16
+ * darkLogoUploadUrl="/api/upload/logo-dark"
17
+ * lightLogoSrc={org.logoLightUrl}
18
+ * darkLogoSrc={org.logoDarkUrl}
19
+ * />
20
+ */
7
21
  let {
8
22
  lightLogoInputName = 'logo_light_url',
9
23
  darkLogoInputName = 'logo_dark_url',
@@ -77,9 +91,9 @@
77
91
 
78
92
  <style>
79
93
  .LogoUpload {
80
- border: var(--border);
94
+ border: var(--field-border);
81
95
  overflow: hidden;
82
- border-radius: var(--border-radius);
96
+ border-radius: var(--border-radius-base);
83
97
  margin: 0;
84
98
  padding: 0;
85
99
  list-style: none;
@@ -88,28 +102,28 @@
88
102
  container-type: inline-size;
89
103
  }
90
104
  .Content {
91
- background: var(--bg-app);
105
+ background: var(--background-main);
92
106
  }
93
107
  .Info {
94
- padding: 1rem 1.5rem;
108
+ padding: var(--space-md) var(--space-lg);
95
109
  display: grid;
96
110
  grid-template-columns: 1fr;
97
- gap: 1rem;
111
+ gap: var(--space-md);
98
112
  }
99
113
  .Content :global(h5) {
100
- margin-bottom: 0 !important;
101
- padding: 1rem 1rem 1rem 1rem;
114
+ margin-block-end: 0;
115
+ padding: var(--space-md);
102
116
  }
103
117
  .Content :global(.Rows) {
104
- padding: 0 1rem 1rem 1rem;
118
+ padding: 0 var(--space-md) var(--space-md) var(--space-md);
105
119
  }
106
120
  @container (min-width: 500px) {
107
121
  .LogoUpload {
108
122
  grid-template-columns: 1fr 1fr;
109
123
  }
110
124
  .Content :global(h5) {
111
- margin-bottom: 0 !important;
112
- padding: 1rem 1.5rem;
125
+ margin-block-end: 0;
126
+ padding: var(--space-md) var(--space-lg);
113
127
  }
114
128
  }
115
129
  </style>