@insymetri/styleguide 0.1.78 → 0.1.80

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 (30) hide show
  1. package/dist/IIDropdownInput/IIDropdownInput.svelte +51 -2
  2. package/dist/IIDropdownInput/IIDropdownInput.svelte.d.ts +21 -0
  3. package/dist/IIDropdownInput/IIDropdownInputStories.svelte +23 -0
  4. package/dist/{IIDropdownMenu/IIDropdownMenu.svelte → IIMenu/IIMenu.svelte} +12 -6
  5. package/dist/{IIDropdownMenu/IIDropdownMenu.svelte.d.ts → IIMenu/IIMenu.svelte.d.ts} +7 -3
  6. package/dist/{IIDropdownMenu/IIDropdownMenuStories.svelte → IIMenu/IIMenuStories.svelte} +7 -7
  7. package/dist/{IIDropdownMenu/IIDropdownMenuStories.svelte.d.ts → IIMenu/IIMenuStories.svelte.d.ts} +3 -3
  8. package/dist/{IIDropdownMenu/IIDropdownMenuSub.svelte.d.ts → IIMenu/IIMenuSub.svelte.d.ts} +3 -3
  9. package/dist/{IIDropdownMenu/IIDropdownMenuSubSimple.svelte.d.ts → IIMenu/IIMenuSubSimple.svelte.d.ts} +3 -3
  10. package/dist/IIMenu/index.d.ts +1 -0
  11. package/dist/IIMenu/index.js +1 -0
  12. package/dist/IIModal/IIModal.svelte +23 -10
  13. package/dist/IIModal/IIModal.svelte.d.ts +6 -1
  14. package/dist/IIModal/IIModalFocusStories.svelte +12 -11
  15. package/dist/index.d.ts +3 -1
  16. package/dist/index.js +3 -1
  17. package/dist/style/base.css +23 -0
  18. package/dist/style/tailwind/animations.d.ts +3 -3
  19. package/dist/style/tailwind/animations.js +18 -7
  20. package/dist/style/tailwind/preset.d.ts +3 -3
  21. package/dist/utils/menu/MenuSearchInput.svelte +1 -1
  22. package/dist/utils/menu/menu-styles.d.ts +3 -3
  23. package/dist/utils/menu/menu-styles.js +6 -3
  24. package/dist/utils/menu/use-floating.svelte.d.ts +4 -0
  25. package/dist/utils/menu/use-floating.svelte.js +11 -5
  26. package/package.json +1 -1
  27. package/dist/IIDropdownMenu/index.d.ts +0 -1
  28. package/dist/IIDropdownMenu/index.js +0 -1
  29. /package/dist/{IIDropdownMenu/IIDropdownMenuSub.svelte → IIMenu/IIMenuSub.svelte} +0 -0
  30. /package/dist/{IIDropdownMenu/IIDropdownMenuSubSimple.svelte → IIMenu/IIMenuSubSimple.svelte} +0 -0
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type {Snippet} from 'svelte'
3
+ import {createAttachmentKey} from 'svelte/attachments'
3
4
  import {fly} from 'svelte/transition'
4
5
  import {IIIcon} from '../IIIcon'
5
6
  import {cn} from '../utils/cn'
@@ -18,15 +19,34 @@
18
19
  disabled?: boolean
19
20
  error?: boolean
20
21
  errorMessage?: string
22
+ /** Fired when the selected value changes. */
23
+ onValueChange?: (value: string) => void
24
+ /** @deprecated Use `onValueChange`. */
21
25
  onSelect?: (value: string) => void
22
26
  onblur?: () => void
23
27
  matchTriggerWidth?: boolean
24
28
  renderItem?: Snippet<[item: MenuItem, selected: boolean]>
25
29
  renderSelected?: Snippet<[item: MenuItem]>
30
+ /**
31
+ * Replace the default combobox field with a custom trigger. Spread `props`
32
+ * onto your trigger element/component — it carries the ref, click/keydown
33
+ * handlers, and combobox aria attributes. `open`, `selectedItem`, and
34
+ * `selectedLabel` are provided for rendering. When set, `class`, `label`,
35
+ * and `renderSelected` no longer affect the (now custom) trigger.
36
+ */
37
+ trigger?: Snippet<[{
38
+ props: Record<string, unknown>
39
+ open: boolean
40
+ selectedItem: MenuItem | undefined
41
+ selectedLabel: string
42
+ }]>
26
43
  searchable?: boolean
27
44
  searchPlaceholder?: string
28
45
  autofocus?: boolean
46
+ /** Classes for the trigger (root) element. */
29
47
  class?: string
48
+ /** Classes for the dropdown panel (popover surface). */
49
+ contentClass?: string
30
50
  }
31
51
 
32
52
  let {
@@ -37,15 +57,18 @@
37
57
  disabled = false,
38
58
  error = false,
39
59
  errorMessage,
60
+ onValueChange,
40
61
  onSelect,
41
62
  onblur,
42
63
  matchTriggerWidth = false,
43
64
  renderItem,
44
65
  renderSelected,
66
+ trigger,
45
67
  searchable = false,
46
68
  searchPlaceholder = 'Search...',
47
69
  autofocus = false,
48
70
  class: className,
71
+ contentClass,
49
72
  }: Props = $props()
50
73
 
51
74
  const showError = $derived(error || !!errorMessage)
@@ -79,6 +102,25 @@
79
102
  const selectedItem = $derived(items.find(i => i.value === value))
80
103
  const selectedLabel = $derived(selectedItem?.label ?? placeholder)
81
104
 
