@marianmeres/stuic 2.60.0 → 2.62.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
@@ -50,6 +50,9 @@
50
50
 
51
51
  let headerHeight = $state(0);
52
52
 
53
+ // Safari was showing scrollbar so adding 1px extra which solves the problem
54
+ let topOffset = $derived(headerHeight + 1);
55
+
53
56
  // pragmatic use case...
54
57
  let mainWidth: number = $state(0);
55
58
  setContext(APP_SHELL_SIMPLE_MAIN_WIDTH, {
@@ -77,7 +80,7 @@
77
80
  bind:this={elRail}
78
81
  data-shell="rail"
79
82
  style:top="{headerHeight}px"
80
- style:height="calc(100dvh - {headerHeight}px)"
83
+ style:height="calc(100dvh - {topOffset}px)"
81
84
  class={twMerge(
82
85
  "sticky shrink-0",
83
86
  "flex flex-col items-center",
@@ -95,7 +98,7 @@
95
98
  bind:this={elAside}
96
99
  data-shell="aside"
97
100
  style:top="{headerHeight}px"
98
- style:height="calc(100dvh - {headerHeight}px)"
101
+ style:height="calc(100dvh - {topOffset}px)"
99
102
  class={twMerge(
100
103
  "sticky shrink-0",
101
104
  "flex flex-col items-center",
@@ -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,9 +91,8 @@
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();
99
98
 
@@ -109,7 +108,7 @@
109
108
 
110
109
  // scroll the active option into view
111
110
  $effect(() => {
112
- if (modal.visibility().visible && options.active?.[itemIdPropName]) {
111
+ if (modalDialog.visibility().visible && options.active?.[itemIdPropName]) {
113
112
  activeEl = qsa(`#${btn_id(options.active[itemIdPropName])}`, optionsBox)[0] as any;
114
113
  activeEl?.scrollIntoView({ behavior: "smooth", block: "center" });
115
114
  activeEl?.focus();
@@ -151,11 +150,11 @@
151
150
  }
152
151
 
153
152
  export function close() {
154
- modal.close();
153
+ modalDialog.close();
155
154
  }
156
155
 
157
156
  export function open(openerOrEvent?: null | HTMLElement | MouseEvent) {
158
- modal.open(openerOrEvent);
157
+ modalDialog.open(openerOrEvent);
159
158
  }
160
159
 
161
160
  // internal DRY
@@ -195,7 +194,7 @@
195
194
  <!-- this must be on window as we're catching any typing anywhere -->
196
195
  <svelte:window
197
196
  onkeydown={(e) => {
198
- if (modal.visibility().visible) {
197
+ if (modalDialog.visibility().visible) {
199
198
  // arrow navigation
200
199
  if (["ArrowDown", "ArrowUp"].includes(e.key)) {
201
200
  e.preventDefault();
@@ -213,151 +212,150 @@
213
212
  }}
214
213
  />
215
214
 
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}
215
+ <ModalDialog
216
+ bind:this={modalDialog}
217
+ classDialog="items-start"
218
+ class="w-full max-w-3xl bg-transparent pointer-events-none"
222
219
  {noScrollLock}
223
220
  >
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
- }
221
+ <div class="pt-0 md:pt-[20vh] w-full">
222
+ <form
223
+ class="pointer-events-auto"
224
+ onsubmit={(e) => {
225
+ e.preventDefault();
226
+ modalDialog.close();
252
227
  }}
253
228
  >
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}
229
+ <FieldInput
230
+ type="text"
231
+ name="q"
232
+ renderSize="lg"
233
+ bind:input
234
+ bind:value={q}
235
+ class="search m-2 shadow-xl"
236
+ classLabelBox="m-0"
237
+ autocomplete="off"
238
+ placeholder={searchPlaceholder ?? t("search_placeholder")}
239
+ classInputBoxWrap={twMerge(
240
+ // always look like focused
241
+ `border-primary border-input-accent dark:border-input-accent-dark`,
242
+ `ring-input-accent/20 dark:ring-input-accent-dark/20 ring-4`
243
+ )}
244
+ onkeydown={(e) => {
245
+ if (e.key === "Enter") {
246
+ e.preventDefault();
247
+ submit();
248
+ }
249
+ }}
250
+ >
251
+ {#snippet inputBefore()}
252
+ <div class="flex flex-col items-center justify-center pl-3 opacity-75">
253
+ {@html iconSearch({ size: 19, strokeWidth: 3 })}
355
254
  </div>
356
- {/if}
357
- {/snippet}
358
- </FieldInput>
359
- </form>
360
- </Modal>
255
+ {/snippet}
256
+ {#snippet inputAfter()}
257
+ <div class="flex pl-2 items-center justify-center opacity-50">
258
+ {#if isFetching}
259
+ <Spinner class="w-4" />
260
+ {/if}
261
+ </div>
262
+ <div class="flex items-center justify-center">
263
+ <button
264
+ type="button"
265
+ class={twMerge(
266
+ "rounded m-1 opacity-75",
267
+ "hover:opacity-100 hover:bg-neutral-200 dark:hover:bg-neutral-800",
268
+ "focus-visible:opacity-100 focus-visible:outline-0",
269
+ "focus-visible:bg-neutral-200 dark:focus-visible:bg-neutral-800"
270
+ )}
271
+ onclick={(e) => {
272
+ e.preventDefault();
273
+ if (!`${q || ""}`.trim()) {
274
+ return modalDialog.close();
275
+ }
276
+ q = "";
277
+ input?.focus();
278
+ }}
279
+ >
280
+ <X class="m-2 size-6" />
281
+ </button>
282
+ </div>
283
+ {/snippet}
284
+ {#snippet inputBelow()}
285
+ {#if options.size}
286
+ <div
287
+ class={twMerge(
288
+ "options block space-y-1 p-1",
289
+ "overflow-y-auto overflow-x-hidden mb-1",
290
+ "border-t border-black/20",
291
+ "max-h-60"
292
+ )}
293
+ bind:this={optionsBox}
294
+ tabindex="-1"
295
+ >
296
+ {#each _normalize_and_group_options(options.items) as [_optgroup, _opts]}
297
+ <!-- {console.log(11111, _optgroup, _opts)} -->
298
+ <div class="p-1">
299
+ {#if _optgroup}
300
+ <div
301
+ class="text-sm capitalize opacity-50 border-b border-black/10 mb-1 p-1"
302
+ >
303
+ {_optgroup}
304
+ </div>
305
+ {/if}
306
+ <ul>
307
+ {#each _opts as item (item.id)}
308
+ {@const active =
309
+ item[itemIdPropName] === options.active?.[itemIdPropName]}
310
+ <!-- {@const isSelected = false} -->
311
+ <li class:active>
312
+ <button
313
+ class:active
314
+ type="button"
315
+ class={twMerge(
316
+ "no-focus-visible",
317
+ "text-left rounded-md py-2 px-2.5",
318
+ "min-w-0 w-full overflow-hidden text-ellipsis whitespace-nowrap",
319
+ "border border-transparent",
320
+ "focus:outline-0 focus:border-neutral-400 dark:focus:border-neutral-500",
321
+ "focus-visible:outline-0 focus-visible:ring-0",
322
+ "hover:border-neutral-400 dark:hover:border-neutral-500",
323
+ active && "bg-neutral-200 dark:bg-neutral-800",
324
+ classOption,
325
+ // active && "border-neutral-400",
326
+ active && classOptionActive
327
+ )}
328
+ id={btn_id(item[itemIdPropName])}
329
+ tabindex="-1"
330
+ onclick={() => {
331
+ _optionsColl.setActive(item);
332
+ submit();
333
+ }}
334
+ onkeydown={(e) => {
335
+ // need to handle tab here, because the tabindex="-1" is ignored
336
+ // in the focus-trap selectors... so, on Tab, manually focusin input
337
+ if (e.key === "Tab") {
338
+ e.preventDefault();
339
+ input?.focus();
340
+ }
341
+ }}
342
+ >
343
+ <!-- role="checkbox"
344
+ aria-checked={active} -->
345
+ {_renderOptionLabel(item)}
346
+ </button>
347
+ </li>
348
+ {/each}
349
+ </ul>
350
+ </div>
351
+ {/each}
352
+ </div>
353
+ {/if}
354
+ {/snippet}
355
+ </FieldInput>
356
+ </form>
357
+ </div>
358
+ </ModalDialog>
361
359
 
362
360
  <style>
363
361
  .options {
@@ -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>