@marianmeres/stuic 2.61.0 → 2.63.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -146,6 +146,18 @@ function getStringContent(content) {
146
146
  }
147
147
  return "";
148
148
  }
149
+ /**
150
+ * Find the appropriate container for the popover element.
151
+ * If the anchor is inside an open dialog (modal), append to the dialog
152
+ * so the popover renders in the same top layer as the modal.
153
+ */
154
+ function getPopoverContainer(anchorEl) {
155
+ const dialog = anchorEl.closest("dialog[open]");
156
+ if (dialog) {
157
+ return dialog;
158
+ }
159
+ return document.body;
160
+ }
149
161
  /**
150
162
  * A Svelte action that displays a popover anchored to an element using CSS Anchor Positioning.
151
163
  *
@@ -329,7 +341,9 @@ export function popover(anchorEl, fn) {
329
341
  anchorEl.setAttribute("aria-expanded", "true");
330
342
  const offsetValue = currentOptions.offset || "0.25rem";
331
343
  const useAnchorPositioning = isSupported && !currentOptions.forceFallback;
332
- // Create elements
344
+ // Get appropriate container (dialog if inside one, otherwise body)
345
+ // This ensures popover renders in same stacking context as modal dialogs
346
+ const container = getPopoverContainer(anchorEl);
333
347
  if (useAnchorPositioning) {
334
348
  // CSS Anchor Positioning mode
335
349
  popoverEl = document.createElement("div");
@@ -343,7 +357,7 @@ export function popover(anchorEl, fn) {
343
357
  margin: ${offsetValue};
344
358
  `;
345
359
  popoverEl.classList.add(...twMerge("stuic-popover", _classPopover, currentOptions.class).split(/\s/));
346
- document.body.appendChild(popoverEl);
360
+ container.appendChild(popoverEl);
347
361
  }
348
362
  else {
349
363
  // Fallback centered modal mode
@@ -351,7 +365,7 @@ export function popover(anchorEl, fn) {
351
365
  backdropEl = document.createElement("div");
352
366
  backdropEl.classList.add(...twMerge("stuic-popover-backdrop", _classBackdrop).split(/\s/));
353
367
  backdropEl.style.cssText = `transition-duration: ${TRANSITION}ms;`;
354
- document.body.appendChild(backdropEl);
368
+ container.appendChild(backdropEl);
355
369
  // Backdrop click closes popover
356
370
  if (currentOptions.closeOnClickOutside !== false) {
357
371
  backdropEl.addEventListener("click", hide);
@@ -383,7 +397,7 @@ export function popover(anchorEl, fn) {
383
397
  `;
384
398
  popoverEl.classList.add(...twMerge("stuic-popover-fallback", _classPopover, currentOptions.class).split(/\s/));
385
399
  wrapperEl.appendChild(popoverEl);
386
- document.body.appendChild(wrapperEl);
400
+ container.appendChild(wrapperEl);
387
401
  // Lock body scroll in fallback (modal) mode
388
402
  BodyScroll.lock();
389
403
  // Click on wrapper (outside popover) closes
@@ -48,6 +48,18 @@ const POSITION_MAP = {
48
48
  left: "left",
49
49
  right: "right",
50
50
  };
51
+ /**
52
+ * Find the appropriate container for the tooltip element.
53
+ * If the anchor is inside an open dialog (modal), append to the dialog
54
+ * so the tooltip renders in the same top layer as the modal.
55
+ */
56
+ function getTooltipContainer(anchorEl) {
57
+ const dialog = anchorEl.closest("dialog[open]");
58
+ if (dialog) {
59
+ return dialog;
60
+ }
61
+ return document.body;
62
+ }
51
63
  // Global tooltip configuration - allows disabling all tooltips at runtime
52
64
  const globalTooltipConfig = $state({ enabled: true });
53
65
  // Touch device auto-detection (runs once on first tooltip init)
