@dryui/ui 1.5.1 → 1.6.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.
Files changed (85) hide show
  1. package/dist/button-group/context.svelte.js +4 -7
  2. package/dist/calendar/calendar-root.svelte +15 -32
  3. package/dist/chip-group/context.svelte.d.ts +2 -4
  4. package/dist/chip-group/context.svelte.js +2 -9
  5. package/dist/context-menu/context-menu-content.svelte +24 -12
  6. package/dist/context-menu/context-menu-group.svelte +3 -2
  7. package/dist/context-menu/context-menu-item.svelte +8 -61
  8. package/dist/context-menu/context-menu-label.svelte +3 -11
  9. package/dist/context-menu/context-menu-root.svelte +10 -29
  10. package/dist/context-menu/context-menu-separator.svelte +2 -9
  11. package/dist/context-menu/context.svelte.d.ts +2 -12
  12. package/dist/date-picker/datepicker-content.svelte +11 -81
  13. package/dist/date-picker/datepicker-content.svelte.d.ts +1 -1
  14. package/dist/date-picker/datepicker-input-root.svelte +39 -47
  15. package/dist/date-range-picker/date-range-picker-content.svelte +11 -75
  16. package/dist/date-range-picker/date-range-picker-content.svelte.d.ts +1 -1
  17. package/dist/date-range-picker/date-range-picker-root.svelte +44 -49
  18. package/dist/drag-and-drop/group-context.svelte.d.ts +1 -1
  19. package/dist/drag-and-drop/group-context.svelte.js +4 -4
  20. package/dist/dropdown-menu/context.svelte.d.ts +2 -8
  21. package/dist/dropdown-menu/dropdown-menu-content.svelte +15 -3
  22. package/dist/dropdown-menu/dropdown-menu-group.svelte +3 -2
  23. package/dist/dropdown-menu/dropdown-menu-item.svelte +8 -61
  24. package/dist/dropdown-menu/dropdown-menu-label.svelte +3 -11
  25. package/dist/dropdown-menu/dropdown-menu-root.svelte +10 -21
  26. package/dist/dropdown-menu/dropdown-menu-separator.svelte +2 -9
  27. package/dist/flip-card/context.svelte.d.ts +5 -0
  28. package/dist/flip-card/context.svelte.js +2 -0
  29. package/dist/flip-card/flip-card-back.svelte +2 -2
  30. package/dist/flip-card/flip-card-root.svelte +42 -15
  31. package/dist/heading/heading.svelte +10 -1
  32. package/dist/heading/heading.svelte.d.ts +1 -0
  33. package/dist/heading/index.d.ts +1 -0
  34. package/dist/hover-card/hover-card-content.svelte +9 -21
  35. package/dist/hover-card/hover-card-root.svelte +2 -2
  36. package/dist/hover-card/hover-card-root.svelte.d.ts +4 -0
  37. package/dist/index.d.ts +2 -0
  38. package/dist/index.js +1 -0
  39. package/dist/internal/anchored-overlay-content.svelte.d.ts +20 -0
  40. package/dist/internal/anchored-overlay-content.svelte.js +28 -0
  41. package/dist/internal/date-family-controller.svelte.d.ts +45 -0
  42. package/dist/internal/date-family-controller.svelte.js +99 -0
  43. package/dist/internal/menu-group.svelte +15 -0
  44. package/dist/internal/menu-group.svelte.d.ts +9 -0
  45. package/dist/internal/menu-item.svelte +82 -0
  46. package/dist/internal/menu-item.svelte.d.ts +11 -0
  47. package/dist/internal/menu-label.svelte +24 -0
  48. package/dist/internal/menu-label.svelte.d.ts +9 -0
  49. package/dist/internal/menu-root-state.svelte.d.ts +24 -0
  50. package/dist/internal/menu-root-state.svelte.js +42 -0
  51. package/dist/internal/menu-separator.svelte +19 -0
  52. package/dist/internal/menu-separator.svelte.d.ts +7 -0
  53. package/dist/internal/motion.js +12 -1
  54. package/dist/internal/picker-popover-content.svelte +112 -0
  55. package/dist/internal/picker-popover-content.svelte.d.ts +16 -0
  56. package/dist/link-preview/link-preview-content.svelte +7 -10
  57. package/dist/list/list-item-icon.svelte +3 -3
  58. package/dist/list/list-item-icon.svelte.d.ts +1 -1
  59. package/dist/list/list-item-text.svelte +3 -3
  60. package/dist/list/list-item-text.svelte.d.ts +1 -1
  61. package/dist/list/list-item.svelte +58 -35
  62. package/dist/list/list-item.svelte.d.ts +8 -2
  63. package/dist/popover/popover-content.svelte +9 -11
  64. package/dist/range-calendar/range-calendar-root.svelte +13 -19
  65. package/dist/text/index.d.ts +1 -0
  66. package/dist/text/text.svelte +3 -1
  67. package/dist/text/text.svelte.d.ts +1 -0
  68. package/dist/theme-toggle/index.d.ts +18 -0
  69. package/dist/theme-toggle/index.js +3 -0
  70. package/dist/theme-toggle/theme-controller.svelte.d.ts +54 -0
  71. package/dist/theme-toggle/theme-controller.svelte.js +121 -0
  72. package/dist/theme-toggle/theme-flash.d.ts +16 -0
  73. package/dist/theme-toggle/theme-flash.js +38 -0
  74. package/dist/theme-toggle/theme-toggle.svelte +189 -0
  75. package/dist/theme-toggle/theme-toggle.svelte.d.ts +40 -0
  76. package/dist/tooltip/tooltip-content.svelte +8 -10
  77. package/dist/typography/heading.svelte +13 -89
  78. package/dist/typography/heading.svelte.d.ts +3 -8
  79. package/dist/typography/index.d.ts +8 -7
  80. package/dist/typography/text.svelte +12 -84
  81. package/dist/typography/text.svelte.d.ts +3 -10
  82. package/package.json +7 -2
  83. package/skills/dryui/SKILL.md +18 -5
  84. package/skills/dryui/rules/composition.md +1 -1
  85. package/skills/dryui/rules/theming.md +1 -2
