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
@@ -1,26 +1,36 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from "svelte";
3
+
4
+ /**
5
+ * @description
6
+ * A wrapper that enforces an aspect ratio on its children. Accepts numeric, ratio string, or colon-separated formats.
7
+ * @cssprop --aspect-ratio - The aspect ratio of the container. (Default: 16 / 9)
8
+ * @example
9
+ * <AspectRatio ratio="4:3">
10
+ * <img src="photo.jpg" alt="Photo" style="width: 100%; height: 100%; object-fit: cover;" />
11
+ * </AspectRatio>
12
+ */
3
13
  let {
4
- ratio = 16 / 9,
14
+ ratio = "16 / 9",
5
15
  children
6
16
  }: {
7
- ratio?: number;
17
+ /** Aspect ratio as a number (e.g. 1.778), a ratio string (e.g. "16 / 9"), or colon-separated (e.g. "16:9"). */
18
+ ratio?: number | string;
8
19
  children: Snippet;
9
20
  } = $props();
21
+
22
+ const cssRatio = $derived(
23
+ typeof ratio === "string" ? ratio.replace(":", " / ") : ratio
24
+ );
10
25
  </script>
11
26
 
12
- <div class="AspectRatio" style="--aspect-ratio: {ratio}">
27
+ <div class="AspectRatio" style="--aspect-ratio: {cssRatio}">
13
28
  {@render children()}
14
29
  </div>
15
30
 
16
31
  <style>
17
- @property --aspect-ratio {
18
- syntax: "<number-percentage> / <number-percentage>";
19
- inherits: false;
20
- initial-value: 16 / 9;
21
- }
22
32
  .AspectRatio {
23
- aspect-ratio: var(--aspect-ratio);
33
+ aspect-ratio: var(--aspect-ratio, 16 / 9);
24
34
  }
25
35
  </style>
26
36
 
@@ -1,6 +1,7 @@
1
1
  import type { Snippet } from "svelte";
2
2
  type $$ComponentProps = {
3
- ratio?: number;
3
+ /** Aspect ratio as a number (e.g. 1.778), a ratio string (e.g. "16 / 9"), or colon-separated (e.g. "16:9"). */
4
+ ratio?: number | string;
4
5
  children: Snippet;
5
6
  };
6
7
  declare const AspectRatio: import("svelte").Component<$$ComponentProps, {}, "">;
@@ -9,6 +9,9 @@
9
9
  * You can pick from three crop styles: circle, square, or rounded. The color of the placeholder is based on the name, making each visually distinct from each other.
10
10
  * @cssprop --border-radius - The border radius of the avatar when cropped as rounded.
11
11
  * @cssprop --mask-image - Custom mask image to use for the avatar.