@@ -164,7 +176,9 @@ export function tooltip(anchorEl, fn) {
164
176
  tooltipEl.setAttribute("role", "tooltip");
165
177
  tooltipEl.style.cssText += `position-anchor: ${anchorName}; transition-duration: ${TRANSITION}ms; position-area: ${POSITION_MAP[position] || "top"};`;
166
178
  tooltipEl.classList.add(...twMerge("stuic-tooltip", _classTooltip).split(/\s/));
167
- document.body.appendChild(tooltipEl);
179
+ // Append to dialog if inside one, otherwise body (for proper top-layer rendering in modals)
180
+ const container = getTooltipContainer(anchorEl);
181
+ container.appendChild(tooltipEl);
168
182
  //
169
183
  tooltipEl.addEventListener("mouseenter", schedule_show);
170
184
  tooltipEl.addEventListener("mouseleave", schedule_hide);
@@ -216,11 +230,6 @@ export function tooltip(anchorEl, fn) {
216
230
  tooltipEl.classList.add("tt-visible");
217
231
  on_show?.();
218
232
  });
219
- // waitForTwoRepaints().then(() => {
220
- // tooltipEl.classList.add("tt-visible");
221
- // this approach is nicer, but I have a suspicion of the event handlers not being destroyed properly (maybe just a hot-reload issues...)
222
- // waitForTransitionEnd(tooltipEl).then(() => on_show?.());
223
- // });
224
233
  }
225
234
  }, TIMEOUT);
226
235
  }
@@ -239,11 +248,6 @@ export function tooltip(anchorEl, fn) {
239
248
  tooltipEl.classList.remove("tt-block");
240
249
  on_hide?.();
241
250
  }, TRANSITION);
242
- // this approach is nicer, but I have a suspicion of the event handlers not being destroyed properly (maybe just a hot-reload issues...)
243
- // waitForTransitionEnd(tooltipEl).then(() => {
244
- // tooltipEl.classList.remove("tt-block");
245
- // on_hide?.();
246
- // });
247
251
  }, TIMEOUT);
248
252
  }
249
253
  // "reactive" params re/set
@@ -21,7 +21,7 @@
21
21
  iconZoomOut,
22
22
  } from "../../icons/index.js";
23
23
  import { getFileTypeLabel } from "../../utils/get-file-type-label.js";
24
- import { Modal } from "../Modal/index.js";
24
+ import { ModalDialog } from "../ModalDialog/index.js";
25
25
  import { createClog } from "@marianmeres/clog";
26
26
  import { isImage } from "../../utils/is-image.js";
27
27
  import { isPlainObject } from "../../utils/is-plain-object.js";
@@ -60,10 +60,8 @@
60
60
  assets: string[] | AssetPreview[];
61
61
  classControls?: string;
62
62
  //
63
- modalClassBackdrop?: string;
64
- modalClassInner?: string;
63
+ modalClassDialog?: string;
65
64
  modalClass?: string;
66
- modalClassMain?: string;
67
65
  //
68
66
  /** Optional translate function */
69
67
  t?: TranslateFn;
@@ -176,10 +174,8 @@
176
174
  const clog = createClog("AssetsPreview", { color: "auto" });
177
175
 
178
176
  let {
179
- modalClassBackdrop = "",
180
- modalClassInner = "",
177
+ modalClassDialog = "",
181
178
  modalClass = "",
182
- modalClassMain = "",
183
179
  assets: _assets,
184
180
  t = t_default,
185
181
  classControls = "",
@@ -192,7 +188,7 @@
192
188
  (_assets ?? []).map(normalizeInput).filter(Boolean) as AssetPreviewNormalized[]
193
189
  );
194
190
  let previewIdx = $state<number>(0);
195
- let modal: Modal | undefined = $state();
191
+ let modal: ModalDialog | undefined = $state();
196
192
  let dotTooltip: string | undefined = $state();
197
193
 