@@ -1,8 +1,8 @@
1
1
  <script lang="ts">
2
- import { fromAction } from 'svelte/attachments';
3
2
  import type { Snippet } from 'svelte';
4
3
  import type { HTMLAttributes } from 'svelte/elements';
5
- import { createAnchoredPopover, type Placement } from '@dryui/primitives';
4
+ import type { Placement } from '@dryui/primitives';
5
+ import PickerPopoverContent from '../internal/picker-popover-content.svelte';
6
6
  import { getDateRangePickerCtx } from './context.svelte.js';
7
7
 
8
8
  interface Props extends HTMLAttributes<HTMLDivElement> {
@@ -21,79 +21,15 @@
21
21
  }: Props = $props();
22
22
 
23
23
  const ctx = getDateRangePickerCtx();
24
-
25
- let el = $state<HTMLDivElement | null>(null);
26
-
27
- function attachContent(node: HTMLDivElement) {
28
- el = node;
29
-
30
- return () => {
31
- if (el === node) {
32
- el = null;
33
- }
34
- };
35
- }
36
-
37
- const popover = createAnchoredPopover({
38
- triggerEl: () => ctx.triggerEl,
39
- contentEl: () => el ?? null,
40
- open: () => ctx.open,
41
- placement: () => placement,
42
- offset: () => offset
43
- });
44
24
  </script>
45
25
 