12
+ * @cssprop --size - The size of the avatar. (Default: 3rem)
13
+ * @cssprop --user-color - The background color of the avatar. (Default: #666666)
14
+ * @cssprop --text-color - The text color of the avatar. (Default: rgba(0,0,0,0.85))
12
15
  * @example
13
16
  * <p>With picture:</p>
14
17
  * <Avatar name="Auth70" shape="rounded" src="https://avatars.githubusercontent.com/u/122825113?s=200&v=4" --size="4rem" />
@@ -84,11 +87,6 @@
84
87
  .Avatar.circle { border-radius: 50%; }
85
88
  .Avatar.square { border-radius: 0; }
86
89
  .Avatar.rounded { border-radius: var(--border-radius); }
87
- .Avatar img {
88
- block-size: 100%;
89
- inline-size: 100%;
90
- object-fit: cover;
91
- }
92
90
  .Avatar .Placeholder {
93
91
  display: flex;
94
92
  align-items: center;
@@ -97,9 +95,8 @@
97
95
  inline-size: 100%;
98
96
  background-color: var(--user-color);
99
97
  color: var(--text-color);
100
- user-select: none;
101
- font-size: 1rem;
102
- font-weight: 600;
98
+ font-size: calc(var(--size, 3rem) * 0.4);
99
+ font-weight: var(--font-weight-medium);
103
100
  user-select: none;
104
101
  }
105
102
  </style>
@@ -1,9 +1,22 @@
1
1
  <script lang="ts">
2
+ /**
3
+ * @description
4
+ * A close button rendered as an X icon. Can be absolutely positioned in a corner of its containing element.
5
+ * @cssprop --close-padding - Padding around the icon. (Default: var(--space-xs))
6
+ * @cssprop --close-icon-size - Size of the close icon. (Default: max(1.5rem, 1rem))
7
+ * @example
8
+ * <div style="position: relative; padding: 2rem; border: 1px solid gray;">
9
+ * <Close position="top right" onclick={() => alert('closed')} />
10
+ * <p>Content with a close button</p>
11
+ * </div>
12
+ */
2
13
  let {
3
14
  onclick,
4
15
  position
5
16
  }: {
17
+ /** Callback when the close button is clicked. */
6
18
  onclick?: (e: MouseEvent) => void;
19
+ /** Absolute position within the parent container. */
7
20
  position?: "top left" | "top right" | "bottom left" | "bottom right";
8
21
  } = $props();
9
22
  </script>
@@ -18,10 +31,9 @@
18
31
  <style>
19
32
  .Close {
20
33
  cursor: pointer;
21
- padding: var(--close-padding, 0.5rem);
34
+ padding: var(--close-padding, var(--space-xs));
22
35
  border-radius: 50%;
23
- color: var(--text-color, light-dark(black, white));
24
- cursor: pointer;
36
+ color: var(--text-color-p);
25
37
  pointer-events: auto;
26
38
  border: none;
27
39
  }
@@ -40,37 +52,22 @@
40
52
  svg {
41
53
  display: block;
42
54
  margin: 0;
43
- width: max(1.5rem, 16px);
44
- height: max(1.5rem, 16px);
55
+ width: var(--close-icon-size, max(1.5rem, 1rem));
56
+ height: var(--close-icon-size, max(1.5rem, 1rem));
45
57
  }
46
58
 
47
59
  .Close:hover {
48
- color: var(--text-color-subtle, light-dark(rgba(0, 0, 0, 0.5), rgba(255, 255, 255, 0.5)));
49
- }
50
-
51
- .Close.top {
52
- position: absolute;
53
- top: 0;
54
- right: 0;
55
- z-index: 100;
60
+ color: var(--text-color-p-subtle);
56
61
  }
57
62
 
63
+ .Close.top,
58
64
  .Close.bottom {
59
65
  position: absolute;
60
- bottom: 0;
61
- right: 0;
62
- }
63
-
64
- .Close.left {
65
- position: absolute;
66
- top: 0;
67
- left: 0;
68
- right: auto;
66
+ z-index: 100;
69
67
  }
70
68
 
71
- .Close.right {
72
- position: absolute;
73
- top: 0;
74
- right: 0;
75
- }
69
+ .Close.top.right { inset: 0 0 auto auto; }
70
+ .Close.top.left { inset: 0 auto auto 0; }
71
+ .Close.bottom.right { inset: auto 0 0 auto; }
72
+ .Close.bottom.left { inset: auto auto 0 0; }
76
73
  </style>
@@ -1,5 +1,7 @@
1
1
  type $$ComponentProps = {
2
+ /** Callback when the close button is clicked. */
2
3
  onclick?: (e: MouseEvent) => void;
4
+ /** Absolute position within the parent container. */
3
5
  position?: "top left" | "top right" | "bottom left" | "bottom right";
4
6
  };
5
7
  declare const Close: import("svelte").Component<$$ComponentProps, {}, "">;
@@ -18,7 +18,7 @@
18
18
  </script>
19
19
 
20
20
  <Tooltip {tip}>
21
- <a href="#contexttip" onclick={(e) => { e.preventDefault(); e.stopPropagation(); }}>
21
+ <a class="ContextTip" href="#contexttip" onclick={(e) => { e.preventDefault(); e.stopPropagation(); }}>
22
22
  <Icon icon={Help} --icon-width="16px" --icon-height="16px" --cursor="help" --vertical-align="baseline" />
23
23
  </a>
24
24
  </Tooltip>
@@ -35,7 +35,8 @@
35
35
  justify-content: center;
36
36
  }