198
194
  // Zoom state
@@ -478,13 +474,15 @@
478
474
  />
479
475
 
480
476
  {#if assets.length}
481
- <Modal
477
+ <ModalDialog
482
478
  bind:this={modal}
483
- onEscape={modal?.close}
484
- classBackdrop="p-2 md:p-2 {modalClassBackdrop}"
485
- classInner="max-w-full h-full {modalClassInner}"
486
- class="max-h-full md:max-h-full rounded-lg {modalClass}"
487
- classMain="flex items-center justify-center relative stuic-assets-preview stuic-assets-preview-open {modalClassMain}"
479
+ classDialog={modalClassDialog}
480
+ class={twMerge(
481
+ "max-w-full max-h-full h-full rounded-lg",
482
+ "flex items-center justify-center relative",
483
+ "stuic-assets-preview stuic-assets-preview-open",
484
+ modalClass
485
+ )}
488
486
  >
489
487
  {@const previewAsset = assets?.[previewIdx]}
490
488
  {#if previewAsset}
@@ -561,7 +559,7 @@
561
559
  onclick={zoomOut}
562
560
  disabled={zoomLevelIdx === 0}
563
561
  aria-label={t("zoom_out")}
564
- use:tooltip
562
+ use:tooltip={() => ({ content: t("zoom_out") })}
565
563
  >
566
564
  {@html iconZoomOut({ class: "size-6" })}
567
565
  </button>
@@ -572,7 +570,7 @@
572
570
  onclick={zoomIn}
573
571
  disabled={zoomLevelIdx === ZOOM_LEVELS.length - 1}
574
572
  aria-label={t("zoom_in")}
575
- use:tooltip
573
+ use:tooltip={() => ({ content: t("zoom_in") })}
576
574
  >
577
575
  {@html iconZoomIn({ class: "size-6" })}
578
576
  </button>
@@ -651,5 +649,5 @@
651
649
  </div>
652
650
  {/if}
653
651
  {/if}
654
- </Modal>
652
+ </ModalDialog>
655
653
  {/if}
@@ -12,10 +12,8 @@ export interface AssetPreview {
12
12
  export interface Props {
13
13
  assets: string[] | AssetPreview[];
14
14
  classControls?: string;
15
- modalClassBackdrop?: string;
16
- modalClassInner?: string;
15
+ modalClassDialog?: string;
17
16
  modalClass?: string;
18
- modalClassMain?: string;
19
17
  /** Optional translate function */
20
18
  t?: TranslateFn;
21
19
  /** Optional delete handler - receives the current asset and its index */
@@ -32,7 +30,7 @@ declare const AssetsPreview: import("svelte").Component<Props, {
32
30
  open: (index?: number) => void;
33
31
  close: () => void;
34
32
  visibility: () => {
35
- readonly visible: boolean;
33
+ readonly visible: boolean | undefined;
36
34
  };
37
35
  setOpener: (opener?: null | HTMLElement) => void;
38
36
  }, "">;
@@ -1,6 +1,6 @@
1
1
  <script lang="ts" module>
2
2
  import { ItemCollection, type Item } from "@marianmeres/item-collection";
3
- import { Modal } from "../Modal/index.js";
3
+ import { ModalDialog } from "../ModalDialog/index.js";
4
4
  import { createClog } from "@marianmeres/clog";
5
5
  import { FieldInput } from "../Input/index.js";
6
6
  import { twMerge } from "../../utils/tw-merge.js";
@@ -91,11 +91,11 @@
91
91
  });
92
92
  }
93
93
 
94
- let modal: Modal = $state()!;
94
+ let modalDialog: ModalDialog = $state()!;
95
95
  let isFetching = $state(false);
96
- let modalEl: HTMLDivElement | undefined = $state();
97
96
  let optionsBox: HTMLDivElement | undefined = $state();
98
97
  let activeEl: HTMLButtonElement | undefined = $state();
98
+ let fetchRequestId = 0;
99
99
 
100
100
  //
101
101
  // svelte-ignore state_referenced_locally
@@ -109,7 +109,7 @@
109
109
 
110
110
  // scroll the active option into view
111
111
  $effect(() => {
112
- if (modal.visibility().visible && options.active?.[itemIdPropName]) {
112
+ if (modalDialog.visibility().visible && options.active?.[itemIdPropName]) {
113
113
  activeEl = qsa(`#${btn_id(options.active[itemIdPropName])}`, optionsBox)[0] as any;
114
114
  activeEl?.scrollIntoView({ behavior: "smooth", block: "center" });
115
115
  activeEl?.focus();
@@ -120,15 +120,18 @@
120
120
 
121
121
  //
122
122
  const debounced = new Debounced(() => q, 150);
123
- watch([() => debounced.current], ([currQ], [oldQ]) => {
123
+ watch([() => debounced.current], ([currQ], [_oldQ]) => {
124
124
  if (!currQ && !showAllOnEmptyQ) {
125
125
  _optionsColl.clear();
126
126
  return;
127
127
  }
128
128
 
129
129
  isFetching = true;
130
+ const currentRequest = ++fetchRequestId;
130
131
  getOptions(`${currQ}`, [])
131
132
  .then((res) => {
133
+ // Ignore stale responses
134
+ if (currentRequest !== fetchRequestId) return;
132
135
  _optionsColl.clear().addMany(res);
133
136
  })
134
137
  .catch((e) => {
@@ -151,15 +154,16 @@
151
154
  }
152
155
 
153
156
  export function close() {
154
- modal.close();
157
+ modalDialog.close();
155
158
  }
156
159
 
157
160
  export function open(openerOrEvent?: null | HTMLElement | MouseEvent) {
158
- modal.open(openerOrEvent);
161
+ modalDialog.open(openerOrEvent);
159
162
  }
160
163
 
161
164
  // internal DRY
162
165
  const rand = Math.random().toString(36).slice(2);
166
+ const listId = `cmd-menu-list-${rand}`;
163
167
  function btn_id(id: string | number, prefix = "btn-") {
164
168
  return prefix + rand + strHash(`${id}`.repeat(3));
165
169
  }
@@ -195,7 +199,7 @@
195
199
  <!-- this must be on window as we're catching any typing anywhere -->
196
200
  <svelte:window
197
201
  onkeydown={(e) => {
198
- if (modal.visibility().visible) {
202
+ if (modalDialog.visibility().visible) {
199
203
  // arrow navigation
200
204
  if (["ArrowDown", "ArrowUp"].includes(e.key)) {
201
205
  e.preventDefault();
@@ -213,154 +217,174 @@
213
217
  }}
214
218
  />
215
219
 
216
- <Modal
217
- bind:this={modal}
218
- onEscape={modal?.close}
219
- class="bg-transparent dark:bg-transparent"
220
- classInner="max-w-2xl"
221
- bind:el={modalEl}
220
+ <ModalDialog
221
+ bind:this={modalDialog}
222
+ classDialog="items-start"
223
+ class="w-full max-w-3xl bg-transparent pointer-events-none"
222
224
  {noScrollLock}
223
225
  >
224
- <form
225
- onsubmit={(e) => {
226
- e.preventDefault();
227
- // collection.setQuery(`${q}`.trim());
228
- modal.close();
229
- // clog("TODO save", `${q}`.trim());
230
- }}
231
- class=""
232
- >
233
- <FieldInput
234
- type="text"
235
- name="q"
236
- bind:input
237
- bind:value={q}
238
- class="search m-4 mb-12 shadow-xl"
239
- classLabelBox="m-0"
240
- autocomplete="off"
241
- placeholder={searchPlaceholder ?? t("search_placeholder")}
242
- classInputBoxWrap={twMerge(
243
- // always look like focused
244
- `border-primary border-input-accent dark:border-input-accent-dark`,
245
- `ring-input-accent/20 dark:ring-input-accent-dark/20 ring-4`
246
- )}
247
- onkeydown={(e) => {
248
- if (e.key === "Enter") {
249
- e.preventDefault();
250
- submit();
251
- }
226
+ <div class="pt-0 md:pt-[20vh] w-full">
227
+ <form
228
+ class="pointer-events-auto"
229
+ onsubmit={(e) => {
230
+ e.preventDefault();
231
+ modalDialog.close();
252
232
  }}
253
233
  >
254
- {#snippet inputBefore()}
255
- <div class="flex flex-col items-center justify-center pl-3 opacity-50">
256
- {@html iconSearch({ size: 14 })}
257
- </div>
258
- {/snippet}
259
- {#snippet inputAfter()}
260
- <div class="flex pl-2 items-center justify-center opacity-50">
261
- {#if isFetching}
262
- <Spinner class="w-4" />
263
- {/if}
264
- </div>
265
- <div class="flex items-center justify-center">
266
- <button
267
- type="button"
268
- class={twMerge(
269
- "opacity-50 rounded m-1",
270
- "hover:opacity-100 hover:bg-neutral-200 dark:hover:bg-neutral-800",
271
- "focus-visible:opacity-100 focus-visible:outline-0",
272
- "focus-visible:bg-neutral-200 dark:focus-visible:bg-neutral-800"
273
- )}
274
- onclick={(e) => {
275
- e.preventDefault();
276
- if (!`${q || ""}`.trim()) {
277
- return modal.close();
278
- }
279
- q = "";
280
- input?.focus();
281
- }}
282
- >
283
- <X class="m-2 size-4 " />
284
- </button>
285
- </div>
286
- {/snippet}
287
- {#snippet inputBelow()}
288
- {#if options.size}
289
- <div
290
- class={twMerge(
291
- "options block space-y-1 p-1",
292
- "overflow-y-auto overflow-x-hidden mb-1",
293
- "border-t border-black/20",
294
- "max-h-60"
295
- )}
296
- bind:this={optionsBox}
297
- tabindex="-1"
298
- >
299
- {#each _normalize_and_group_options(options.items) as [_optgroup, _opts]}
300
- <!-- {console.log(11111, _optgroup, _opts)} -->
301
- <div class="p-1">
302
- {#if _optgroup}
303
- <div
304
- class="text-sm capitalize opacity-50 border-b border-black/10 mb-1 p-1"
305
- >
306
- {_optgroup}
307
- </div>
308
- {/if}
309
- <ul>
310
- {#each _opts as item (item.id)}
311
- {@const active =
312
- item[itemIdPropName] === options.active?.[itemIdPropName]}
313
- <!-- {@const isSelected = false} -->
314
- <li class:active>
315
- <button
316
- class:active
317
- type="button"
318
- class={twMerge(
319
- "no-focus-visible",
320
- "text-left rounded-md py-2 px-2.5",
321
- "min-w-0 w-full overflow-hidden text-ellipsis whitespace-nowrap",
322
- "border border-transparent",
323
- "focus:outline-0 focus:border-neutral-400 dark:focus:border-neutral-500",
324
- "focus-visible:outline-0 focus-visible:ring-0",
325
- "hover:border-neutral-400 dark:hover:border-neutral-500",
326
- active && "bg-neutral-200 dark:bg-neutral-800",
327
- classOption,
328
- // active && "border-neutral-400",
329
- active && classOptionActive
330
- )}
331
- id={btn_id(item[itemIdPropName])}
332
- tabindex="-1"
333
- onclick={() => {
334
- _optionsColl.setActive(item);
335
- submit();
336
- }}
337
- onkeydown={(e) => {
338
- // need to handle tab here, because the tabindex="-1" is ignored
339
- // in the focus-trap selectors... so, on Tab, manually focusin input
340
- if (e.key === "Tab") {
341
- e.preventDefault();
342
- input?.focus();
343
- }
344
- }}
345
- >
346
- <!-- role="checkbox"
347
- aria-checked={active} -->
348
- {_renderOptionLabel(item)}
349
- </button>
350
- </li>
351
- {/each}
352
- </ul>
353
- </div>
354
- {/each}
234
+ <FieldInput
235
+ type="text"
236
+ name="q"
237
+ renderSize="lg"
238
+ bind:input
239
+ bind:value={q}
240
+ class="search m-2 shadow-xl"
241
+ classLabelBox="m-0"
242
+ autocomplete="off"
243
+ aria-autocomplete="list"
244
+ aria-controls={options.size ? listId : undefined}
245
+ aria-activedescendant={options.active ? btn_id(options.active[itemIdPropName]) : undefined}
246
+ placeholder={searchPlaceholder ?? t("search_placeholder")}
247
+ classInputBoxWrap={twMerge(
248
+ // always look like focused
249
+ `border-primary border-input-accent dark:border-input-accent-dark`,
250
+ `ring-input-accent/20 dark:ring-input-accent-dark/20 ring-4`
251
+ )}
252
+ onkeydown={(e) => {
253
+ if (e.key === "Enter") {
254
+ e.preventDefault();
255
+ submit();
256
+ }
257
+ }}
258
+ >
259
+ {#snippet inputBefore()}
260
+ <div class="flex flex-col items-center justify-center pl-3 opacity-75">
261
+ {@html iconSearch({ size: 19, strokeWidth: 3 })}
262
+ </div>
263
+ {/snippet}
264
+ {#snippet inputAfter()}
265
+ <div class="flex pl-2 items-center justify-center opacity-50">
266
+ {#if isFetching}
267
+ <Spinner class="w-4" />
268
+ {/if}
355
269
  </div>
356
- {/if}
357
- {/snippet}
358
- </FieldInput>
359
- </form>
360
- </Modal>
270
+ <div class="flex items-center justify-center">
271
+ <button
272
+ type="button"
273
+ class={twMerge(
274
+ "rounded m-1 opacity-75",
275
+ "hover:opacity-100 hover:bg-neutral-200 dark:hover:bg-neutral-800",
276
+ "focus-visible:opacity-100 focus-visible:outline-0",
277
+ "focus-visible:bg-neutral-200 dark:focus-visible:bg-neutral-800"
278
+ )}
279
+ onclick={(e) => {
280
+ e.preventDefault();
281
+ if (!`${q || ""}`.trim()) {
282
+ return modalDialog.close();
283
+ }
284
+ q = "";
285
+ input?.focus();
286
+ }}
287
+ >
288
+ <X class="m-2 size-6" />
289
+ </button>
290
+ </div>
291
+ {/snippet}
292
+ {#snippet inputBelow()}
293
+ <div class="sr-only" aria-live="polite" aria-atomic="true">
294
+ {#if options.size}
295
+ {options.size} results available
296
+ {/if}
297
+ </div>
298
+ {#if options.size}
299
+ <div
300
+ class={twMerge(
301
+ "options block space-y-1 p-1",
302
+ "overflow-y-auto overflow-x-hidden mb-1",
303
+ "border-t border-black/20",
304
+ "max-h-60"
305
+ )}
306
+ bind:this={optionsBox}
307
+ tabindex="-1"
308
+ role="listbox"
309
+ id={listId}
310
+ aria-label="Search results"
311
+ >
312
+ {#each _normalize_and_group_options(options.items) as [_optgroup, _opts]}
313
+ <!-- {console.log(11111, _optgroup, _opts)} -->
314
+ <div class="p-1">
315
+ {#if _optgroup}
316
+ <div
317
+ class="text-sm capitalize opacity-50 border-b border-black/10 mb-1 p-1"
318
+ >
319
+ {_optgroup}
320
+ </div>
321
+ {/if}
322
+ <ul role="presentation">
323
+ {#each _opts as item (item[itemIdPropName])}
324
+ {@const active =
325
+ item[itemIdPropName] === options.active?.[itemIdPropName]}
326
+ <!-- {@const isSelected = false} -->
327
+ <li class:active role="presentation">
328
+ <button
329
+ class:active
330
+ type="button"
331
+ role="option"
332
+ aria-selected={active}
333
+ class={twMerge(
334
+ "no-focus-visible",
335
+ "text-left rounded-md py-2 px-2.5",
336
+ "min-w-0 w-full overflow-hidden text-ellipsis whitespace-nowrap",
337
+ "border border-transparent",
338
+ "focus:outline-0 focus:border-neutral-400 dark:focus:border-neutral-500",
339
+ "focus-visible:outline-0 focus-visible:ring-0",
340
+ "hover:border-neutral-400 dark:hover:border-neutral-500",
341
+ active && "bg-neutral-200 dark:bg-neutral-800",
342
+ classOption,
343
+ active && classOptionActive
344
+ )}
345
+ id={btn_id(item[itemIdPropName])}
346
+ tabindex="-1"
347
+ onclick={() => {
348
+ _optionsColl.setActive(item);
349
+ submit();
350
+ }}
351
+ onkeydown={(e) => {
352
+ // need to handle tab here, because the tabindex="-1" is ignored
353
+ // in the focus-trap selectors... so, on Tab, manually focusin input
354
+ if (e.key === "Tab") {
355
+ e.preventDefault();
356
+ input?.focus();
357
+ }
358
+ }}
359
+ >
360
+ {_renderOptionLabel(item)}
361
+ </button>
362
+ </li>
363
+ {/each}
364
+ </ul>
365
+ </div>
366
+ {/each}
367
+ </div>
368
+ {/if}
369
+ {/snippet}
370
+ </FieldInput>
371
+ </form>
372
+ </div>
373
+ </ModalDialog>
361
374
 
362
375
  <style>
363
376
  .options {
364
377
  scrollbar-width: thin;
365
378
  }
379
+ .sr-only {
380
+ position: absolute;
381
+ width: 1px;
382
+ height: 1px;
383
+ padding: 0;
384
+ margin: -1px;
385
+ overflow: hidden;
386
+ clip: rect(0, 0, 0, 0);
387
+ white-space: nowrap;
388
+ border: 0;
389
+ }
366
390
  </style>
@@ -87,6 +87,12 @@ A searchable command palette/menu modal for quick navigation and selection. Supp
87
87
  />
88
88
  ```
89
89
 
90
+ ## Layout
91
+
92
+ The command menu uses `ModalDialog` internally with top-aligned positioning:
93
+ - **Mobile**: Input at top of screen with 1rem margins from edges
94
+ - **Desktop (md+)**: Input positioned at ~20% from top, max-width 768px, centered horizontally
95
+
90
96
  ## Keyboard Navigation
91
97
 
92
98
  - **Arrow Up/Down**: Navigate options
@@ -241,7 +241,7 @@ interface DropdownMenuExpandableItem {
241
241
 
242
242
  ```svelte
243
243
  <script lang="ts">
244
- import { AvatarInitials, DropdownMenu } from 'stuic';
244
+ import { Avatar, DropdownMenu } from 'stuic';
245
245
  </script>
246
246
 
247
247
  <DropdownMenu
@@ -252,7 +252,7 @@ interface DropdownMenuExpandableItem {
252
252
  >
253
253
  {#snippet trigger({ isOpen, toggle, triggerProps })}
254
254
  <button {...triggerProps} onclick={toggle}>
255
- <AvatarInitials input="john.doe@example.com" autoColor />
255
+ <Avatar input="john.doe@example.com" autoColor />
256
256
  </button>
257
257
  {/snippet}
258
258
  </DropdownMenu>