lutra 0.1.34 → 0.1.36

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.
@@ -14,6 +14,7 @@
14
14
  open = $bindable(false),
15
15
  items,
16
16
  trigger: triggerProp,
17
+ align = "start",
17
18
  width,
18
19
  maxWidth,
19
20
  }: {
@@ -23,6 +24,8 @@
23
24
  items: Item[];
24
25
  /** The trigger snippet or string */
25
26
  trigger: string | Snippet<[{ toggle: () => void; isOpen: boolean; triggerAttrs?: Record<string, string> }]>;
27
+ /** Horizontal alignment: "start" (default), "center", or "end" */
28
+ align?: "start" | "center" | "end";
26
29
  /** The width of the menu */
27
30
  width?: string;
28
31
  /** The max width of the menu */
@@ -92,54 +95,60 @@
92
95
  <UiContent>
93
96
  <Popover
94
97
  bind:open
95
- position="block-end span-inline-end"
96
- fallbacks={["flip-block", "flip-inline", "flip-block flip-inline"]}
97
- offset="0.5rem 0 0 0"
98
+ position="block-end"
99
+ {align}
100
+ fallbacks={["flip-block", "flip-inline"]}
101
+ offset="0.5rem"
98
102
  {width}
99
- {maxWidth}
103
+ maxWidth={maxWidth || "50ch"}
104
+ maxHeight="calc(50vh - 2rem)"
100
105
  onopen={handleOpen}
101
106
  onclose={handleClose}
102
107
  class="MenuDropdownPopover"
108
+ --popover-background="var(--menu-background-color)"
109
+ --popover-border="var(--menu-border-size) var(--menu-border-style) var(--menu-border-color)"
110
+ --popover-border-radius="var(--menu-border-radius)"
111
+ --popover-shadow="0 0.5rem 1rem var(--shadow-color)"
103
112
  >
104
- {#snippet trigger(popoverArgs)}
105
- <span class="MenuDropdownTrigger" style={popoverArgs.anchorAttrs.style}>
106
- {#if isStringTrigger}
107
- <button
108
- type="button"
109
- class="button"
110
- {...popoverArgs.triggerAttrs}
111
- aria-haspopup="menu"
112
- >
113
- {triggerProp}
114
- </button>
115
- {:else if typeof triggerProp !== "string"}
116
- {@render triggerProp({
117
- toggle: popoverArgs.toggle,
118
- isOpen: popoverArgs.isOpen,
119
- triggerAttrs: popoverArgs.triggerAttrs,
120
- })}
121
- {/if}
122
- </span>
123
- {/snippet}
124
-
125
- <div
126
- class="MenuDropdownContent"
127
- class:scrollable
128
- role="menu"
129
- bind:this={menuEl}
130
- >
131
- <ul>
132
- {#each items as item, index}
133
- <MenuItem
134
- {keyboardHasFocus}
135
- onmouseover={handleMouseover}
136
- onselect={handleSelect}
137
- {item}
138
- {index}
139
- />
140
- {/each}
141
- </ul>
142
- </div>
113
+ {#snippet trigger(popoverArgs)}
114
+ <span class="MenuDropdownTrigger" style={popoverArgs.anchorAttrs.style}>
115
+ {#if isStringTrigger}
116
+ <button
117
+ type="button"
118
+ class="button"
119
+ {...popoverArgs.triggerAttrs}
120
+ aria-haspopup="menu"
121
+ >
122
+ {triggerProp}
123
+ </button>
124
+ {:else if typeof triggerProp !== "string"}
125
+ {@render triggerProp({
126
+ toggle: popoverArgs.toggle,
127
+ isOpen: popoverArgs.isOpen,
128
+ triggerAttrs: popoverArgs.triggerAttrs,
129
+ })}
130
+ {/if}
131
+ </span>
132
+ {/snippet}
133
+
134
+ <div
135
+ class="MenuDropdownContent"
136
+ class:scrollable
137
+ role="menu"
138
+ bind:this={menuEl}
139
+ >
140
+ <ul>
141
+ {#each items as item, index}
142
+ <MenuItem
143
+ {keyboardHasFocus}
144
+ onmouseover={handleMouseover}
145
+ onselect={handleSelect}
146
+ {item}
147
+ {index}
148
+ />
149
+ {/each}
150
+ </ul>
151
+ </div>
143
152
  </Popover>
144
153
  </UiContent>
145
154
 
@@ -148,18 +157,7 @@
148
157
  display: inline-block;
149
158
  }
150
159
 
151
- :global(.MenuDropdownPopover) {
152
- /* Override Popover defaults for menu styling */
153
- border: var(--menu-border-size) var(--menu-border-style) var(--menu-border-color);
154
- border-radius: var(--menu-border-radius);
155
- box-shadow: 0 0.5rem 1rem var(--shadow-color);
156
- background-color: var(--menu-background-color);
157
- }
158
-
159
160
  .MenuDropdownContent {
160
- width: var(--popover-width, fit-content);
161
- max-width: var(--popover-max-width, 50ch);
162
- max-height: calc(50vh - 2rem);
163
161
  overflow-x: clip;
164
162
  overflow-y: auto;
165
163
  scrollbar-width: thin;
@@ -11,6 +11,8 @@ type $$ComponentProps = {
11
11
  isOpen: boolean;
12
12
  triggerAttrs?: Record<string, string>;
13
13
  }]>;
14
+ /** Horizontal alignment: "start" (default), "center", or "end" */
15
+ align?: "start" | "center" | "end";
14
16
  /** The width of the menu */
15
17
  width?: string;
16
18
  /** The max width of the menu */
@@ -1,9 +1,16 @@
1
1
  <script lang="ts">
2
2
  import type { Component, Snippet } from 'svelte';
3
- import { onMount, onDestroy } from 'svelte';
4
3
  import type { ModalButton } from './ModalTypes.js';
5
4
  import type { RenderFn } from '../types.js';
6
5
 
6
+ /**
7
+ * Internal component for rendering modal content.
8
+ * Used by openModal() - not intended for direct use.
9
+ * Use <Modal> or <Dialog> instead.
10
+ *
11
+ * Note: Focus trapping, ESC handling, and backdrop are provided
12
+ * by the parent <dialog> element, not by this component.
13
+ */
7
14
  let {
8
15
  children,
9
16
  text,
@@ -18,13 +25,6 @@
18
25
  shape = 'rounded',
19
26
  contained = true,
20
27
  unstyled = false,
21
- showScrim = true,
22
- closeOnScrim = true,
23
- trapFocus = true,
24
- dismissOnEsc = true,
25
- width,
26
- maxWidth,
27
- maxHeight,
28
28
  }: {
29
29
  children?: Snippet<[close: () => void]>;
30
30
  text?: string;
@@ -39,23 +39,17 @@
39
39
  shape?: 'rounded' | 'sharp';
40
40
  contained?: boolean;
41
41
  unstyled?: boolean;
42
- showScrim?: boolean;
43
- closeOnScrim?: boolean;
44
- trapFocus?: boolean;
45
- dismissOnEsc?: boolean;
46
- width?: string;
47
- maxWidth?: string;
48
- maxHeight?: string;
49
42
  } = $props();
50
43
 
51
- let dialogEl: HTMLDivElement | null = $state(null);
52
- let previousActiveElement: HTMLElement | null = null;
53
44
  const baseId = crypto.randomUUID();
54
45
  let titleId = $derived(title ? `modal-title-${baseId}` : undefined);
55
46
  let loading: Record<number, boolean> = $state({});
56
47
 
57
48
  async function handleButtonClick(btn: ModalButton, index: number) {
58
- if (!btn.onclick) return;
49
+ if (!btn.onclick) {
50
+ close();
51
+ return;
52
+ }
59
53
  const result = btn.onclick(close);
60
54
  if (result instanceof Promise) {
61
55
  loading[index] = true;
@@ -66,76 +60,8 @@
66
60
  }
67
61
  }
68
62
  }
69
-
70
- onMount(() => {
71
- previousActiveElement = document.activeElement as HTMLElement;
72
- if (dialogEl && trapFocus) {
73
- // Focus first focusable element
74
- const focusableElements = getFocusableElements();
75
- if (focusableElements.length > 0) {
76
- focusableElements[0].focus();
77
- }
78
- }
79
- dialogEl?.focus();
80
- window.addEventListener('keydown', handleKeydown);
81
- });
82
-
83
- onDestroy(() => {
84
- // Restore focus on unmount
85
- previousActiveElement?.focus();
86
- window.removeEventListener('keydown', handleKeydown);
87
- });
88
-
89
- function getFocusableElements(): HTMLElement[] {
90
- if (!dialogEl) return [];
91
- const selector = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
92
- return Array.from(dialogEl.querySelectorAll(selector));
93
- }
94
-
95
- function handleKeydown(e: KeyboardEvent) {
96
- if (dismissOnEsc && e.key === 'Escape') {
97
- e.preventDefault();
98
- close();
99
- return;
100
- }
101
-
102
- if (trapFocus && e.key === 'Tab') {
103
- const focusableElements = getFocusableElements();
104
- if (focusableElements.length === 0) return;
105
-
106
- const firstElement = focusableElements[0];
107
- const lastElement = focusableElements[focusableElements.length - 1];
108
- const activeElement = document.activeElement;
109
-
110
- if (e.shiftKey) {
111
- // Shift+Tab
112
- if (activeElement === firstElement) {
113
- e.preventDefault();
114
- lastElement.focus();
115
- }
116
- } else {
117
- // Tab
118
- if (activeElement === lastElement) {
119
- e.preventDefault();
120
- firstElement.focus();
121
- }
122
- }
123
- }
124
- }
125
-
126
- function handleScrimClick(e: MouseEvent) {
127
- if (closeOnScrim && e.target === e.currentTarget) {
128
- close();
129
- }
130
- }
131
63
  </script>
132
64
 
133
- {#if showScrim}
134
- <!-- svelte-ignore a11y_click_events_have_key_events -->
135
- <!-- svelte-ignore a11y_no_static_element_interactions -->
136
- <div class="ModalScrim" onclick={handleScrimClick}></div>
137
- {/if}
138
-
139
65
  <div
140
66
  class="ModalContent {shape}"
141
67
  class:contained
@@ -143,10 +69,6 @@
143
69
  role="dialog"
144
70
  aria-modal="true"
145
71
  aria-labelledby={titleId}
146
- tabindex="-1"
147
- bind:this={dialogEl}
148
- onkeydown={handleKeydown}
149
- style="--modal-width: {width}; --modal-max-width: {maxWidth}; --modal-max-height: {maxHeight};"
150
72
  >
151
73
  <div class="ModalContentArea">
152
74
  {#if title}
@@ -185,14 +107,6 @@
185
107
  </div>
186
108
 
187
109
  <style>
188
- .ModalScrim {
189
- background: var(--scrim-background);
190
- backdrop-filter: var(--scrim-backdrop-filter);
191
- position: fixed;
192
- inset: 0;
193
- z-index: 0;
194
- }
195
-
196
110
  .ModalContent {
197
111
  display: grid;
198
112
  grid-template-rows: 1fr auto;
@@ -205,8 +119,6 @@
205
119
  border-radius: var(--modal-border-radius);
206
120
  box-shadow: 0 0.5rem 1rem var(--modal-shadow-color);
207
121
  overflow: hidden;
208
- position: relative;
209
- z-index: 1;
210
122
  }
211
123
 
212
124
  .ModalContent.sharp {
@@ -269,11 +181,4 @@
269
181
  border: none;
270
182
  padding: 0;
271
183
  }
272
-
273
- @media (prefers-reduced-motion: reduce) {
274
- .ModalContent {
275
- transition: none;
276
- }
277
- }
278
184
  </style>
279
-
@@ -15,13 +15,6 @@ type $$ComponentProps = {
15
15
  shape?: 'rounded' | 'sharp';
16
16
  contained?: boolean;
17
17
  unstyled?: boolean;
18
- showScrim?: boolean;
19
- closeOnScrim?: boolean;
20
- trapFocus?: boolean;
21
- dismissOnEsc?: boolean;
22
- width?: string;
23
- maxWidth?: string;
24
- maxHeight?: string;
25
18
  };
26
19
  declare const ModalContent: Component<$$ComponentProps, {}, "">;
27
20
  type ModalContent = ReturnType<typeof ModalContent>;
@@ -5,17 +5,26 @@
5
5
  /**
6
6
  * A base popover component using the Popover API and CSS Anchor Positioning.
7
7
  * Use this as a foundation for dropdowns, tooltips, contextual help, etc.
8
+ *
9
+ * Styling is controlled via CSS custom properties:
10
+ * - `--popover-background` - Background color
11
+ * - `--popover-border` - Border shorthand
12
+ * - `--popover-border-radius` - Border radius
13
+ * - `--popover-shadow` - Box shadow
14
+ * - `--popover-padding` - Content padding
8
15
  */
9
16
  let {
10
17
  open = $bindable(false),
11
18
  anchor,
12
- position = "block-end span-inline-start",
19
+ position = "block-end",
20
+ align = "center",
13
21
  fallbacks = ["flip-block", "flip-inline"],
14
22
  mode = "auto",
15
23
  offset,
16
24
  width,
17
25
  maxWidth,
18
26
  maxHeight,
27
+ unstyled = false,
19
28
  trigger,
20
29
  children,
21
30
  class: className = "",
@@ -26,13 +35,15 @@
26
35
  open?: boolean;
27
36
  /** Element to anchor to (if no trigger snippet provided) */
28
37
  anchor?: HTMLElement | string;
29
- /** CSS position-area value */
38
+ /** CSS position-area value for placement */
30
39
  position?: string;
40
+ /** Alignment: "start", "center", "end", or "anchor-center" */
41
+ align?: "start" | "center" | "end" | "anchor-center";
31
42
  /** CSS position-try-fallbacks values */
32
43
  fallbacks?: string[];
33
44
  /** Popover mode: "auto" (light dismiss) or "manual" */
34
45
  mode?: "auto" | "manual";
35
- /** Gap between anchor and popover */
46
+ /** Gap between anchor and popover (CSS margin value) */
36
47
  offset?: string;
37
48
  /** Width of the popover */
38
49
  width?: string;
@@ -40,6 +51,8 @@
40
51
  maxWidth?: string;
41
52
  /** Max height of the popover */
42
53
  maxHeight?: string;
54
+ /** Remove default styling (background, border, shadow) */
55
+ unstyled?: boolean;
43
56
  /** Optional trigger snippet - receives attrs to spread on trigger element */
44
57
  trigger?: Snippet<[{
45
58
  toggle: () => void;
@@ -127,14 +140,6 @@
127
140
  }
128
141
  }
129
142
 
130
- function show() {
131
- popoverEl?.showPopover();
132
- }
133
-
134
- function hide() {
135
- popoverEl?.hidePopover();
136
- }
137
-
138
143
  // Attrs to spread on trigger element
139
144
  const triggerAttrs = $derived({
140
145
  popovertarget: id,
@@ -148,12 +153,23 @@
148
153
  style: `anchor-name: ${anchorName}`,
149
154
  });
150
155
 
156
+ // Map align to CSS justify-self value
157
+ const justifySelf = $derived(
158
+ align === "center" ? "anchor-center" :
159
+ align === "anchor-center" ? "anchor-center" :
160
+ align === "start" ? "self-start" :
161
+ align === "end" ? "self-end" :
162
+ "anchor-center"
163
+ );
164
+
151
165
  // CSS custom properties for the popover
152
166
  const popoverStyle = $derived([
153
167
  `position-anchor: ${anchorName}`,
154
168
  `position-area: ${position}`,
169
+ `justify-self: ${justifySelf}`,
170
+ `position-try-order: most-block-size`,
155
171
  fallbacks.length ? `position-try-fallbacks: ${fallbacks.join(", ")}` : "",
156
- offset ? `margin: ${offset}` : "",
172
+ offset ? `--popover-offset: ${offset}` : "",
157
173
  width ? `--popover-width: ${width}` : "",
158
174
  maxWidth ? `--popover-max-width: ${maxWidth}` : "",
159
175
  maxHeight ? `--popover-max-height: ${maxHeight}` : "",
@@ -171,6 +187,7 @@
171
187
  {id}
172
188
  popover={mode}
173
189
  class="Popover {className}"
190
+ class:unstyled
174
191
  style={popoverStyle}
175
192
  >
176
193
  {@render children()}
@@ -182,19 +199,34 @@
182
199
  }
183
200
 
184
201
  .Popover {
185
- /* Reset browser defaults */
186
- margin: 0;
202
+ /* Reset browser popover defaults */
187
203
  padding: 0;
188
- border: none;
189
- background: transparent;
190
- overflow: visible;
204
+
205
+ /* Default surface styling - can be overridden via CSS custom properties */
206
+ background: var(--popover-background, var(--surface-background, var(--background-body)));
207
+ border: var(--popover-border, var(--surface-border, var(--border-size-thin) var(--border-style) var(--border-color)));
208
+ border-radius: var(--popover-border-radius, var(--surface-border-radius, var(--border-radius-base)));
209
+ box-shadow: var(--popover-shadow, var(--surface-shadow, 0 0.5rem 1rem var(--shadow-color)));
191
210
 
192
211
  /* Sizing */
193
- width: var(--popover-width, auto);
194
- max-width: var(--popover-max-width, none);
195
- max-height: var(--popover-max-height, none);
212
+ width: var(--popover-width, max-content);
213
+ max-width: var(--popover-max-width, calc(100vw - 2rem));
214
+ max-height: var(--popover-max-height, calc(100vh - 2rem));
215
+ overflow: auto;
216
+
217
+ /* Offset from anchor */
218
+ margin-block: var(--popover-offset, 0.25rem);
219
+ margin-inline: 0;
196
220
 
197
- /* Anchor positioning is set via inline style */
221
+ /* Ensure popover stays in viewport */
222
+ inset: auto;
223
+ }
224
+
225
+ .Popover.unstyled {
226
+ background: transparent;
227
+ border: none;
228
+ border-radius: 0;
229
+ box-shadow: none;
198
230
  }
199
231
 
200
232
  /* Animation */
@@ -219,4 +251,3 @@
219
251
  }
220
252
  }
221
253
  </style>
222
-
@@ -4,13 +4,15 @@ type $$ComponentProps = {
4
4
  open?: boolean;
5
5
  /** Element to anchor to (if no trigger snippet provided) */
6
6
  anchor?: HTMLElement | string;
7
- /** CSS position-area value */
7
+ /** CSS position-area value for placement */
8
8
  position?: string;
9
+ /** Alignment: "start", "center", "end", or "anchor-center" */
10
+ align?: "start" | "center" | "end" | "anchor-center";
9
11
  /** CSS position-try-fallbacks values */
10
12
  fallbacks?: string[];
11
13
  /** Popover mode: "auto" (light dismiss) or "manual" */
12
14
  mode?: "auto" | "manual";
13
- /** Gap between anchor and popover */
15
+ /** Gap between anchor and popover (CSS margin value) */
14
16
  offset?: string;
15
17
  /** Width of the popover */
16
18
  width?: string;
@@ -18,6 +20,8 @@ type $$ComponentProps = {
18
20
  maxWidth?: string;
19
21
  /** Max height of the popover */
20
22
  maxHeight?: string;
23
+ /** Remove default styling (background, border, shadow) */
24
+ unstyled?: boolean;
21
25
  /** Optional trigger snippet - receives attrs to spread on trigger element */
22
26
  trigger?: Snippet<[
23
27
  {
@@ -6,6 +6,7 @@
6
6
 
7
7
  /**
8
8
  * A styled toast/notification card.
9
+ * Uses the same surface styling system as Popover and Modal.
9
10
  * Typically used via addToast() but can be used standalone.
10
11
  */
11
12
  let {
@@ -76,14 +77,19 @@
76
77
  position: relative;
77
78
  display: grid;
78
79
  grid-template-columns: 1fr;
79
- gap: var(--toast-gap, var(--space-sm));
80
- padding: var(--toast-padding, var(--space-md));
81
- background: var(--toast-background, var(--surface-color));
82
- border: var(--toast-border, 1px solid var(--border-color));
83
- border-radius: var(--toast-border-radius, var(--radius-md));
84
- box-shadow: var(--toast-shadow, 0 4px 12px var(--shadow-color));
85
- min-width: var(--toast-min-width, 16rem);
86
- max-width: var(--toast-max-width, 24rem);
80
+ gap: var(--notification-gap, var(--space-sm));
81
+
82
+ /* Surface styling - consistent with Popover and Modal */
83
+ background: var(--notification-background-color, var(--background-body));
84
+ border: var(--notification-border-size) var(--notification-border-style) var(--notification-border-color);
85
+ border-radius: var(--notification-border-radius);
86
+ box-shadow: 0 0.25rem 1rem var(--notification-shadow-color);
87
+
88
+ padding-block: var(--notification-padding-block);
89
+ padding-inline: var(--notification-padding-inline);
90
+ min-width: var(--notification-min-width);
91
+ max-width: var(--notification-max-width);
92
+
87
93
  animation: toast-enter 0.2s ease-out;
88
94
  }
89
95
 
@@ -102,8 +108,8 @@
102
108
  .Icon {
103
109
  display: flex;
104
110
  align-items: flex-start;
105
- font-size: var(--toast-icon-size, 1.25rem);
106
- color: var(--toast-icon-color, var(--text-color-muted));
111
+ font-size: var(--notification-icon-size);
112
+ color: var(--text-color-p-subtle);
107
113
  }
108
114
 
109
115
  .Content {
@@ -113,14 +119,14 @@
113
119
  }
114
120
 
115
121
  .Title {
116
- font-weight: var(--toast-title-weight, 600);
117
- color: var(--toast-title-color, var(--text-color));
122
+ font-weight: var(--notification-title-font-weight);
123
+ color: var(--notification-title-color);
118
124
  line-height: 1.3;
119
125
  }
120
126
 
121
127
  .Text {
122
128
  margin: 0;
123
- color: var(--toast-text-color, var(--text-color-muted));
129
+ color: var(--notification-text-color);
124
130
  line-height: 1.4;
125
131
  }
126
132
 
@@ -147,4 +153,3 @@
147
153
  }
148
154
  }
149
155
  </style>
150
-
@@ -13,7 +13,6 @@ export { default as MenuDropdown } from './MenuDropdown.svelte';
13
13
  export { default as MenuItem } from './MenuItem.svelte';
14
14
  export { default as MenuItemContent } from './MenuItemContent.svelte';
15
15
  export { default as Modal } from './Modal.svelte';
16
- export { default as ModalContent } from './ModalContent.svelte';
17
16
  export { default as PageContent } from './PageContent.svelte';
18
17
  export { default as Popover } from './Popover.svelte';
19
18
  export { default as Tag } from './Tag.svelte';
@@ -13,7 +13,6 @@ export { default as MenuDropdown } from './MenuDropdown.svelte';
13
13
  export { default as MenuItem } from './MenuItem.svelte';
14
14
  export { default as MenuItemContent } from './MenuItemContent.svelte';
15
15
  export { default as Modal } from './Modal.svelte';
16
- export { default as ModalContent } from './ModalContent.svelte';
17
16
  export { default as PageContent } from './PageContent.svelte';
18
17
  export { default as Popover } from './Popover.svelte';
19
18
  export { default as Tag } from './Tag.svelte';
@@ -1,8 +1,15 @@
1
1
  import type { ModalOptions } from './ModalTypes.js';
2
2
  /**
3
3
  * Opens a modal programmatically using the native `<dialog>` element.
4
+ *
5
+ * The dialog provides:
6
+ * - Backdrop (::backdrop pseudo-element)
7
+ * - Focus trapping (native behavior)
8
+ * - ESC key handling (cancel event)
9
+ *
4
10
  * @param opts - Modal options or a simple string for text content
5
11
  * @returns An object with the modal id and a close function
12
+ *
6
13
  * @example
7
14
  * // Simple text modal
8
15
  * const { close } = openModal('Hello world!');
@@ -18,8 +18,15 @@ function unlockScroll() {
18
18
  }
19
19
  /**
20
20
  * Opens a modal programmatically using the native `<dialog>` element.
21
+ *
22
+ * The dialog provides:
23
+ * - Backdrop (::backdrop pseudo-element)
24
+ * - Focus trapping (native behavior)
25
+ * - ESC key handling (cancel event)
26
+ *
21
27
  * @param opts - Modal options or a simple string for text content
22
28
  * @returns An object with the modal id and a close function
29
+ *
23
30
  * @example
24
31
  * // Simple text modal
25
32
  * const { close } = openModal('Hello world!');
@@ -83,16 +90,9 @@ export function openModal(opts) {
83
90
  props: opts.props,
84
91
  buttonsConfig,
85
92
  title: opts.title,
86
- showScrim: false, // Dialog handles backdrop
87
- closeOnScrim: opts.closeOnScrim ?? true,
88
- trapFocus: opts.trapFocus ?? true,
89
- dismissOnEsc: opts.dismissOnEsc ?? true,
90
93
  unstyled: opts.unstyled ?? false,
91
94
  shape: opts.shape ?? 'rounded',
92
95
  contained: opts.contained ?? true,
93
- width: opts.width,
94
- maxWidth: opts.maxWidth,
95
- maxHeight: opts.maxHeight,
96
96
  },
97
97
  });
98
98
  // Handle dialog events
@@ -496,6 +496,30 @@
496
496
  @property --modal-actions-padding-inline { syntax: "<length>"; inherits: true; initial-value: 16px; }
497
497
  @property --modal-actions-padding-block { syntax: "<length>"; inherits: true; initial-value: 16px; }
498
498
 
499
+ /**
500
+ * Shared Surface System
501
+ * These provide consistent defaults across all overlay components (Popover, Modal, Menu, Toast)
502
+ */
503
+
504
+ @property --surface-background { syntax: "<color>"; inherits: true; initial-value: #ffffff; }
505
+ @property --surface-border { syntax: "*"; inherits: true; initial-value: 1px solid #d1d5db; }
506
+ @property --surface-border-radius { syntax: "<length>"; inherits: true; initial-value: 8px; }
507
+ @property --surface-shadow { syntax: "*"; inherits: true; initial-value: 0 0.5rem 1rem rgba(0, 0, 0, 0.1); }
508
+
509
+ /**
510
+ * Popover Component
511
+ */
512
+
513
+ @property --popover-background { syntax: "<color>"; inherits: true; initial-value: #ffffff; }
514
+ @property --popover-border { syntax: "*"; inherits: true; initial-value: 1px solid #d1d5db; }
515
+ @property --popover-border-radius { syntax: "<length>"; inherits: true; initial-value: 8px; }
516
+ @property --popover-shadow { syntax: "*"; inherits: true; initial-value: 0 0.5rem 1rem rgba(0, 0, 0, 0.1); }
517
+ @property --popover-padding { syntax: "<length>"; inherits: true; initial-value: 0; }
518
+ @property --popover-offset { syntax: "<length>"; inherits: true; initial-value: 0.25rem; }
519
+ @property --popover-width { syntax: "*"; inherits: true; initial-value: max-content; }
520
+ @property --popover-max-width { syntax: "<length>"; inherits: true; initial-value: calc(100vw - 2rem); }
521
+ @property --popover-max-height { syntax: "<length>"; inherits: true; initial-value: calc(100vh - 2rem); }
522
+
499
523
  /**
500
524
  * Overlay System
501
525
  */
@@ -238,11 +238,29 @@
238
238
  --table-row-background-hover: color-mix(in srgb, var(--theme-surface-interactive) 60%, transparent);
239
239
  --table-cell-color: var(--text-color-p);
240
240
 
241
+ /**
242
+ * Shared Surface (for Popover, Modal, Menu, Toast)
243
+ */
244
+
245
+ --surface-background: var(--background-body);
246
+ --surface-border: var(--border-size-thin) var(--border-style) var(--border-color);
247
+ --surface-border-radius: var(--border-radius-base);
248
+ --surface-shadow: 0 0.5rem 1rem var(--shadow-color);
249
+
250
+ /**
251
+ * Popover
252
+ */
253
+
254
+ --popover-background: var(--surface-background);
255
+ --popover-border: var(--surface-border);
256
+ --popover-border-radius: var(--surface-border-radius);
257
+ --popover-shadow: var(--surface-shadow);
258
+
241
259
  /**
242
260
  * Modals
243
261
  */
244
262
 
245
- --modal-background: var(--background-body);
263
+ --modal-background: var(--surface-background);
246
264
  --modal-border-color: var(--border-color);
247
265
  --modal-shadow-color: var(--shadow-color);
248
266
  --modal-actions-background: var(--theme-surface-subtlest);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lutra",
3
- "version": "0.1.34",
3
+ "version": "0.1.36",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "bump-and-publish:patch": "pnpm version:patch && pnpm build && npm publish",
@@ -45,7 +45,7 @@
45
45
  "@sveltejs/vite-plugin-svelte": "^6.2.1",
46
46
  "@types/node": "^25.0.1",
47
47
  "publint": "^0.3.16",
48
- "svelte": "^5.45.10",
48
+ "svelte": "^5.46.0",
49
49
  "svelte-check": "^4.3.4",
50
50
  "typescript": "^5.9.3",
51
51
  "vite": "^7.2.7"