46
- <div
47
- {@attach attachContent}
48
- {@attach fromAction(popover.applyPosition, () => style)}
49
- popover="auto"
50
- role="dialog"
51
- id={ctx.contentId}
52
- aria-labelledby={ctx.triggerId}
53
- data-state={ctx.open ? 'open' : 'closed'}
54
- data-drp-content
55
- class={className}
56
- ontoggle={(e) => {
57
- const newState = (e as ToggleEvent).newState === 'open';
58
- if (newState && !ctx.open) {
59
- ctx.show();
60
- } else if (!newState && ctx.open) {
61
- ctx.close();
62
- }
63
- }}
26
+ <PickerPopoverContent
27
+ controller={ctx}
28
+ dataAttribute="data-drp-content"
29
+ {placement}
30
+ {offset}
31
+ contentStyle={style}
32
+ contentClass={className}
33
+ {children}
64
34
  {...rest}
65
- >
66
- {@render children()}
67
- </div>
68
-
69
- <style>
70
- [data-drp-content] {
71
- inset: unset;
72
- margin: 0;
73
- padding: var(--dry-space-3);
74
- border: 1px solid var(--dry-color-stroke-weak);
75
- border-radius: var(--dry-radius-lg);
76
- background: var(--dry-color-bg-overlay);
77
- box-shadow: var(--dry-shadow-lg);
78
- color: var(--dry-color-text-strong);
79
- }
80
-
81
- [data-drp-content]:popover-open {
82
- opacity: 1;
83
- transform: translateY(0) scale(1);
84
- }
85
-
86
- @starting-style {
87
- [data-drp-content]:popover-open {
88
- opacity: 0;
89
- transform: translateY(calc(var(--dry-motion-distance-xs) * -1))
90
- scale(var(--dry-motion-scale-enter));
91
- }
92
- }
93
-
94
- [data-drp-content] {
95
- transition:
96
- opacity var(--dry-duration-fast) var(--dry-ease-emphasized),
97
- transform var(--dry-duration-fast) var(--dry-ease-emphasized);
98
- }
99
- </style>
35
+ />
@@ -1,6 +1,6 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import type { HTMLAttributes } from 'svelte/elements';
3
- import { type Placement } from '@dryui/primitives';
3
+ import type { Placement } from '@dryui/primitives';
4
4
  interface Props extends HTMLAttributes<HTMLDivElement> {
5
5
  placement?: Placement;
6
6
  offset?: number;
@@ -1,8 +1,10 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
- import { generateFormId } from '@dryui/primitives';
3
+ import {
4
+ createDateViewController,
5
+ createPickerPopoverController
6
+ } from '../internal/date-family-controller.svelte.js';
4
7
  import { setDateRangePickerCtx } from './context.svelte.js';
5
- import { getWeekStartDay, addMonths, isSameDay } from '@dryui/primitives';
6
8
 
7
9
  interface Props {
8
10
  open?: boolean;
@@ -26,18 +28,10 @@
26
28
  children
27
29
  }: Props = $props();
28
30
 
29
- const weekStartDay = $derived(getWeekStartDay(locale));
30
-
31
- const triggerId = generateFormId('date-range-picker-trigger');
32
- const contentId = generateFormId('date-range-picker-content');
33
-
34
- // View state: which month/year is the calendar showing
35
- let viewMonth = $state(startDate ? startDate.getMonth() : new Date().getMonth());
36
- let viewYear = $state(startDate ? startDate.getFullYear() : new Date().getFullYear());
37
- let triggerEl = $state<HTMLElement | null>(null);
38
-
39
- // The day that has keyboard focus within the calendar grid
40
- let focusedDate = $state<Date>(startDate ?? new Date());
31
+ const view = createDateViewController({
32
+ initialDate: startDate,
33
+ locale: () => locale
34
+ });
41
35
 
42
36
  // Hover date for range preview
43
37
  let hoverDate = $state<Date | null>(null);
@@ -45,6 +39,22 @@
45
39
  // Selection mode: 'start' means next click sets start, 'end' means next click sets end
46
40
  let selecting = $state<'start' | 'end'>('start');
47
41
 
42
+ const popover = createPickerPopoverController({
43
+ triggerIdPrefix: 'date-range-picker-trigger',
44
+ contentIdPrefix: 'date-range-picker-content',
45
+ open: () => open,
46
+ setOpen: (nextOpen) => {
47
+ open = nextOpen;
48
+ },
49
+ disabled: () => disabled,
50
+ onShow: () => {
51
+ selecting = 'start';
52
+ },
53
+ onClose: () => {
54
+ hoverDate = null;
55
+ }
56
+ });
57
+
48
58
  setDateRangePickerCtx({
49
59
  get open() {
50
60
  return open;
@@ -56,13 +66,13 @@
56
66
  return endDate;
57
67
  },
58
68
  get focusedDate() {
59
- return focusedDate;
69
+ return view.focusedDate;
60
70
  },
61
71
  get viewMonth() {
62
- return viewMonth;
72
+ return view.viewMonth;
63
73
  },
64
74
  get viewYear() {
65
- return viewYear;
75
+ return view.viewYear;
66
76
  },
67
77
  get locale() {
68
78
  return locale;
@@ -77,7 +87,7 @@
77
87
  return disabled;
78
88
  },
79
89
  get weekStartDay() {
80
- return weekStartDay;
90
+ return view.weekStartDay;
81
91
  },
82
92
  get hoverDate() {
83
93
  return hoverDate;
@@ -85,41 +95,33 @@
85
95
  get selecting() {
86
96
  return selecting;
87
97
  },
88
- triggerId,
89
- contentId,
98
+ get triggerId() {
99
+ return popover.triggerId;
100
+ },
101
+ get contentId() {
102
+ return popover.contentId;
103
+ },
90
104
  get triggerEl() {
91
- return triggerEl;
105
+ return popover.triggerEl;
92
106
  },
93
107
  set triggerEl(element: HTMLElement | null) {
94
- triggerEl = element;
108
+ popover.setTriggerEl(element);
95
109
  },
96
110
  show() {
97
- if (!disabled) {
98
- selecting = 'start';
99
- open = true;
100
- }
111
+ popover.show();
101
112
  },
102
113
  close() {
103
- open = false;
104
- hoverDate = null;
114
+ popover.close();
105
115
  },
106
116
  toggle() {
107
- if (!disabled) {
108
- if (!open) {
109
- selecting = 'start';
110
- }
111
- open = !open;
112
- if (!open) {
113
- hoverDate = null;
114
- }
115
- }
117
+ popover.toggle();
116
118
  },
117
119
  selectDate(date: Date) {
118
120
  if (selecting === 'start') {
119
121
  startDate = date;
120
122
  endDate = null;
121
123
  selecting = 'end';
122
- focusedDate = date;
124
+ view.focusDate(date);
123
125
  } else {
124
126
  // 'end' mode
125
127
  if (startDate && date.getTime() < startDate.getTime()) {
@@ -130,27 +132,20 @@
130
132
  endDate = date;
131
133
  }
132
134
  selecting = 'start';
133
- hoverDate = null;
134
- open = false;
135
+ popover.close();
135
136
  }
136
137
  },
137
138
  setHoverDate(date: Date | null) {
138
139
  hoverDate = date;
139
140
  },
140
141
  nextMonth() {
141
- const next = addMonths(new Date(viewYear, viewMonth, 1), 1);
142
- viewMonth = next.getMonth();
143
- viewYear = next.getFullYear();
142
+ view.nextMonth();
144
143
  },
145
144
  prevMonth() {
146
- const prev = addMonths(new Date(viewYear, viewMonth, 1), -1);
147
- viewMonth = prev.getMonth();
148
- viewYear = prev.getFullYear();
145
+ view.prevMonth();
149
146
  },
150
147
  setFocusedDate(date: Date) {
151
- focusedDate = date;
152
- viewMonth = date.getMonth();
153
- viewYear = date.getFullYear();
148
+ view.setFocusedDate(date);
154
149
  }
155
150
  });
156
151
  </script>
@@ -9,5 +9,5 @@ export interface DragAndDropGroupContext {
9
9
  setActiveTarget(listId: string | null, index: number | null): void;
10
10
  move(fromListId: string, fromIndex: number, toListId: string, toIndex: number): void;
11
11
  }
12
- export declare function setGroupCtx(ctx: DragAndDropGroupContext): void;
12
+ export declare function setGroupCtx(ctx: DragAndDropGroupContext): DragAndDropGroupContext;
13
13
  export declare function getGroupCtx(): DragAndDropGroupContext | null;
@@ -1,8 +1,8 @@
1
- import { getContext, setContext, hasContext } from 'svelte';
2
- const GROUP_KEY = Symbol('drag-and-drop-group');
1
+ import { createContext } from '@dryui/primitives';
2
+ const [_setGroupCtx, _getGroupCtx] = createContext('drag-and-drop-group');
3
3
  export function setGroupCtx(ctx) {
4
- setContext(GROUP_KEY, ctx);
4
+ return _setGroupCtx(ctx);
5
5
  }
6
6
  export function getGroupCtx() {
7
- return hasContext(GROUP_KEY) ? getContext(GROUP_KEY) : null;
7
+ return _getGroupCtx() ?? null;
8
8
  }
@@ -1,10 +1,4 @@
1
- export interface DropdownMenuContext {
2
- readonly open: boolean;
3
- readonly triggerId: string;
4
- readonly contentId: string;
5
- triggerEl: HTMLElement | null;
6
- show: () => void;
7
- close: () => void;
8
- toggle: () => void;
1
+ import type { MenuRootState } from '../internal/menu-root-state.svelte.js';
2
+ export interface DropdownMenuContext extends MenuRootState {
9
3
  }
10
4
  export declare const setDropdownMenuCtx: (ctx: DropdownMenuContext) => DropdownMenuContext, getDropdownMenuCtx: () => DropdownMenuContext;
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
+ import { fromAction } from 'svelte/attachments';
3
4
  import type { HTMLAttributes } from 'svelte/elements';
4
5
  import { createAnchoredPopover, createMenuNavigation } from '@dryui/primitives';
5
6
  import type { Placement } from '@dryui/primitives';
@@ -22,7 +23,17 @@
22
23
 
23
24
  const ctx = getDropdownMenuCtx();
24
25
 
25
- let el = $state<HTMLDivElement>();
26
+ let el = $state<HTMLDivElement | null>(null);
27
+
28
+ function attachContent(node: HTMLDivElement) {
29
+ el = node;
30
+
31
+ return () => {
32
+ if (el === node) {
33
+ el = null;
34
+ }
35
+ };
36
+ }
26
37
 
27
38
  const popover = createAnchoredPopover({
28
39
  triggerEl: () => ctx.triggerEl,
@@ -39,7 +50,8 @@
39
50
  </script>
40
51
 
41
52
  <div
42
- bind:this={el}
53
+ {@attach attachContent}
54
+ {@attach fromAction(popover.applyPosition, () => style)}
43
55
  popover="auto"
44
56
  role="menu"
45
57
  tabindex="-1"
@@ -48,7 +60,6 @@
48
60
  data-dropdown-menu-content
49
61
  data-state={ctx.open ? 'open' : 'closed'}
50
62
  class={className}
51
- use:popover.applyPosition={style}
52
63
  ontoggle={(e) => {
53
64
  const newState = (e as ToggleEvent).newState === 'open';
54
65
  if (newState && !ctx.open) {
@@ -70,6 +81,7 @@
70
81
  inset: unset;
71
82
  margin: 0;
72
83
 
84
+ --dry-menu-item-padding: var(--dry-space-3) var(--dry-space-4);
73
85
  background: var(--dry-menu-bg, var(--dry-overlay-bg, var(--dry-color-bg-overlay)));
74
86
  color: var(--dry-color-text-strong);
75
87
  border: 1px solid
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
3
  import type { HTMLAttributes } from 'svelte/elements';
4
+ import MenuGroup from '../internal/menu-group.svelte';
4
5
 
5
6
  interface Props extends HTMLAttributes<HTMLDivElement> {
6
7
  children: Snippet;
@@ -9,6 +10,6 @@
9
10
  let { class: className, children, ...rest }: Props = $props();
10
11
  </script>
11
12
 
12
- <div role="group" class={className} {...rest}>
13
+ <MenuGroup {className} {...rest}>
13
14
  {@render children()}
14
- </div>
15
+ </MenuGroup>
@@ -2,6 +2,7 @@
2
2
  import type { Snippet } from 'svelte';
3
3
  import type { HTMLAttributes } from 'svelte/elements';
4
4
  import { getDropdownMenuCtx } from './context.svelte.js';
5
+ import MenuItem from '../internal/menu-item.svelte';
5
6
 
6
7
  interface Props extends HTMLAttributes<HTMLDivElement> {
7
8
  disabled?: boolean;
@@ -11,70 +12,16 @@
11
12
  let { class: className, disabled, children, onclick, onkeydown, ...rest }: Props = $props();
12
13
 
13
14
  const ctx = getDropdownMenuCtx();
14
-
15
- function handleClick(e: MouseEvent & { currentTarget: HTMLDivElement }) {
16
- if (disabled) return;
17
- if (onclick) (onclick as (e: MouseEvent & { currentTarget: HTMLDivElement }) => void)(e);
18
- ctx.close();
19
- }
20
-
21
- function handleKeydown(e: KeyboardEvent & { currentTarget: HTMLDivElement }) {
22
- if (disabled) return;
23
- if (e.key === 'Enter' || e.key === ' ') {
24
- e.preventDefault();
25
- (e.currentTarget as HTMLElement).click();
26
- }
27
- if (onkeydown) (onkeydown as (e: KeyboardEvent & { currentTarget: HTMLDivElement }) => void)(e);
28
- }
29
15
  </script>
30
16
 
31
- <div
32
- role="menuitem"
33
- tabindex={disabled ? undefined : -1}
34
- aria-disabled={disabled || undefined}
17
+ <MenuItem
35
18
  data-dropdown-menu-item
36
- data-disabled={disabled || undefined}
37
- class={className}
38
- onclick={handleClick}
39
- onkeydown={handleKeydown}
19
+ {className}
20
+ close={ctx.close}
21
+ {disabled}
22
+ {onclick}
23
+ {onkeydown}
40
24
  {...rest}
41
25
  >
42
26
  {@render children()}
43
- </div>
44
-
45
- <style>
46
- [data-dropdown-menu-item] {
47
- display: grid;
48
- grid-auto-flow: column;
49
- grid-auto-columns: max-content;
50
- align-items: center;
51
- gap: var(--dry-space-2);
52
- padding: var(--dry-menu-item-padding, var(--dry-space-3) var(--dry-space-4));
53
- border-radius: var(
54
- --dry-menu-item-radius,
55
- min(var(--dry-control-radius, var(--dry-radius-sm)), var(--dry-space-4))
56
- );
57
- font-size: var(--dry-type-small-size, var(--dry-text-sm-size));
58
- cursor: pointer;
59
- user-select: none;
60
- outline: none;
61
- color: var(--dry-color-text-strong);
62
- min-height: var(--dry-space-11);
63
- transition: background var(--dry-duration-fast) var(--dry-ease-default);
64
- }
65
-
66
- [data-dropdown-menu-item]:hover:not([data-disabled]),
67
- [data-dropdown-menu-item]:focus-visible {
68
- background: var(--dry-color-fill);
69
- }
70
-
71
- [data-dropdown-menu-item]:active:not([data-disabled]) {
72
- background: var(--dry-color-fill-hover);
73
- }
74
-
75
- [data-dropdown-menu-item][data-disabled] {
76
- color: var(--dry-color-text-disabled);
77
- cursor: not-allowed;
78
- pointer-events: none;
79
- }
80
- </style>
27
+ </MenuItem>
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
3
  import type { HTMLAttributes } from 'svelte/elements';
4
+ import MenuLabel from '../internal/menu-label.svelte';
4
5
 
5
6
  interface Props extends HTMLAttributes<HTMLDivElement> {
6
7
  children: Snippet;
@@ -9,15 +10,6 @@
9
10
  let { class: className, children, ...rest }: Props = $props();
10
11
  </script>
11
12
 
12
- <div role="presentation" data-dropdown-menu-label class={className} {...rest}>
13
+ <MenuLabel data-dropdown-menu-label {className} {...rest}>
13
14
  {@render children()}
14
- </div>
15
-
16
- <style>
17
- [data-dropdown-menu-label] {
18
- padding: var(--dry-space-1_5) var(--dry-space-2);
19
- font-size: var(--dry-type-tiny-size, var(--dry-text-xs-size));
20
- color: var(--dry-color-text-weak);
21
- font-weight: 500;
22
- }
23
- </style>
15
+ </MenuLabel>
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
- import { generateFormId } from '@dryui/primitives';
4
3
  import { setDropdownMenuCtx } from './context.svelte.js';
4
+ import { createMenuRootState } from '../internal/menu-root-state.svelte.js';
5
5
 
6
6
  interface Props {
7
7
  open?: boolean;
@@ -10,26 +10,15 @@
10
10
 
11
11
  let { open = $bindable(false), children }: Props = $props();
12
12
 
13
- const triggerId = generateFormId('dropdown-trigger');
14
- const contentId = generateFormId('dropdown-content');
15
-
16
- setDropdownMenuCtx({
17
- get open() {
18
- return open;
19
- },
20
- triggerId,
21
- contentId,
22
- triggerEl: null,
23
- show() {
24
- open = true;
25
- },
26
- close() {
27
- open = false;
28
- },
29
- toggle() {
30
- open = !open;
31
- }
32
- });
13
+ setDropdownMenuCtx(
14
+ createMenuRootState({
15
+ idBase: 'dropdown',
16
+ getOpen: () => open,
17
+ setOpen: (value) => {
18
+ open = value;
19
+ }
20
+ })
21
+ );
33
22
  </script>
34
23
 
35
24
  {@render children()}
@@ -1,17 +1,10 @@
1
1
  <script lang="ts">
2
2
  import type { HTMLAttributes } from 'svelte/elements';
3
+ import MenuSeparator from '../internal/menu-separator.svelte';
3
4
 
4
5
  interface Props extends HTMLAttributes<HTMLDivElement> {}
5
6
 
6
7
  let { class: className, ...rest }: Props = $props();
7
8
  </script>
8
9
 
9
- <div role="separator" data-dropdown-menu-separator class={className} {...rest}></div>
10
-
11
- <style>
12
- [data-dropdown-menu-separator] {
13
- height: 1px;
14
- background: var(--dry-color-stroke-weak);
15
- margin: var(--dry-space-1) 0;
16
- }
17
- </style>
10
+ <MenuSeparator data-dropdown-menu-separator {className} {...rest}></MenuSeparator>
@@ -0,0 +1,5 @@
1
+ export interface FlipCardContext {
2
+ readonly flipped: boolean;
3
+ readonly direction: 'horizontal' | 'vertical';
4
+ }
5
+ export declare const setFlipCardCtx: (ctx: FlipCardContext) => FlipCardContext, getFlipCardCtx: () => FlipCardContext;
@@ -0,0 +1,2 @@
1
+ import { createContext } from '@dryui/primitives';
2
+ export const [setFlipCardCtx, getFlipCardCtx] = createContext('flip-card');
@@ -1,14 +1,14 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
3
  import type { HTMLAttributes } from 'svelte/elements';
4
- import { getContext } from 'svelte';
4
+ import { getFlipCardCtx } from './context.svelte.js';
5
5
 
6
6
  interface Props extends HTMLAttributes<HTMLDivElement> {
7
7
  children: Snippet;
8
8
  }
9
9
 
10
10
  let { class: className, children, ...rest }: Props = $props();
11
- const ctx = getContext<{ flipped: boolean }>('flip-card');
11
+ const ctx = getFlipCardCtx();
12
12
  </script>
13
13
 
14
14
  <div
@@ -1,7 +1,9 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
3
  import type { HTMLAttributes } from 'svelte/elements';
4
- import { setContext } from 'svelte';
4
+ import Button from '../button/button.svelte';
5
+ import VisuallyHidden from '../visually-hidden/visually-hidden.svelte';
6
+ import { setFlipCardCtx } from './context.svelte.js';
5
7
 
6
8
  interface Props extends HTMLAttributes<HTMLDivElement> {
7
9
  trigger?: 'hover' | 'click';
@@ -14,6 +16,8 @@
14
16
  trigger = 'hover',
15
17
  direction = 'horizontal',
16
18
  flipped = $bindable(false),
19
+ 'aria-label': ariaLabel,
20
+ 'aria-labelledby': ariaLabelledBy,
17
21
  class: className,
18
22
  children,
19
23
  ...rest
@@ -23,7 +27,9 @@
23
27
  flipped = !flipped;
24
28
  }
25
29
 
26
- setContext('flip-card', {
30
+ const toggleLabel = $derived(flipped ? 'Show front of card' : 'Show back of card');
31
+
32
+ setFlipCardCtx({
27
33
  get flipped() {
28
34
  return flipped;
29
35
  },
@@ -33,29 +39,34 @@
33
39
  });
34
40
  </script>
35
41
 
36
- <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
37
42
  <div
38
43
  data-flip-card
44
+ data-part="root"
39
45
  data-flipped={flipped ? '' : undefined}
40
46
  data-trigger={trigger}
41
47
  data-direction={direction}
42
- role={trigger === 'click' ? 'button' : 'group'}
43
- aria-roledescription="flip card"
44
- tabindex={trigger === 'click' ? 0 : undefined}
48
+ role={trigger === 'hover' ? 'group' : undefined}
49
+ aria-roledescription={trigger === 'hover' ? 'flip card' : undefined}
50
+ aria-label={trigger === 'hover' ? ariaLabel : undefined}
51
+ aria-labelledby={trigger === 'hover' ? ariaLabelledBy : undefined}
45
52
  onmouseenter={trigger === 'hover' ? () => (flipped = true) : undefined}
46
53
  onmouseleave={trigger === 'hover' ? () => (flipped = false) : undefined}
47
- onclick={trigger === 'click' ? toggle : undefined}
48
- onkeydown={trigger === 'click'
49
- ? (e) => {
50
- if (e.key === 'Enter' || e.key === ' ') {
51
- e.preventDefault();
52
- toggle();
53
- }
54
- }
55
- : undefined}
56
54
  class={className}
57
55
  {...rest}
58
56
  >
57
+ {#if trigger === 'click'}
58
+ <span data-flip-card-toggle-shell>
59
+ <Button
60
+ variant="bare"
61
+ aria-pressed={flipped}
62
+ aria-label={ariaLabel ?? toggleLabel}
63
+ aria-labelledby={ariaLabelledBy}
64
+ onclick={toggle}
65
+ >
66
+ <VisuallyHidden>{toggleLabel}</VisuallyHidden>
67
+ </Button>
68
+ </span>
69
+ {/if}
59
70
  {@render children()}
60
71
  </div>
61
72
 
@@ -72,6 +83,22 @@
72
83
  transform-style: preserve-3d;
73
84
  }
74
85
 
86
+ [data-flip-card-toggle-shell] {
87
+ position: absolute;
88
+ inset: 0;
89
+ z-index: 3;
90
+ display: grid;
91
+ border-radius: inherit;
92
+ --dry-btn-bg: transparent;
93
+ --dry-btn-border: transparent;
94
+ --dry-btn-color: inherit;
95
+ --dry-btn-padding-x: 0;
96
+ --dry-btn-padding-y: 0;
97
+ --dry-btn-min-height: 0;
98
+ --dry-btn-radius: inherit;
99
+ box-shadow: none;
100
+ }
101
+
75
102
  [data-flip-card][data-direction='horizontal'][data-flipped] {
76
103
  --dry-flip-card-front-transform: rotateY(180deg);
77
104
  --dry-flip-card-back-transform: rotateY(360deg);