37
37
  a:focus-visible {
38
- color: var(--focus-ring-color);
38
+ outline: var(--focus-ring);
39
39
  outline-offset: 0px;
40
+ color: var(--focus-ring-color);
40
41
  }
41
42
  </style>
@@ -2,8 +2,14 @@
2
2
  import type { Snippet } from "svelte";
3
3
 
4
4
  /**
5
+ * @description
5
6
  * A simple dialog component using the native `<dialog>` element.
6
7
  * For more features (buttons, scrim control, etc.), use `<Modal>` instead.
8
+ * Uses modal tokens for styling (background, border, padding, shadow, etc.).
9
+ * @example
10
+ * <Dialog bind:open={showDialog} title="Confirm">
11
+ * <p>Are you sure?</p>
12
+ * </Dialog>
7
13
  */
8
14
  let {
9
15
  open = $bindable(false),
@@ -81,6 +87,38 @@
81
87
  .Dialog[open] {
82
88
  display: grid;
83
89
  grid-template-rows: auto 1fr;
90
+ opacity: 1;
91
+ translate: 0 0;
92
+ transition:
93
+ opacity var(--transition-duration-fast) ease-out,
94
+ translate var(--transition-duration-fast) ease-out,
95
+ display var(--transition-duration-fast) allow-discrete,
96
+ overlay var(--transition-duration-fast) allow-discrete;
97
+ }
98
+
99
+ .Dialog[open]::backdrop {
100
+ opacity: 1;
101
+ transition:
102
+ opacity var(--transition-duration-fast) ease-out,
103
+ display var(--transition-duration-fast) allow-discrete,
104
+ overlay var(--transition-duration-fast) allow-discrete;
105
+ }
106
+
107
+ @starting-style {
108
+ .Dialog[open] {
109
+ opacity: 0;
110
+ translate: 0 var(--space-xs);
111
+ }
112
+ .Dialog[open]::backdrop {
113
+ opacity: 0;
114
+ }
115
+ }
116
+
117
+ @media (prefers-reduced-motion: reduce) {
118
+ .Dialog,
119
+ .Dialog::backdrop {
120
+ transition: none;
121
+ }
84
122
  }
85
123
 
86
124
  .DialogHeader {
@@ -4,8 +4,8 @@
4
4
  /**
5
5
  * @description
6
6
  * A component that displays an icon. It can be an image url or a component. The icon will be centered in the container.
7
- * @cssprop --icon-width - The width of the icon. (Default: var(--font-size, 1em))
8
- * @cssprop --icon-height - The height of the icon. (Default: var(--font-size, 1em))
7
+ * @cssprop --icon-width - The width of the icon. (Default: font-size or 1rem)
8
+ * @cssprop --icon-height - The height of the icon. (Default: font-size or 1rem)
9
9
  * @cssprop --icon-color - The color of the icon. (Default: var(--text-color, currentColor))
10
10
  * @example
11
11
  * <script>
@@ -9,7 +9,7 @@
9
9
  * A component that displays an icon with a possible label. It can also have a click event.
10
10
  * The button has a padding of 0.75em by default to make it easier to tap on mobile devices. The padding can be changed using the `--padding` CSS variable.
11
11
  * Icon and text color will be inherited from the parent element.
12
- * @cssprop --padding - The padding of the icon button. (Default: 0.75em)
12
+ * @cssprop --padding - The padding of the icon button. (Default: var(--space-sm))
13
13
  * @example
14
14
  * <script>
15
15
  * import Copy from 'lutra/Icons/Copy.svelte';
@@ -71,10 +71,11 @@
71
71
  color: inherit;
72
72
  opacity: 1;
73
73
  background-color: var(--field-background-color, transparent);
74
- transition: background-color 0.04s;
74
+ transition: background-color var(--transition-duration-fast);
75
75
  border-radius: var(--field-border-radius);
76
76
  }
77
- .IconButton:hover {
77
+ .IconButton:hover,
78
+ .IconButton:focus-visible {
78
79
  background-color: var(--menu-background-color-hover);
79
80
  }
80
81
  .IconButton:active {
@@ -84,37 +85,24 @@
84
85
  border: none;
85
86
  background: none;
86
87
  cursor: pointer;
87
- color: var(--text-color-p, light-dark(black, white));
88
88
  }
89
89
  .IconMask {
90
90
  height: 100%;
91
- padding-inline: calc(var(--padding, 0.75em) * 0.8);
92
- padding-block: calc(var(--padding, 0.75em) * 0.8);
91
+ padding-inline: calc(var(--padding, var(--space-sm)) * 0.8);
92
+ padding-block: calc(var(--padding, var(--space-sm)) * 0.8);
93
93
  display: inline-grid;
94
- gap: 0.5rem;
94
+ gap: var(--space-xs);
95
95
  grid-template: "icon";
96
96
  align-items: center;
97
97
  }
98
98
  .IconMask.mask {
99
- -webkit-mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 0), rgba(0, 0, 0, 1) 35%, rgba(0, 0, 0, 1) 65%, rgba(0, 0, 0, 0));
100
- mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 0), rgba(0, 0, 0, 1) 35%, rgba(0, 0, 0, 1) 65%, rgba(0, 0, 0, 0));
99
+ -webkit-mask-image: linear-gradient(to bottom, transparent, black 35%, black 65%, transparent);
100
+ mask-image: linear-gradient(to bottom, transparent, black 35%, black 65%, transparent);
101
101
  }
102
102
  .IconContent {
103
103
  grid-area: icon;
104
104
  display: inline-flex;
105
- gap: 0.5rem;
105
+ gap: var(--space-xs);
106
106
  align-items: center;
107
107
  }
108
- @media(max-width: 960px) {
109
- .IconMask {
110
- padding-inline: calc(var(--padding, 0.75em) * 0.75);
111
- padding-block: calc(var(--padding, 0.75em) * 0.75);
112
- }
113
- }
114
- @media(max-width: 320px) {
115
- .IconMask {
116
- padding-inline: calc(var(--padding, 0.75em) * 0.6);
117
- padding-block: calc(var(--padding, 0.75em) * 0.6);
118
- }
119
- }
120
108
  </style>
@@ -9,7 +9,7 @@
9
9
  * <Image aspectRatio="16:9" fit="cover" src="https://images.unsplash.com/photo-1712337646541-d0c6f85447f8" alt="An example image" />
10
10
  */
11
11
 
12
- import { browser } from "$app/environment";
12
+ import { BROWSER } from 'esm-env';
13
13
  import { decode } from "blurhash";
14
14
  import { fade } from "svelte/transition";
15
15
 
@@ -94,7 +94,7 @@
94
94
  }
95
95
 
96
96
  let decoded = $state(false);
97
- let loaded = $state(browser ? false : true);
97
+ let loaded = $state(BROWSER ? false : true);
98
98
 
99
99
  const onload = () => {
100
100
  loaded = true;
@@ -43,7 +43,7 @@
43
43
  let _label = $derived(isSet ? StatusColors[color as StatusColor] : label ? label : 'status');
44
44
  </script>
45
45
 
46
- <span role="status" aria-label="{_label}" class="Indicator {color} {motion}" style="--bgColor: {isSet ? 'var(--status-'+color+')' : color};"></span>
46
+ <span role="status" aria-label="{_label}" class="Indicator {color} {motion}" style="--bgColor: {isSet ? 'var(--status-'+color+'-color)' : color};"></span>
47
47
 
48
48
  <style>
49
49
  .Indicator {
@@ -242,6 +242,7 @@
242
242
  --mask: radial-gradient(circle, rgba(0, 0, 0, 0) 20%, rgba(0, 0, 0, 0) 45%, black 50%, black 100%);
243
243
  -webkit-mask-image: var(--mask);
244
244
  mask-image: var(--mask);
245
+ mask-size: 100% 100%;
245
246
  filter: drop-shadow(0 0 calc(var(--isize) * 0.05) var(--bgColor));
246
247
  }
247
248
  .Indicator.spin::after,
@@ -1,6 +1,19 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from "svelte";
3
3
 
4
+ /**
5
+ * @description
6
+ * Negates the padding of a parent container by applying negative margins, allowing child content (e.g. images) to span edge-to-edge.
7
+ * Reads `--inset-block` and `--inset-inline` from the parent context (typically set by MenuItem or similar).
8
+ * @cssprop --inset-block - The block inset to negate. (Default: 0)
9
+ * @cssprop --inset-inline - The inline inset to negate. (Default: 0)
10
+ * @example
11
+ * <div style="padding: 1rem; --inset-block: 1rem; --inset-inline: 1rem;">
12
+ * <Inset>
13
+ * <img src="banner.jpg" alt="Full-bleed banner" />
14
+ * </Inset>
15
+ * </div>
16
+ */
4
17
  let {
5
18
  children,
6
19
  }: {
@@ -1,22 +1,26 @@
1
1
  <script lang="ts">
2
- import type { Snippet } from "svelte";
2
+ import { getContext, type Snippet } from "svelte";
3
3
  import "../css/lutra.css";
4
4
  import Theme from "./Theme.svelte";
5
5
  import ToastContainer from "./ToastContainer.svelte";
6
+ import { getContextItem, LutraContext, type LutraContextTypeMap, type LutraTheme } from "../types.js";
7
+
8
+ const lutra = getContext<() => LutraContextTypeMap>('lutra');
6
9
 
7
10
  /**
8
11
  * Default layout component that imports styles and provides theming.
9
12
  * Includes ToastContainer for toast notifications.
10
13
  */
11
14
  let {
12
- theme,
15
+ theme = lutra()?.[LutraContext.Theme]?.() ?? "system",
13
16
  children,
14
17
  }: {
15
18
  /** The theme to use. Leave empty for auto-detection. */
16
- theme?: 'light' | 'dark' | undefined;
19
+ theme?: LutraTheme;
17
20
  /** The content to display. */
18
21
  children: Snippet;
19
22
  } = $props();
23
+
20
24
  </script>
21
25
 
22
26
  <Theme theme={theme}>
@@ -1,8 +1,9 @@
1
- import type { Snippet } from "svelte";
1
+ import { type Snippet } from "svelte";
2
2
  import "../css/lutra.css";
3
+ import { type LutraTheme } from "../types.js";
3
4
  type $$ComponentProps = {
4
5
  /** The theme to use. Leave empty for auto-detection. */
5
- theme?: 'light' | 'dark' | undefined;
6
+ theme?: LutraTheme;
6
7
  /** The content to display. */
7
8
  children: Snippet;
8
9
  };
@@ -7,8 +7,18 @@
7
7
  import { arrowNavigation, matchOnType } from "../util/keyboard.svelte.js";
8
8
 
9
9
  /**
10
- * A dropdown menu using the base Popover component.
11
- * Handles menu-specific keyboard navigation and item rendering.
10
+ * @description
11
+ * A dropdown menu built on the Popover component. Handles keyboard navigation (arrow keys, type-ahead)
12
+ * and renders MenuItem entries. Uses menu tokens for styling via the Popover's CSS variable overrides.
13
+ * @example
14
+ * <MenuDropdown
15
+ * trigger="Options"
16
+ * items={[
17
+ * { type: 'item', text: 'Edit', onclick: () => {} },
18
+ * { type: 'divider' },
19
+ * { type: 'item', text: 'Delete', color: 'alert', onclick: () => {} },
20
+ * ]}
21
+ * />
12
22
  */
13
23
  let {
14
24
  open = $bindable(false),
@@ -3,6 +3,17 @@
3
3
  import MenuItemContent from "./MenuItemContent.svelte";
4
4
  import type { MenuItem as Item } from "./MenuTypes.js";
5
5
 
6
+ /**
7
+ * @description
8
+ * A single menu item within a menu list. Supports item, header, text, and divider types.
9
+ * Items can be links, buttons, or custom rendered content. Keyboard navigation is handled by the parent MenuDropdown.
10
+ * @cssprop --menu-item-font-size - Font size of the menu item. (Default: var(--font-size-sm))
11
+ * @cssprop --menu-item-padding-block - Block padding of the menu item. (Default: var(--space-xs))
12
+ * @cssprop --menu-item-padding-inline - Inline padding of the menu item. (Default: var(--space-md))
13
+ * @cssprop --menu-item-gap - Gap between icon and text within the item. (Default: var(--space-sm))
14
+ * @cssprop --menu-item-margin - Top margin of the first item. (Default: var(--space-xs))
15
+ * @cssprop --menu-shortcut-font-size - Font size of the keyboard shortcut label. (Default: max(0.75em, 9px))
16
+ */
6
17
  let {
7
18
  item,
8
19
  index,
@@ -11,11 +22,17 @@
11
22
  keyboardHasFocus,
12
23
  shape = 'default',
13
24
  }: {
25
+ /** The menu item data object. */
14
26
  item: Item;
27
+ /** The index of this item in the menu list. */
15
28
  index: number;
29
+ /** Callback when the mouse enters the item. */
16
30
  onmouseover?: (e: MouseEvent, item: Item, index: number) => void;
31
+ /** Callback when the item is selected. */
17
32
  onselect?: (item: Item, index: number) => void;
33
+ /** Whether keyboard navigation is active, suppressing mouse hover styles. */
18
34
  keyboardHasFocus?: boolean;
35
+ /** The border-radius shape of the item. */
19
36
  shape?: 'default' | 'rounded' | 'pill';
20
37
  } = $props();
21
38
 
@@ -104,20 +121,19 @@
104
121
  li .Item,
105
122
  li .Header,
106
123
  li .Text {
107
- font-size: var(--font-size, 0.9em);
124
+ font-size: var(--menu-item-font-size, var(--font-size-sm));
108
125
  text-align: left;
109
- padding-block: 0.5rem;
110
- padding-inline: 1rem;
126
+ padding-block: var(--menu-item-padding-block, var(--space-xs));
127
+ padding-inline: var(--menu-item-padding-inline, var(--space-md));
111
128
  display: inline-flex;
112
129
  align-items: center;
113
130
  justify-content: space-between;
114
131
  width: 100%;
115
- color: inherit;
116
132
  text-decoration: none;
117
133
  color: var(--color);
118
- --inset-block: 0.5rem;
119
- --inset-inline: 1rem;
120
- border-radius: none;
134
+ --inset-block: var(--menu-item-padding-block, var(--space-xs));
135
+ --inset-inline: var(--menu-item-padding-inline, var(--space-md));
136
+ border-radius: 0;
121
137
  white-space: nowrap;
122
138
  }
123
139
 
@@ -128,7 +144,7 @@
128
144
  }
129
145
 
130
146
  li .Header {
131
- font-weight: 600;
147
+ font-weight: var(--font-weight-medium);
132
148
  }
133
149
 
134
150
  li:not(.keyboardHasFocus) .Item:not(.Custom):hover,
@@ -146,7 +162,7 @@
146
162
  }
147
163
 
148
164
  li .Item span.Shortcut {
149
- font-size: max(0.75em, 9px);
165
+ font-size: var(--menu-shortcut-font-size, max(0.75em, 9px));
150
166
  text-align: right;
151
167
  color: var(--menu-text-color-subtle);
152
168
  white-space: nowrap;
@@ -157,23 +173,23 @@
157
173
  }
158
174
 
159
175
  li.divider {
160
- padding-block: 0.5rem;
176
+ padding-block: var(--menu-item-padding-block, var(--space-xs));
161
177
  }
162
178
 
163
179
  hr {
164
180
  display: block;
165
181
  border: none;
166
182
  margin: 0;
167
- border-top: 1px solid var(--menu-border-color);
183
+ border-top: var(--menu-border-size) var(--menu-border-style) var(--menu-border-color);
168
184
  }
169
185
 
170
186
  li:first-child[data-type="item"] {
171
- margin-block-start: var(--menu-item-margin, 0.5rem);
187
+ margin-block-start: var(--menu-item-margin, var(--space-xs));
172
188
  }
173
189
 
174
- @media (pointer: none) {
190
+ @media (pointer: coarse) {
175
191
  li .Item span.Shortcut {
176
192
  display: none;
177
- }
193
+ }
178
194
  }
179
195
  </style>
@@ -1,10 +1,16 @@
1
1
  import type { MenuItem as Item } from "./MenuTypes.js";
2
2
  type $$ComponentProps = {
3
+ /** The menu item data object. */
3
4
  item: Item;
5
+ /** The index of this item in the menu list. */
4
6
  index: number;
7
+ /** Callback when the mouse enters the item. */
5
8
  onmouseover?: (e: MouseEvent, item: Item, index: number) => void;
9
+ /** Callback when the item is selected. */
6
10
  onselect?: (item: Item, index: number) => void;
11
+ /** Whether keyboard navigation is active, suppressing mouse hover styles. */
7
12
  keyboardHasFocus?: boolean;
13
+ /** The border-radius shape of the item. */
8
14
  shape?: 'default' | 'rounded' | 'pill';
9
15
  };
10
16
  declare const MenuItem: import("svelte").Component<$$ComponentProps, {}, "">;
@@ -5,30 +5,20 @@
5
5
  import type { ModalButton } from "./ModalTypes.js";
6
6
 
7
7
  /**
8
- * A modal component using the native `<dialog>` element.
9
- *
10
- * **Pattern A: Trigger-based**
11
- * ```svelte
12
- * <Modal {trigger} {content} />
13
- * ```
14
- *
15
- * **Pattern B: Controlled state**
16
- * ```svelte
8
+ * @description
9
+ * A modal component using the native `<dialog>` element. Supports trigger-based, controlled, and auto-show patterns.
10
+ * Uses modal tokens for styling. The `contained` prop can be set via context.
11
+ * @cssprop --modal-width - Width of the modal. (Default: fit-content)
12
+ * @cssprop --modal-min-width - Minimum width. (Default: auto)
13
+ * @cssprop --modal-max-width - Maximum width. (Default: 40rem)
14
+ * @cssprop --modal-max-height - Maximum height. (Default: 80svh)
15
+ * @example
17
16
  * <Modal bind:open={showModal}>
18
17
  * {#snippet content(close)}
19
18
  * <p>Modal content</p>
19
+ * <button onclick={close}>Close</button>
20
20
  * {/snippet}
21
21
  * </Modal>
22
- * ```
23
- *
24
- * **Pattern C: Auto-show (no trigger)**
25
- * ```svelte
26
- * {#if shouldShow}
27
- * <Modal open onclose={() => shouldShow = false}>
28
- * {#snippet content(close)}...{/snippet}
29
- * </Modal>
30
- * {/if}
31
- * ```
32
22
  */
33
23
  let {
34
24
  open = $bindable(false),
@@ -241,6 +231,31 @@
241
231
  dialog.Modal[open] {
242
232
  display: grid;
243
233
  grid-template-rows: 1fr auto;
234
+ opacity: 1;
235
+ translate: 0 0;
236
+ transition:
237
+ opacity var(--transition-duration-fast) ease-out,
238
+ translate var(--transition-duration-fast) ease-out,
239
+ display var(--transition-duration-fast) allow-discrete,
240
+ overlay var(--transition-duration-fast) allow-discrete;
241
+ }
242
+
243
+ dialog.Modal[open]::backdrop {
244
+ opacity: 1;
245
+ transition:
246
+ opacity var(--transition-duration-fast) ease-out,
247
+ display var(--transition-duration-fast) allow-discrete,
248
+ overlay var(--transition-duration-fast) allow-discrete;
249
+ }
250
+
251
+ @starting-style {
252
+ dialog.Modal[open] {
253
+ opacity: 0;
254
+ translate: 0 var(--space-xs);
255
+ }
256
+ dialog.Modal[open]::backdrop {
257
+ opacity: 0;
258
+ }
244
259
  }
245
260
 
246
261
  dialog.Modal.contained {
@@ -306,7 +321,8 @@
306
321
  }
307
322
 
308
323
  @media (prefers-reduced-motion: reduce) {
309
- dialog.Modal {
324
+ dialog.Modal,
325
+ dialog.Modal::backdrop {
310
326
  transition: none;
311
327
  }
312
328
  }