105
+ // Props a custom `trigger` snippet spreads onto its element. The attachment
106
+ // key and function are stable, so recomputing this derived object when `open`
107
+ // changes (for aria-expanded) does not re-run the ref attachment.
108
+ const attachTrigger = (node: Element) => {
109
+ triggerEl = node as HTMLElement
110
+ return () => { if (triggerEl === node) triggerEl = null }
111
+ }
112
+ const triggerAttachmentKey = createAttachmentKey()
113
+ const triggerProps = $derived({
114
+ role: 'combobox',
115
+ 'aria-haspopup': 'listbox',
116
+ 'aria-expanded': open,
117
+ type: 'button',
118
+ disabled: disabled || undefined,
119
+ onclick: toggle,
120
+ onkeydown: handleTriggerKeydown,
121
+ [triggerAttachmentKey]: attachTrigger,
122
+ })
123
+
82
124
  function scrollHighlightedIntoView() {
83
125
  requestAnimationFrame(() => {
84
126
  const el = floatingEl?.querySelector<HTMLElement>(`[role="option"][data-index="${highlightedIndex}"]`)
@@ -111,12 +153,14 @@
111
153
  placement: 'bottom-start',
112
154
  offset: 2,
113
155
  matchWidth: matchTriggerWidth,
156
+ constrainHeight: true,
114
157
  })
115
158
 
116
159
  function handleSelect(item: MenuItem) {
117
160
  if (item.disabled) return
118
161
  value = item.value
119
162
  open = false
163
+ onValueChange?.(item.value)
120
164
  onSelect?.(item.value)
121
165
  onblur?.()
122
166
  triggerEl?.focus()
@@ -151,6 +195,7 @@
151
195
  const match = items[matchIndex]
152
196
  if (!wasOpen) {
153
197
  value = match.value
198
+ onValueChange?.(match.value)
154
199
  onSelect?.(match.value)
155
200
  } else {
156
201
  highlightedIndex = matchIndex
@@ -239,6 +284,9 @@
239
284
  {#if label}
240
285
  <span class="text-small-emphasis text-secondary mb-4">{label}</span>
241
286
  {/if}
287
+ {#if trigger}
288
+ {@render trigger({props: triggerProps, open, selectedItem, selectedLabel})}
289
+ {:else}
242
290
  <button
243
291
  bind:this={triggerEl}
244
292
  type="button"
@@ -265,6 +313,7 @@
265
313
  {/if}
266
314
  <IIIcon iconName="caret-down" />
267
315
  </button>
316
+ {/if}
268
317
 
269
318
  {#if open}
270
319
  <div use:portal use:clickOutside={{onClose: close, ignore: () => triggerEl}}>
@@ -272,7 +321,7 @@
272
321
  bind:this={floatingEl}
273
322
  role="listbox"
274
323
  data-menu-content
275
- class="min-w-100 bg-dropdown-bg border-[0.5px] border-dropdown-border rounded-10 shadow-dropdown p-4 z-16 pointer-events-auto outline-none"
324
+ class={cn('flex flex-col min-w-100 bg-dropdown-bg border-[0.5px] border-dropdown-border rounded-10 shadow-dropdown p-4 z-16 pointer-events-auto outline-none overflow-hidden', contentClass)}
276
325
  transition:fly={{y: -4, duration: 150}}
277
326
  >
278
327
  {#if searchable}
@@ -283,7 +332,7 @@
283
332
  onkeydown={(e) => search.handleKeydown(e, '[data-menu-content]')}
284
333
  />
285
334
  {/if}
286
- <div class="max-h-300 overflow-y-auto">
335
+ <div class="ii-menu-scroll min-h-0 overflow-y-auto -mr-2">
287
336
  {#each (searchable ? search.filteredItems as MenuItem[] : items) as item, listIndex (item.value)}
288
337
  {@const index = searchable ? search.getItemIndex(item) : listIndex}
289
338
  {@const isHighlighted = searchable
@@ -8,15 +8,36 @@ type Props = {
8
8
  disabled?: boolean;
9
9
  error?: boolean;
10
10
  errorMessage?: string;
11
+ /** Fired when the selected value changes. */
12
+ onValueChange?: (value: string) => void;
13
+ /** @deprecated Use `onValueChange`. */
11
14
  onSelect?: (value: string) => void;
12
15
  onblur?: () => void;
13
16
  matchTriggerWidth?: boolean;
14
17
  renderItem?: Snippet<[item: MenuItem, selected: boolean]>;
15
18
  renderSelected?: Snippet<[item: MenuItem]>;
19
+ /**
20
+ * Replace the default combobox field with a custom trigger. Spread `props`
21
+ * onto your trigger element/component — it carries the ref, click/keydown
22
+ * handlers, and combobox aria attributes. `open`, `selectedItem`, and
23
+ * `selectedLabel` are provided for rendering. When set, `class`, `label`,
24
+ * and `renderSelected` no longer affect the (now custom) trigger.
25
+ */
26
+ trigger?: Snippet<[
27
+ {
28
+ props: Record<string, unknown>;
29
+ open: boolean;
30
+ selectedItem: MenuItem | undefined;
31
+ selectedLabel: string;
32
+ }
33
+ ]>;
16
34
  searchable?: boolean;
17
35
  searchPlaceholder?: string;
18
36
  autofocus?: boolean;
37
+ /** Classes for the trigger (root) element. */
19
38
  class?: string;
39
+ /** Classes for the dropdown panel (popover surface). */
40
+ contentClass?: string;
20
41
  };
21
42
  declare const IIDropdownInput: import("svelte").Component<Props, {}, "value">;
22
43
  type IIDropdownInput = ReturnType<typeof IIDropdownInput>;
@@ -1,10 +1,12 @@
1
1
  <script lang="ts">
2
2
  import IIDropdownInput from './IIDropdownInput.svelte'
3
3
  import {IIIcon} from '../IIIcon'
4
+ import {cn} from '../utils/cn'
4
5
  import type {IconName} from '../icons'
5
6
 
6
7
  let basicValue = $state<string | undefined>(undefined)
7
8
  let disabledItemValue = $state<string | undefined>(undefined)
9
+ let customTriggerValue = $state<string | undefined>(undefined)
8
10
 
9
11
  const statusItems = [
10
12
  {label: 'Active', value: 'active'},
@@ -85,4 +87,25 @@
85
87
  </IIDropdownInput>
86
88
  </div>
87
89
  </section>
90
+
91
+ <!-- Custom Trigger -->
92
+ <section>
93
+ <h2 class="text-default-emphasis text-primary mb-8">Custom Trigger</h2>
94
+ <p class="text-small text-secondary mb-12">Use the <code>trigger</code> snippet to replace the default field with any element. Spread <code>props</code> to wire up the ref, click/keyboard handling, and combobox aria attributes.</p>
95
+ <IIDropdownInput items={statusItems} bind:value={customTriggerValue}>
96
+ {#snippet trigger({props, open, selectedItem, selectedLabel})}
97
+ <button
98
+ {...props}
99
+ class={cn(
100
+ 'inline-flex items-center gap-6 py-6 px-12 rounded-control border border-button-secondary bg-input-bg text-small text-button-secondary cursor-default outline-none transition-colors hover:border-button-secondary-hover focus-visible:ring-3 focus-visible:ring-primary',
101
+ open && 'border-button-secondary-hover'
102
+ )}
103
+ >
104
+ <IIIcon iconName="funnel" class="w-14 h-14" />
105
+ <span>{selectedItem ? selectedLabel : 'Filter'}</span>
106
+ <IIIcon iconName="caret-down" class="w-12 h-12" />
107
+ </button>
108
+ {/snippet}
109
+ </IIDropdownInput>
110
+ </section>
88
111
  </div>
@@ -9,8 +9,8 @@
9
9
  import MenuSearchInput from '../utils/menu/MenuSearchInput.svelte'
10
10
  import MenuNoResults from '../utils/menu/MenuNoResults.svelte'
11
11
  import MenuItemTooltip from '../utils/menu/MenuItemTooltip.svelte'
12
- import IIDropdownMenuSub from './IIDropdownMenuSub.svelte'
13
- import IIDropdownMenuSubSimple from './IIDropdownMenuSubSimple.svelte'
12
+ import IIMenuSub from './IIMenuSub.svelte'
13
+ import IIMenuSubSimple from './IIMenuSubSimple.svelte'
14
14
 
15
15
  type Props = {
16
16
  items: MenuEntry[]
@@ -21,10 +21,14 @@
21
21
  side?: 'top' | 'right' | 'bottom' | 'left'
22
22
  align?: 'start' | 'center' | 'end'
23
23
  collisionPadding?: number
24
+ /** @deprecated Use `class` — it now targets the trigger. */
24
25
  triggerClass?: string
25
26
  searchable?: boolean
26
27
  searchPlaceholder?: string
28
+ /** Classes for the trigger (root) element. */
27
29
  class?: string
30
+ /** Classes for the menu panel (popover surface). */
31
+ contentClass?: string
28
32
  }
29
33
 
30
34
  let {
@@ -40,6 +44,7 @@
40
44
  searchable = false,
41
45
  searchPlaceholder = 'Search...',
42
46
  class: className,
47
+ contentClass,
43
48
  }: Props = $props()
44
49
 
45
50
  let triggerEl = $state<HTMLElement | null>(null)
@@ -64,6 +69,7 @@
64
69
  placement: placement as any,
65
70
  offset: 4,
66
71
  shift: {padding: collisionPadding},
72
+ constrainHeight: true,
67
73
  })
68
74
 
69
75
  const search = createMenuSearch({
@@ -270,7 +276,7 @@
270
276
  </div>
271
277
  {#if isOpen}
272
278
  {#if entry.searchable}
273
- <IIDropdownMenuSub
279
+ <IIMenuSub
274
280
  items={entry.items}
275
281
  onSelect={handleSelect}
276
282
  onClose={() => {
@@ -282,7 +288,7 @@
282
288
  renderItemContent={itemContent}
283
289
  />
284
290
  {:else}
285
- <IIDropdownMenuSubSimple
291
+ <IIMenuSubSimple
286
292
  items={entry.items}
287
293
  onSelect={handleSelect}
288
294
  onClose={() => {
@@ -331,7 +337,7 @@
331
337
  aria-haspopup="menu"
332
338
  aria-expanded={open}
333
339
  data-state={open ? 'open' : 'closed'}
334
- class={cn(children ? cn('[all:unset] cursor-default', triggerClass) : defaultTriggerClass)}
340
+ class={cn(children ? '[all:unset] cursor-default' : defaultTriggerClass, triggerClass, className)}
335
341
  onclick={toggle}
336
342
  >
337
343
  {#if children}
@@ -347,7 +353,7 @@
347
353
  bind:this={floatingEl}
348
354
  role="menu"
349
355
  data-menu-content
350
- class={cn(menuStyles.content, className)}
356
+ class={cn(menuStyles.content, contentClass)}
351
357
  onkeydown={handleMenuKeydown}
352
358
  transition:fly={{y: -4, duration: 150}}
353
359
  >
@@ -9,11 +9,15 @@ type Props = {
9
9
  side?: 'top' | 'right' | 'bottom' | 'left';
10
10
  align?: 'start' | 'center' | 'end';
11
11
  collisionPadding?: number;
12
+ /** @deprecated Use `class` — it now targets the trigger. */
12
13
  triggerClass?: string;
13
14
  searchable?: boolean;
14
15
  searchPlaceholder?: string;
16
+ /** Classes for the trigger (root) element. */
15
17
  class?: string;
18
+ /** Classes for the menu panel (popover surface). */
19
+ contentClass?: string;
16
20
  };
17
- declare const IIDropdownMenu: import("svelte").Component<Props, {}, "open">;
18
- type IIDropdownMenu = ReturnType<typeof IIDropdownMenu>;
19
- export default IIDropdownMenu;
21
+ declare const IIMenu: import("svelte").Component<Props, {}, "open">;
22
+ type IIMenu = ReturnType<typeof IIMenu>;
23
+ export default IIMenu;
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import IIDropdownMenu from './IIDropdownMenu.svelte'
2
+ import IIMenu from './IIMenu.svelte'
3
3
  import {IIButton} from '../IIButton'
4
4
  import {IIIcon} from '../IIIcon'
5
5
 
@@ -13,7 +13,7 @@
13
13
  <section>
14
14
  <h2 class="text-default-emphasis text-primary mb-8">With Separators</h2>
15
15
  <p class="text-small text-secondary mb-12">Menu entries can include separator dividers.</p>
16
- <IIDropdownMenu
16
+ <IIMenu
17
17
  items={[
18
18
  {label: 'Edit', value: 'edit'},
19
19
  {label: 'Duplicate', value: 'duplicate'},
@@ -29,7 +29,7 @@
29
29
  <section>
30
30
  <h2 class="text-default-emphasis text-primary mb-8">With Groups</h2>
31
31
  <p class="text-small text-secondary mb-12">Items organized into labeled groups with headings.</p>
32
- <IIDropdownMenu
32
+ <IIMenu
33
33
  items={[
34
34
  {
35
35
  type: 'group',
@@ -57,7 +57,7 @@
57
57
  <section>
58
58
  <h2 class="text-default-emphasis text-primary mb-8">Custom Trigger</h2>
59
59
  <p class="text-small text-secondary mb-12">Using the children snippet with triggerClass for custom trigger styling.</p>
60
- <IIDropdownMenu
60
+ <IIMenu
61
61
  items={[
62
62
  {label: 'Profile', value: 'profile'},
63
63
  {label: 'Settings', value: 'settings'},
@@ -72,14 +72,14 @@
72
72
  <span>Account</span>
73
73
  <IIIcon iconName="caret-down" class="w-12 h-12" />
74
74
  {/snippet}
75
- </IIDropdownMenu>
75
+ </IIMenu>
76
76
  </section>
77
77
 
78
78
  <!-- Searchable -->
79
79
  <section>
80
80
  <h2 class="text-default-emphasis text-primary mb-8">Searchable</h2>
81
81
  <p class="text-small text-secondary mb-12">A search input at the top filters menu items as you type.</p>
82
- <IIDropdownMenu
82
+ <IIMenu
83
83
  items={[
84
84
  {label: 'Edit', value: 'edit'},
85
85
  {label: 'Duplicate', value: 'duplicate'},
@@ -98,7 +98,7 @@
98
98
  <!-- Mixed Entries -->
99
99
  <section>
100
100
  <h2 class="text-default-emphasis text-primary mb-8">Mixed: Items + Separators + Groups</h2>
101
- <IIDropdownMenu
101
+ <IIMenu
102
102
  items={[
103
103
  {label: 'Quick Action', value: 'quick'},
104
104
  {type: 'separator'},
@@ -11,8 +11,8 @@ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> =
11
11
  };
12
12
  z_$$bindings?: Bindings;
13
13
  }
14
- declare const IIDropdownMenuStories: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
14
+ declare const IIMenuStories: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
15
15
  [evt: string]: CustomEvent<any>;
16
16
  }, {}, {}, string>;
17
- type IIDropdownMenuStories = InstanceType<typeof IIDropdownMenuStories>;
18
- export default IIDropdownMenuStories;
17
+ type IIMenuStories = InstanceType<typeof IIMenuStories>;
18
+ export default IIMenuStories;
@@ -8,6 +8,6 @@ type Props = {
8
8
  searchPlaceholder?: string;
9
9
  renderItemContent: Snippet<[MenuItem]>;
10
10
  };
11
- declare const IIDropdownMenuSub: import("svelte").Component<Props, {}, "">;
12
- type IIDropdownMenuSub = ReturnType<typeof IIDropdownMenuSub>;
13
- export default IIDropdownMenuSub;
11
+ declare const IIMenuSub: import("svelte").Component<Props, {}, "">;
12
+ type IIMenuSub = ReturnType<typeof IIMenuSub>;
13
+ export default IIMenuSub;
@@ -8,6 +8,6 @@ type Props = {
8
8
  renderItemContent: Snippet<[MenuItem]>;
9
9
  renderSubMenu: Snippet<[SubEntry]>;
10
10
  };
11
- declare const IIDropdownMenuSubSimple: import("svelte").Component<Props, {}, "">;
12
- type IIDropdownMenuSubSimple = ReturnType<typeof IIDropdownMenuSubSimple>;
13
- export default IIDropdownMenuSubSimple;
11
+ declare const IIMenuSubSimple: import("svelte").Component<Props, {}, "">;
12
+ type IIMenuSubSimple = ReturnType<typeof IIMenuSubSimple>;
13
+ export default IIMenuSubSimple;
@@ -0,0 +1 @@
1
+ export { default as IIMenu } from './IIMenu.svelte';
@@ -0,0 +1 @@
1
+ export { default as IIMenu } from './IIMenu.svelte';
@@ -15,7 +15,12 @@
15
15
  showCloseButton?: boolean
16
16
  size?: 'sm' | 'md' | 'lg' | 'xl' | 'full'
17
17
  overlay?: 'dark' | 'light' | 'none'
18
- /** Cap the modal height (any CSS value, e.g. "60vh"). Defaults to 90vh. */
18
+ /** Vertically center the modal instead of anchoring it 15% from the top. */
19
+ centered?: boolean
20
+ /** Cap the modal height (any CSS value, e.g. "60vh"). Defaults to 78vh
21
+ * (90vh when `centered`). Note: when anchored (not centered) the modal
22
+ * sits 15% from the top, so a value above ~83vh pushes the bottom edge
23
+ * off-screen. */
19
24
  maxHeight?: string
20
25
  /** Replace the body wrapper's padding/scroll defaults — e.g. for a full-height flex body. */
21
26
  bodyClass?: string
@@ -33,6 +38,7 @@
33
38
  showCloseButton = true,
34
39
  size = 'md',
35
40
  overlay = 'dark',
41
+ centered = false,
36
42
  maxHeight,
37
43
  bodyClass,
38
44
  class: className,
@@ -43,7 +49,7 @@
43
49
  onOpenChange?.(newOpen)
44
50
  }
45
51
 
46
- // Portaled menu/listbox content (IIDropdownMenu, bits-ui DropdownMenu/Popover/Select, etc.)
52
+ // Portaled menu/listbox content (IIMenu, bits-ui DropdownMenu/Popover/Select, etc.)
47
53
  // lives in document.body outside Dialog.Content, so bits-ui's DismissibleLayer would treat
48
54
  // interactions with them as "outside" the modal and close it. Preserve those events.
49
55
  function isInsidePortaledMenu(target: Element | null): boolean {
@@ -61,7 +67,7 @@
61
67
  <Dialog.Portal>
62
68
  <Dialog.Overlay
63
69
  class={cn(
64
- 'fixed inset-0 z-14 data-[state=open]:animate-fade-in data-[state=closed]:animate-fade-out motion-reduce:animate-none',
70
+ 'fixed inset-0 z-14 data-[state=open]:animate-modal-overlay-in data-[state=closed]:animate-modal-overlay-out motion-reduce:animate-none',
65
71
  overlay === 'dark' && 'bg-black/30',
66
72
  overlay === 'light' && 'bg-black/10',
67
73
  overlay === 'none' && 'pointer-events-none'
@@ -78,16 +84,23 @@
78
84
  if (isInsidePortaledMenu(target)) e.preventDefault()
79
85
  }}
80
86
  class={cn(
81
- // Centered vertically (top-1/2 + -translate-y-1/2) so a tall modal stays within the
82
- // viewport: with max-h-[90vh] its edges land at ~5vh/95vh. The previous top-[30%]
83
- // anchor pushed the top edge off-screen once a modal grew past ~60vh tall.
84
- 'fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-surface border border-strong rounded-12 w-[calc(100%-20px)] max-h-[90vh] flex flex-col z-15 data-[state=open]:animate-modal-in data-[state=closed]:animate-modal-out motion-reduce:animate-none focus:outline-none',
85
- overlay === 'none' ? 'shadow-floating' : 'shadow-modal',
87
+ // Centering is deliberately kept OFF `transform`: horizontal uses left-0/right-0 +
88
+ // mx-auto, and vertical centering (when `centered`) uses the standalone `translate`
89
+ // property both compose with the open/close slide animation, which owns `transform`
90
+ // (translateY). A transform-based center would be clobbered by the animation and
91
+ // shift the modal for the animation's duration.
92
+ 'fixed left-0 right-0 mx-auto bg-surface border border-primary rounded-12 w-[calc(100%-20px)] flex flex-col z-15 data-[state=open]:animate-modal-in data-[state=closed]:animate-modal-out motion-reduce:animate-none focus:outline-none',
93
+ // Vertical: anchored 15% from the top (default) vs. centered. When anchored, height
94
+ // is capped so a tall modal still clears the bottom (15% + 78vh leaves ~7vh below);
95
+ // centered can safely use the full 90vh since -50% keeps it within the viewport.
96
+ centered ? 'top-1/2 [translate:0_-50%] max-h-[90vh]' : 'top-[15%] max-h-[78vh]',
97
+ // Same edge for every overlay: shadow-modal (previously no-overlay used shadow-floating).
98
+ 'shadow-modal',
86
99
  size === 'sm' && 'max-w-400',
87
100
  size === 'md' && 'max-w-500',
88
101
  size === 'lg' && 'max-w-700',
89
102
  size === 'xl' && 'max-w-[1100px]',
90
- size === 'full' && 'max-w-[95vw] h-[90vh]',
103
+ size === 'full' && 'max-w-[95vw] h-[78vh]',
91
104
  className
92
105
  )}
93
106
  style={maxHeight ? `max-height: ${maxHeight}` : undefined}
@@ -95,7 +108,7 @@
95
108
  <div class="flex items-start justify-between gap-12 px-24 pt-24 shrink-0">
96
109
  <div class="flex flex-col min-w-0 flex-1">
97
110
  <div class="flex items-center gap-12 min-w-0">
98
- <Dialog.Title class="text-h2 text-body m-0 shrink-0">{title}</Dialog.Title>
111
+ <Dialog.Title class="text-h3 text-body m-0 shrink-0">{title}</Dialog.Title>
99
112
  {#if titleAccessory}
100
113
  {@render titleAccessory()}
101
114
  {/if}
@@ -10,7 +10,12 @@ type Props = {
10
10
  showCloseButton?: boolean;
11
11
  size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
12
12
  overlay?: 'dark' | 'light' | 'none';
13
- /** Cap the modal height (any CSS value, e.g. "60vh"). Defaults to 90vh. */
13
+ /** Vertically center the modal instead of anchoring it 15% from the top. */
14
+ centered?: boolean;
15
+ /** Cap the modal height (any CSS value, e.g. "60vh"). Defaults to 78vh
16
+ * (90vh when `centered`). Note: when anchored (not centered) the modal
17
+ * sits 15% from the top, so a value above ~83vh pushes the bottom edge
18
+ * off-screen. */
14
19
  maxHeight?: string;
15
20
  /** Replace the body wrapper's padding/scroll defaults — e.g. for a full-height flex body. */
16
21
  bodyClass?: string;
@@ -19,14 +19,15 @@
19
19
 
20
20
  <IIButton onclick={() => (open = true)}>Open modal</IIButton>
21
21
 
22
- {#if open}
23
- <IIModal bind:open title="Add a note" onOpenChange={v => (open = v)}>
24
- <IITextarea
25
- bind:ref
26
- bind:value
27
- label="Note"
28
- placeholder="The focus ring should be fully visible on the left, right, and bottom."
29
- rows={4}
30
- />
31
- </IIModal>
32
- {/if}
22
+ <!-- Keep IIModal mounted unconditionally (no {#if open}) so bits-ui's Dialog can
23
+ play its exit animation on close wrapping it in {#if} unmounts the whole
24
+ dialog instantly and skips the modal-out/fade-out transitions. -->
25
+ <IIModal bind:open title="Add a note" onOpenChange={v => (open = v)}>
26
+ <IITextarea
27
+ bind:ref
28
+ bind:value
29
+ label="Note"
30
+ placeholder="The focus ring should be fully visible on the left, right, and bottom."
31
+ rows={4}
32
+ />
33
+ </IIModal>
package/dist/index.d.ts CHANGED
@@ -16,7 +16,9 @@ export { IICombobox } from './IICombobox';
16
16
  export { IIContextMenu } from './IIContextMenu';
17
17
  export { IIDateInput } from './IIDateInput';
18
18
  export { IIDropdownInput } from './IIDropdownInput';
19
- export { IIDropdownMenu } from './IIDropdownMenu';
19
+ export { IIMenu } from './IIMenu';
20
+ /** @deprecated Renamed to `IIMenu` (it is an action menu, not a select). Use `IIMenu`. */
21
+ export { IIMenu as IIDropdownMenu } from './IIMenu';
20
22
  export { IIEditableBadges } from './IIEditableBadges';
21
23
  export { IIEditableText } from './IIEditableText';
22
24
  export { IIEmptyState } from './IIEmptyState';
package/dist/index.js CHANGED
@@ -20,7 +20,9 @@ export { IICombobox } from './IICombobox';
20
20
  export { IIContextMenu } from './IIContextMenu';
21
21
  export { IIDateInput } from './IIDateInput';
22
22
  export { IIDropdownInput } from './IIDropdownInput';
23
- export { IIDropdownMenu } from './IIDropdownMenu';
23
+ export { IIMenu } from './IIMenu';
24
+ /** @deprecated Renamed to `IIMenu` (it is an action menu, not a select). Use `IIMenu`. */
25
+ export { IIMenu as IIDropdownMenu } from './IIMenu';
24
26
  export { IIEditableBadges } from './IIEditableBadges';
25
27
  export { IIEditableText } from './IIEditableText';
26
28
  export { IIEmptyState } from './IIEmptyState';
@@ -77,3 +77,26 @@
77
77
  background-color: var(--ii-gray-400);
78
78
  }
79
79
  }
80
+
81
+ /* Shared narrow, rounded scrollbar for menu/dropdown scroll areas. Unlayered so
82
+ it outranks the @layer base `*` rules above. Setting scrollbar-width/color to
83
+ auto opts this element back into ::-webkit-scrollbar styling — the global
84
+ `scrollbar-width: thin` otherwise disables the WebKit pseudo-elements in
85
+ Chromium, so a custom (narrow) thumb width can't be applied. */
86
+ .ii-menu-scroll {
87
+ scrollbar-width: auto;
88
+ scrollbar-color: auto;
89
+ }
90
+ .ii-menu-scroll::-webkit-scrollbar {
91
+ width: 4px;
92
+ }
93
+ .ii-menu-scroll::-webkit-scrollbar-track {
94
+ background: transparent;
95
+ }
96
+ .ii-menu-scroll::-webkit-scrollbar-thumb {
97
+ background-color: var(--ii-gray-300);
98
+ border-radius: 9999px;
99
+ }
100
+ .ii-menu-scroll::-webkit-scrollbar-thumb:hover {
101
+ background-color: var(--ii-gray-400);
102
+ }
@@ -25,13 +25,11 @@ export declare const keyframes: {
25
25
  transform: string;
26
26
  };
27
27
  };
28
- modalIn: {
28
+ modalSlideIn: {
29
29
  from: {
30
- opacity: string;
31
30
  transform: string;
32
31
  };
33
32
  to: {
34
- opacity: string;
35
33
  transform: string;
36
34
  };
37
35
  };
@@ -161,6 +159,8 @@ export declare const animation: {
161
159
  skeleton: string;
162
160
  'modal-in': string;
163
161
  'modal-out': string;
162
+ 'modal-overlay-in': string;
163
+ 'modal-overlay-out': string;
164
164
  'fade-out': string;
165
165
  'login-fade-in-up': string;
166
166
  'login-pulse': string;
@@ -12,9 +12,13 @@ export const keyframes = {
12
12
  '0%': { transform: 'translate(-50%, -50%) rotate(0deg) translateX(24px)' },
13
13
  '100%': { transform: 'translate(-50%, -50%) rotate(360deg) translateX(24px)' },
14
14
  },
15
- modalIn: {
16
- from: { opacity: '0', transform: 'scale(0.96)' },
17
- to: { opacity: '1', transform: 'scale(1)' },
15
+ modalSlideIn: {
16
+ // Transform-only slide into place. Opacity is handled by a separate, faster
17
+ // `fadeIn` animation (see modal-in) so the fade can match the overlay while
18
+ // the slide runs on its own long duration. (IIModal centers via mx-auto, not
19
+ // transform, so translateY here is free of the horizontal centering.)
20
+ from: { transform: 'translateY(-10px)' },
21
+ to: { transform: 'translateY(0)' },
18
22
  },
19
23
  spin: {
20
24
  to: { transform: 'rotate(360deg)' },
@@ -60,8 +64,9 @@ export const keyframes = {
60
64
  to: { opacity: '1', transform: 'translateY(0)' },
61
65
  },
62
66
  modalOut: {
63
- from: { opacity: '1', transform: 'scale(1)' },
64
- to: { opacity: '0', transform: 'scale(0.96)' },
67
+ // Fade out while sliding up.
68
+ from: { opacity: '1', transform: 'translateY(0)' },
69
+ to: { opacity: '0', transform: 'translateY(-6px)' },
65
70
  },
66
71
  fadeOut: {
67
72
  from: { opacity: '1' },
@@ -76,8 +81,14 @@ export const animation = {
76
81
  shake: 'shake 0.4s ease-out',
77
82
  shimmer: 'shimmer 1.5s infinite',
78
83
  skeleton: 'skeleton-loading 1.2s ease-in-out infinite',
79
- 'modal-in': 'modalIn 200ms ease-out',
80
- 'modal-out': 'modalOut 150ms ease-in',
84
+ // Two decoupled animations: the opacity fade matches the overlay exactly
85
+ // (fadeIn 130ms ease-out), while the slide glides in slowly over 2500ms.
86
+ 'modal-in': 'fadeIn 130ms ease-out, modalSlideIn 2500ms cubic-bezier(0.1, 1, 0.1, 1)',
87
+ 'modal-out': 'modalOut 90ms ease-in',
88
+ // Modal-specific overlay fades — snappy, to match the modal's timing rather
89
+ // than the slower shared fade-in/fade-out used by menus, tooltips, etc.
90
+ 'modal-overlay-in': 'fadeIn 130ms ease-out',
91
+ 'modal-overlay-out': 'fadeOut 90ms ease-in',
81
92
  'fade-out': 'fadeOut 150ms ease-in',
82
93
  'login-fade-in-up': 'loginFadeInUp 0.6s ease-out',
83
94
  'login-pulse': 'loginPulse 2s ease-in-out infinite',
@@ -442,13 +442,11 @@ declare const _default: {
442
442
  transform: string;
443
443
  };
444
444
  };
445
- modalIn: {
445
+ modalSlideIn: {
446
446
  from: {
447
- opacity: string;
448
447
  transform: string;
449
448
  };
450
449
  to: {
451
- opacity: string;
452
450
  transform: string;
453
451
  };
454
452
  };
@@ -578,6 +576,8 @@ declare const _default: {
578
576
  skeleton: string;
579
577
  'modal-in': string;
580
578
  'modal-out': string;
579
+ 'modal-overlay-in': string;
580
+ 'modal-overlay-out': string;
581
581
  'fade-out': string;
582
582
  'login-fade-in-up': string;
583
583
  'login-pulse': string;
@@ -14,7 +14,7 @@
14
14
  }: Props = $props()
15
15
  </script>
16
16
 
17
- <div class="-mx-4 -mt-4 px-14 mb-4 border-b-[0.5px] border-dropdown-border">
17
+ <div class="shrink-0 -mx-4 -mt-4 px-14 mb-4 border-b-[0.5px] border-dropdown-border">
18
18
  <input
19
19
  bind:this={inputEl}
20
20
  bind:value={searchQuery}
@@ -2,10 +2,10 @@ export declare const menuStyles: {
2
2
  readonly item: "flex items-center gap-8 px-12 py-8 rounded-4 text-small cursor-default select-none outline-none data-[disabled]:opacity-50 motion-reduce:transition-none";
3
3
  readonly itemDefault: "text-dropdown-item hover:bg-dropdown-item-hover focus:bg-dropdown-item-hover data-[highlighted]:bg-dropdown-item-hover data-[highlighted]:outline-none data-[state=open]:bg-dropdown-item-hover data-[disabled]:hover:bg-transparent data-[disabled]:focus:bg-transparent";
4
4
  readonly itemDestructive: "text-error hover:bg-error-bg focus:bg-error-bg data-[highlighted]:bg-error-bg data-[highlighted]:outline-none data-[disabled]:hover:bg-transparent data-[disabled]:focus:bg-transparent";
5
- readonly content: "min-w-48 max-h-300 overflow-y-auto bg-dropdown-bg border-[0.5px] border-dropdown-border rounded-10 shadow-dropdown p-4 z-12 pointer-events-auto outline-none";
6
- readonly subContent: "min-w-48 max-h-300 overflow-y-auto bg-dropdown-bg border-[0.5px] border-dropdown-border rounded-10 shadow-submenu p-4 z-12 outline-none animate-fade-in motion-reduce:animate-none";
5
+ readonly content: "ii-menu-scroll min-w-48 overflow-y-auto bg-dropdown-bg border-[0.5px] border-dropdown-border rounded-10 shadow-dropdown p-4 z-12 pointer-events-auto outline-none";
6
+ readonly subContent: "ii-menu-scroll min-w-48 max-h-300 overflow-y-auto bg-dropdown-bg border-[0.5px] border-dropdown-border rounded-10 shadow-submenu p-4 z-12 outline-none animate-fade-in motion-reduce:animate-none";
7
7
  readonly searchableSubContent: "min-w-48 bg-dropdown-bg border-[0.5px] border-dropdown-border rounded-10 shadow-submenu p-4 z-12 pointer-events-auto outline-none overflow-hidden animate-fade-in motion-reduce:animate-none";
8
- readonly scrollableItems: "max-h-250 overflow-y-auto overflow-x-hidden";
8
+ readonly scrollableItems: "ii-menu-scroll max-h-250 overflow-y-auto overflow-x-hidden";
9
9
  readonly separator: "h-1 bg-muted -mx-4 my-4";
10
10
  readonly groupHeading: "text-tiny-emphasis text-secondary px-12 py-4 uppercase select-none";
11
11
  };
@@ -3,10 +3,13 @@ export const menuStyles = {
3
3
  item: 'flex items-center gap-8 px-12 py-8 rounded-4 text-small cursor-default select-none outline-none data-[disabled]:opacity-50 motion-reduce:transition-none',
4
4
  itemDefault: 'text-dropdown-item hover:bg-dropdown-item-hover focus:bg-dropdown-item-hover data-[highlighted]:bg-dropdown-item-hover data-[highlighted]:outline-none data-[state=open]:bg-dropdown-item-hover data-[disabled]:hover:bg-transparent data-[disabled]:focus:bg-transparent',
5
5
  itemDestructive: 'text-error hover:bg-error-bg focus:bg-error-bg data-[highlighted]:bg-error-bg data-[highlighted]:outline-none data-[disabled]:hover:bg-transparent data-[disabled]:focus:bg-transparent',
6
- content: 'min-w-48 max-h-300 overflow-y-auto bg-dropdown-bg border-[0.5px] border-dropdown-border rounded-10 shadow-dropdown p-4 z-12 pointer-events-auto outline-none',
7
- subContent: 'min-w-48 max-h-300 overflow-y-auto bg-dropdown-bg border-[0.5px] border-dropdown-border rounded-10 shadow-submenu p-4 z-12 outline-none animate-fade-in motion-reduce:animate-none',
6
+ // content: no fixed max-height IIMenu passes constrainHeight to
7
+ // useFloating, which sizes maxHeight to the available viewport space so the
8
+ // menu grows to fit its items and only scrolls near the screen edge.
9
+ content: 'ii-menu-scroll min-w-48 overflow-y-auto bg-dropdown-bg border-[0.5px] border-dropdown-border rounded-10 shadow-dropdown p-4 z-12 pointer-events-auto outline-none',
10
+ subContent: 'ii-menu-scroll min-w-48 max-h-300 overflow-y-auto bg-dropdown-bg border-[0.5px] border-dropdown-border rounded-10 shadow-submenu p-4 z-12 outline-none animate-fade-in motion-reduce:animate-none',
8
11
  searchableSubContent: 'min-w-48 bg-dropdown-bg border-[0.5px] border-dropdown-border rounded-10 shadow-submenu p-4 z-12 pointer-events-auto outline-none overflow-hidden animate-fade-in motion-reduce:animate-none',
9
- scrollableItems: 'max-h-250 overflow-y-auto overflow-x-hidden',
12
+ scrollableItems: 'ii-menu-scroll max-h-250 overflow-y-auto overflow-x-hidden',
10
13
  separator: 'h-1 bg-muted -mx-4 my-4',
11
14
  groupHeading: 'text-tiny-emphasis text-secondary px-12 py-4 uppercase select-none',
12
15
  };
@@ -9,5 +9,9 @@ export type UseFloatingOptions = {
9
9
  padding?: number;
10
10
  };
11
11
  matchWidth?: boolean;
12
+ /** Constrain the floating element's height to the space available in the
13
+ * viewport (via the size middleware). Lets the panel grow to fit its
14
+ * content and only cap/scroll when it would run off-screen. */
15
+ constrainHeight?: boolean;
12
16
  };
13
17
  export declare function useFloating(opts: UseFloatingOptions): void;
@@ -13,12 +13,18 @@ export function useFloating(opts) {
13
13
  const padding = typeof opts.shift === 'object' ? opts.shift.padding : 8;
14
14
  middleware.push(shiftMiddleware({ padding }));
15
15
  }
16
- if (opts.matchWidth) {
16
+ if (opts.matchWidth || opts.constrainHeight) {
17
17
  middleware.push(sizeMiddleware({
18
- apply({ rects }) {
19
- Object.assign(floating.style, {
20
- minWidth: `${rects.reference.width}px`,
21
- });
18
+ padding: 8,
19
+ apply({ rects, availableHeight }) {
20
+ const styles = {};
21
+ if (opts.matchWidth) {
22
+ styles.minWidth = `${rects.reference.width}px`;
23
+ }
24
+ if (opts.constrainHeight) {
25
+ styles.maxHeight = `${availableHeight}px`;
26
+ }
27
+ Object.assign(floating.style, styles);
22
28
  },
23
29
  }));
24
30
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@insymetri/styleguide",
3
- "version": "0.1.78",
3
+ "version": "0.1.80",
4
4
  "description": "Insymetri shared UI component library built with Svelte 5",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -1 +0,0 @@
1
- export { default as IIDropdownMenu } from './IIDropdownMenu.svelte';
@@ -1 +0,0 @@
1
- export { default as IIDropdownMenu } from './IIDropdownMenu.svelte';