@dryui/ui 1.3.1 → 1.4.1

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 (91) hide show
  1. package/dist/accordion/accordion-content.svelte +1 -1
  2. package/dist/alert/alert.svelte +1 -1
  3. package/dist/app-frame/app-frame.svelte +131 -0
  4. package/dist/app-frame/app-frame.svelte.d.ts +10 -0
  5. package/dist/app-frame/index.d.ts +8 -0
  6. package/dist/app-frame/index.js +1 -0
  7. package/dist/aurora/aurora.svelte +22 -59
  8. package/dist/beam/beam.svelte +28 -9
  9. package/dist/carousel/carousel-button-dots.svelte +25 -8
  10. package/dist/carousel/carousel-button-thumbnails.svelte +25 -8
  11. package/dist/carousel/carousel-root.svelte +115 -4
  12. package/dist/carousel/carousel-slide.svelte +5 -1
  13. package/dist/carousel/carousel-viewport.svelte +2 -0
  14. package/dist/carousel/context.svelte.d.ts +5 -0
  15. package/dist/chart/chart-bars.svelte +25 -16
  16. package/dist/chart/chart-donut.svelte +25 -16
  17. package/dist/chart/chart-root.svelte +134 -30
  18. package/dist/chart/chart-root.svelte.d.ts +1 -0
  19. package/dist/chart/context.svelte.d.ts +3 -1
  20. package/dist/chart/context.svelte.js +1 -0
  21. package/dist/chart/index.d.ts +1 -0
  22. package/dist/chromatic-shift/chromatic-shift.svelte +36 -9
  23. package/dist/collapsible/collapsible-content.svelte +2 -1
  24. package/dist/combobox/combobox-content.svelte +26 -44
  25. package/dist/combobox/combobox-content.svelte.d.ts +1 -1
  26. package/dist/combobox/combobox-input-root.svelte +7 -1
  27. package/dist/combobox/combobox-input.svelte +21 -8
  28. package/dist/country-select/country-select-button-input.svelte +124 -260
  29. package/dist/date-picker/datepicker-content.svelte +18 -26
  30. package/dist/date-picker/datepicker-content.svelte.d.ts +2 -1
  31. package/dist/date-picker/datepicker-input-root.svelte +7 -1
  32. package/dist/date-range-picker/date-range-picker-content.svelte +18 -14
  33. package/dist/date-range-picker/date-range-picker-content.svelte.d.ts +2 -1
  34. package/dist/date-range-picker/date-range-picker-root.svelte +7 -1
  35. package/dist/displacement/displacement.svelte +16 -22
  36. package/dist/drag-and-drop/context.svelte.d.ts +2 -0
  37. package/dist/drag-and-drop/drag-and-drop-handle.svelte +34 -5
  38. package/dist/drag-and-drop/drag-and-drop-item.svelte +23 -14
  39. package/dist/drag-and-drop/drag-and-drop-root.svelte +60 -16
  40. package/dist/god-rays/god-rays.svelte +11 -0
  41. package/dist/gradient-mesh/gradient-mesh.svelte +27 -5
  42. package/dist/hover-card/context.svelte.d.ts +1 -10
  43. package/dist/hover-card/context.svelte.js +1 -2
  44. package/dist/hover-card/hover-card-content.svelte +41 -3
  45. package/dist/hover-card/hover-card-root.svelte +7 -55
  46. package/dist/hover-card/hover-card-trigger.svelte +79 -40
  47. package/dist/hover-card/hover-card-trigger.svelte.d.ts +1 -1
  48. package/dist/index.d.ts +2 -0
  49. package/dist/index.js +1 -0
  50. package/dist/internal/motion.d.ts +1 -1
  51. package/dist/internal/motion.js +1 -1
  52. package/dist/marquee/marquee.svelte +42 -5
  53. package/dist/mega-menu/context.svelte.d.ts +2 -1
  54. package/dist/mega-menu/mega-menu-button-trigger.svelte +2 -14
  55. package/dist/mega-menu/mega-menu-item.svelte +3 -1
  56. package/dist/mega-menu/mega-menu-panel.svelte +35 -3
  57. package/dist/mega-menu/mega-menu-root.svelte +28 -13
  58. package/dist/menubar/context.svelte.d.ts +2 -2
  59. package/dist/menubar/menubar-button-trigger.svelte +5 -3
  60. package/dist/menubar/menubar-content.svelte +20 -12
  61. package/dist/menubar/menubar-root.svelte +4 -4
  62. package/dist/multi-select-combobox/multi-select-combobox-content.svelte +18 -55
  63. package/dist/multi-select-combobox/multi-select-combobox-content.svelte.d.ts +1 -1
  64. package/dist/noise/noise.svelte +38 -6
  65. package/dist/notification-center/context.svelte.d.ts +0 -1
  66. package/dist/notification-center/notification-center-panel.svelte +54 -35
  67. package/dist/notification-center/notification-center-root.svelte +0 -1
  68. package/dist/notification-center/notification-center-trigger-button.svelte +1 -8
  69. package/dist/option-picker/option-picker-description.svelte +2 -2
  70. package/dist/option-picker/option-picker-item.svelte +10 -3
  71. package/dist/option-picker/option-picker-label.svelte +2 -2
  72. package/dist/option-picker/option-picker-preview.svelte +18 -13
  73. package/dist/phone-input/phone-input-select.svelte +2 -152
  74. package/dist/phone-input/phone-input-select.svelte.d.ts +1 -7
  75. package/dist/rich-text-editor/rich-text-editor-toolbar-button-input.svelte +84 -29
  76. package/dist/scroll-area/scroll-area.svelte +16 -4
  77. package/dist/select/select-content.svelte +21 -31
  78. package/dist/select/select-content.svelte.d.ts +1 -1
  79. package/dist/select/select-root-input.svelte +7 -1
  80. package/dist/shimmer/shimmer.svelte +22 -12
  81. package/dist/transfer/transfer-item.svelte +0 -3
  82. package/dist/transfer/transfer-list-input.svelte +1 -6
  83. package/dist/tree/context.svelte.d.ts +7 -1
  84. package/dist/tree/tree-item-children.svelte +12 -10
  85. package/dist/tree/tree-item-label.svelte +6 -17
  86. package/dist/tree/tree-item-label.svelte.d.ts +2 -2
  87. package/dist/tree/tree-item.svelte +28 -1
  88. package/dist/tree/tree-root.svelte +135 -59
  89. package/package.json +8 -2
  90. package/skills/dryui/SKILL.md +1 -0
  91. package/dist/hover-card/hover-card-root.svelte.d.ts +0 -9
@@ -10,11 +10,14 @@
10
10
  const ctx = getRichTextEditorCtx();
11
11
 
12
12
  let toolbarEl: HTMLDivElement;
13
+ type LinkRequestOrigin = 'toolbar' | 'content';
13
14
 
14
15
  let showLinkInput = $state(false);
15
16
  let linkUrl = $state('');
16
17
  let linkInputEl = $state<HTMLInputElement>();
17
18
  let savedSelection = $state<Range | null>(null);
19
+ let linkReturnFocusEl = $state<HTMLElement | null>(null);
20
+ let linkRequestOrigin = $state<LinkRequestOrigin>('toolbar');
18
21
 
19
22
  const FOCUSABLE_SELECTOR = 'button:not([disabled]), [role="button"]:not([disabled])';
20
23
 
@@ -33,6 +36,28 @@
33
36
  }
34
37
  });
35
38
 
39
+ $effect(() => {
40
+ if (!toolbarEl) return;
41
+
42
+ function handleLinkRequest(event: Event) {
43
+ if (ctx.readonly || !(event.target instanceof Node)) return;
44
+
45
+ const editorRoot = toolbarEl.closest<HTMLElement>('[data-rte-root]');
46
+ if (!editorRoot?.contains(event.target)) return;
47
+
48
+ ctx.updateState();
49
+ openLinkInput(ctx.currentLink, {
50
+ origin: 'content',
51
+ returnFocusTo: ctx.contentEl
52
+ });
53
+ }
54
+
55
+ document.addEventListener('rte-link-request', handleLinkRequest);
56
+ return () => {
57
+ document.removeEventListener('rte-link-request', handleLinkRequest);
58
+ };
59
+ });
60
+
36
61
  function handleKeydown(event: KeyboardEvent) {
37
62
  if (
38
63
  event.key !== 'ArrowLeft' &&
@@ -66,11 +91,29 @@
66
91
  items[nextIndex]!.focus();
67
92
  }
68
93
 
69
- function openLinkInput(currentLink: string | null) {
94
+ function focusToolbarItem(item: HTMLElement | null) {
95
+ if (!toolbarEl || !item) return;
96
+
97
+ for (const focusableItem of queryFocusable(toolbarEl)) {
98
+ focusableItem.setAttribute('tabindex', focusableItem === item ? '0' : '-1');
99
+ }
100
+
101
+ item.focus();
102
+ }
103
+
104
+ function openLinkInput(
105
+ currentLink: string | null,
106
+ options: {
107
+ origin?: LinkRequestOrigin;
108
+ returnFocusTo?: HTMLElement | null;
109
+ } = {}
110
+ ) {
70
111
  const sel = window.getSelection();
71
112
  if (sel && sel.rangeCount > 0) {
72
113
  savedSelection = sel.getRangeAt(0).cloneRange();
73
114
  }
115
+ linkRequestOrigin = options.origin ?? 'toolbar';
116
+ linkReturnFocusEl = options.returnFocusTo ?? null;
74
117
  linkUrl = currentLink ?? 'https://';
75
118
  showLinkInput = true;
76
119
  requestAnimationFrame(() => {
@@ -79,13 +122,7 @@
79
122
  });
80
123
  }
81
124
 
82
- function closeLinkInput() {
83
- showLinkInput = false;
84
- linkUrl = '';
85
- savedSelection = null;
86
- }
87
-
88
- function restoreSelection() {
125
+ function restoreSavedSelection() {
89
126
  if (savedSelection) {
90
127
  const sel = window.getSelection();
91
128
  if (sel) {
@@ -94,6 +131,35 @@
94
131
  }
95
132
  }
96
133
  }
134
+
135
+ function cancelLinkInput() {
136
+ if (linkRequestOrigin === 'content') {
137
+ restoreSavedSelection();
138
+ ctx.contentEl?.focus();
139
+ } else {
140
+ focusToolbarItem(linkReturnFocusEl);
141
+ }
142
+
143
+ closeLinkInput();
144
+ }
145
+
146
+ function applyLink() {
147
+ if (linkUrl) {
148
+ restoreSavedSelection();
149
+ ctx.insertLink(linkUrl);
150
+ }
151
+
152
+ closeLinkInput();
153
+ ctx.contentEl?.focus();
154
+ }
155
+
156
+ function closeLinkInput() {
157
+ showLinkInput = false;
158
+ linkUrl = '';
159
+ savedSelection = null;
160
+ linkReturnFocusEl = null;
161
+ linkRequestOrigin = 'toolbar';
162
+ }
97
163
  </script>
98
164
 
99
165
  <div
@@ -321,13 +387,11 @@
321
387
  title="Link (Ctrl+K)"
322
388
  tabindex={-1}
323
389
  onmousedown={(e) => e.preventDefault()}
324
- onclick={() => {
325
- if (ctx.currentLink) {
326
- openLinkInput(ctx.currentLink);
327
- } else {
328
- openLinkInput(null);
329
- }
330
- }}
390
+ onclick={(event) =>
391
+ openLinkInput(ctx.currentLink, {
392
+ origin: 'toolbar',
393
+ returnFocusTo: event.currentTarget as HTMLElement
394
+ })}
331
395
  >
332
396
  <svg
333
397
  width="16"
@@ -382,19 +446,16 @@
382
446
  <input
383
447
  bind:value={linkUrl}
384
448
  bind:this={linkInputEl}
449
+ aria-label="Link URL"
385
450
  data-part="linkInput"
386
451
  type="url"
387
452
  placeholder="https://example.com"
388
453
  onkeydown={(e) => {
389
454
  if (e.key === 'Enter') {
390
455
  e.preventDefault();
391
- if (linkUrl) {
392
- restoreSelection();
393
- ctx.insertLink(linkUrl);
394
- }
395
- closeLinkInput();
456
+ applyLink();
396
457
  } else if (e.key === 'Escape') {
397
- closeLinkInput();
458
+ cancelLinkInput();
398
459
  }
399
460
  }}
400
461
  />
@@ -403,20 +464,14 @@
403
464
  size="sm"
404
465
  type="button"
405
466
  data-part="linkApply"
406
- onclick={() => {
407
- if (linkUrl) {
408
- restoreSelection();
409
- ctx.insertLink(linkUrl);
410
- }
411
- closeLinkInput();
412
- }}>Apply</Button
467
+ onclick={() => applyLink()}>Apply</Button
413
468
  >
414
469
  <Button
415
470
  variant="outline"
416
471
  size="sm"
417
472
  type="button"
418
473
  data-part="linkCancel"
419
- onclick={() => closeLinkInput()}>Cancel</Button
474
+ onclick={() => cancelLinkInput()}>Cancel</Button
420
475
  >
421
476
  </div>
422
477
  {/if}
@@ -7,16 +7,28 @@
7
7
  children: Snippet;
8
8
  }
9
9
 
10
- let { orientation = 'vertical', class: className, children, ...rest }: Props = $props();
10
+ let {
11
+ orientation = 'vertical',
12
+ role: roleProp,
13
+ tabindex = 0,
14
+ 'aria-label': ariaLabel,
15
+ 'aria-labelledby': ariaLabelledBy,
16
+ class: className,
17
+ children,
18
+ ...rest
19
+ }: Props = $props();
20
+
21
+ const role = $derived(roleProp ?? (ariaLabel || ariaLabelledBy ? 'region' : undefined));
11
22
  </script>
12
23
 
13
24
  <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
14
25
  <div
15
- role="region"
16
- aria-label="Scrollable content"
26
+ {role}
27
+ aria-label={ariaLabel}
28
+ aria-labelledby={ariaLabelledBy}
17
29
  data-scroll-area
18
30
  data-orientation={orientation}
19
- tabindex={0}
31
+ {tabindex}
20
32
  class={className}
21
33
  {...rest}
22
34
  >
@@ -1,8 +1,8 @@
1
1
  <script lang="ts">
2
+ import { fromAction } from 'svelte/attachments';
2
3
  import type { Snippet } from 'svelte';
3
4
  import type { HTMLAttributes } from 'svelte/elements';
4
- import { createAnchorPosition } from '@dryui/primitives';
5
- import type { Placement } from '@dryui/primitives';
5
+ import { createAnchoredPopover, type Placement } from '@dryui/primitives';
6
6
  import { getSelectCtx } from './context.svelte.js';
7
7
 
8
8
  const OPTION_SELECTOR = '[role="option"]:not([data-disabled])';
@@ -24,30 +24,17 @@
24
24
 
25
25
  const ctx = getSelectCtx();
26
26
 
27
- let el = $state<HTMLDivElement>();
28
-
29
- const anchor = createAnchorPosition(
30
- () => ctx.triggerEl,
31
- () => el ?? null,
32
- {
33
- get placement() {
34
- return placement;
35
- },
36
- get offset() {
37
- return offset;
38
- }
39
- }
40
- );
27
+ let el = $state<HTMLDivElement | null>(null);
41
28
 
42
- $effect(() => {
43
- if (!el) return;
29
+ function attachContent(node: HTMLDivElement) {
30
+ el = node;
44
31
 
45
- el.style.cssText = typeof style === 'string' ? style : '';
46
- const positionStyles = anchor.styles;
47
- for (const [key, value] of Object.entries(positionStyles)) {
48
- el.style.setProperty(key, value);
49
- }
50
- });
32
+ return () => {
33
+ if (el === node) {
34
+ el = null;
35
+ }
36
+ };
37
+ }
51
38
 
52
39
  function getOptionItems(container: HTMLElement): HTMLElement[] {
53
40
  return Array.from(container.querySelectorAll<HTMLElement>(OPTION_SELECTOR));
@@ -59,12 +46,14 @@
59
46
  items[clamped]?.focus();
60
47
  }
61
48
 
62
- $effect(() => {
63
- if (ctx.open && el && !el.matches(':popover-open')) {
64
- el.showPopover();
49
+ const popover = createAnchoredPopover({
50
+ triggerEl: () => ctx.triggerEl,
51
+ contentEl: () => el ?? null,
52
+ open: () => ctx.open,
53
+ placement: () => placement,
54
+ offset: () => offset,
55
+ onAfterShow: () => {
65
56
  focusFirstSelectItem();
66
- } else if (!ctx.open && el?.matches(':popover-open')) {
67
- el.hidePopover();
68
57
  }
69
58
  });
70
59
 
@@ -127,7 +116,8 @@
127
116
  </script>
128
117
 
129
118
  <div
130
- bind:this={el}
119
+ {@attach attachContent}
120
+ {@attach fromAction(popover.applyPosition, () => style)}
131
121
  popover="auto"
132
122
  role="listbox"
133
123
  id={ctx.contentId}
@@ -156,7 +146,7 @@
156
146
  margin: 0;
157
147
 
158
148
  display: grid;
159
- grid-template-columns: minmax(12rem, max-content);
149
+ grid-template-columns: minmax(max(12rem, anchor-size(inline)), max-content);
160
150
  background: var(--dry-overlay-bg, var(--dry-color-bg-overlay));
161
151
  border: 1px solid var(--dry-overlay-border, var(--dry-color-stroke-weak));
162
152
  border-radius: var(--dry-overlay-radius, var(--dry-radius-md));
@@ -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;
@@ -39,6 +39,7 @@
39
39
  const contentId = generateFormId('select-content');
40
40
 
41
41
  let displayText = $state('');
42
+ let triggerEl = $state<HTMLElement | null>(null);
42
43
 
43
44
  setSelectCtx({
44
45
  get open() {
@@ -55,7 +56,12 @@
55
56
  },
56
57
  triggerId,
57
58
  contentId,
58
- triggerEl: null,
59
+ get triggerEl() {
60
+ return triggerEl;
61
+ },
62
+ set triggerEl(element: HTMLElement | null) {
63
+ triggerEl = element;
64
+ },
59
65
  show() {
60
66
  if (!disabled) open = true;
61
67
  },
@@ -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 { observeInViewport, observePageVisibility } from '@dryui/primitives/internal/motion';
4
5
 
5
6
  interface Props extends Omit<HTMLAttributes<HTMLSpanElement>, 'children'> {
6
7
  color?: string;
@@ -17,27 +18,36 @@
17
18
  }: Props = $props();
18
19
 
19
20
  function setup(node: HTMLSpanElement) {
21
+ let onScreen = true;
22
+ let tabVisible = true;
23
+
24
+ const apply = () => {
25
+ if (onScreen && tabVisible) node.dataset.active = '';
26
+ else delete node.dataset.active;
27
+ };
28
+
20
29
  $effect(() => {
21
30
  node.style.setProperty('--dry-shimmer-color', color);
22
31
  node.style.setProperty('--dry-shimmer-duration', `${duration}s`);
23
32
  });
24
33
 
25
34
  $effect(() => {
26
- if (typeof IntersectionObserver === 'undefined') {
27
- node.dataset.active = '';
28
- return;
29
- }
30
- const io = new IntersectionObserver(
31
- (entries) => {
32
- for (const entry of entries) {
33
- if (entry.isIntersecting) node.dataset.active = '';
34
- else delete node.dataset.active;
35
- }
35
+ const unsubscribeViewport = observeInViewport(
36
+ node,
37
+ (inView) => {
38
+ onScreen = inView;
39
+ apply();
36
40
  },
37
41
  { rootMargin: '100px' }
38
42
  );
39
- io.observe(node);
40
- return () => io.disconnect();
43
+ const unsubscribeVisibility = observePageVisibility((visible) => {
44
+ tabVisible = visible;
45
+ apply();
46
+ });
47
+ return () => {
48
+ unsubscribeViewport();
49
+ unsubscribeVisibility();
50
+ };
41
51
  });
42
52
  }
43
53
  </script>
@@ -25,12 +25,9 @@
25
25
  </script>
26
26
 
27
27
  <label
28
- role="option"
29
28
  data-transfer-item
30
29
  data-disabled={disabled ? '' : undefined}
31
30
  data-selected={isSelected ? '' : undefined}
32
- aria-selected={isSelected}
33
- aria-disabled={disabled}
34
31
  {...rest}
35
32
  >
36
33
  <Checkbox checked={isSelected} {disabled} onchange={toggle} size="sm" />
@@ -58,9 +58,8 @@
58
58
  {/if}
59
59
 
60
60
  <div
61
- role="listbox"
61
+ role="group"
62
62
  aria-label={title ?? (type === 'source' ? 'Available items' : 'Selected items')}
63
- aria-multiselectable="true"
64
63
  data-transfer-list
65
64
  data-type={type}
66
65
  {...rest}
@@ -88,14 +87,10 @@
88
87
  {@render content({ items: filterItems(items) })}
89
88
  {:else}
90
89
  {#each filterItems(items) as item (item.key)}
91
- <!-- svelte-ignore a11y_no_noninteractive_element_to_interactive_role -->
92
90
  <label
93
- role="option"
94
91
  data-transfer-item
95
92
  data-disabled={item.disabled ? '' : undefined}
96
93
  data-selected={selectedSet.has(item.key) ? '' : undefined}
97
- aria-selected={selectedSet.has(item.key)}
98
- aria-disabled={item.disabled ?? false}
99
94
  >
100
95
  <Checkbox
101
96
  checked={selectedSet.has(item.key)}
@@ -1,12 +1,18 @@
1
1
  interface TreeContext {
2
- readonly expandedItems: Set<string>;
2
+ readonly expandedItems: ReadonlySet<string>;
3
3
  readonly selectedItem: string | null;
4
+ readonly focusedItem: string | null;
4
5
  toggleItem: (id: string) => void;
5
6
  expandItem: (id: string) => void;
6
7
  collapseItem: (id: string) => void;
7
8
  selectItem: (id: string) => void;
9
+ setFocusedItem: (id: string) => void;
10
+ registerBranch: (id: string) => void;
11
+ unregisterBranch: (id: string) => void;
8
12
  isExpanded: (id: string) => boolean;
9
13
  isSelected: (id: string) => boolean;
14
+ isFocused: (id: string) => boolean;
15
+ hasChildren: (id: string) => boolean;
10
16
  }
11
17
  export declare const setTreeCtx: (ctx: TreeContext) => TreeContext, getTreeCtx: () => TreeContext;
12
18
  interface TreeItemContext {
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { onDestroy } from 'svelte';
2
3
  import type { Snippet } from 'svelte';
3
4
  import type { HTMLAttributes } from 'svelte/elements';
4
5
  import { getTreeCtx, getTreeItemCtx } from './context.svelte.js';
@@ -7,18 +8,16 @@
7
8
  children: Snippet;
8
9
  }
9
10
 
10
- let { class: className, children, style, ...rest }: Props = $props();
11
+ let { class: className, children, ...rest }: Props = $props();
11
12
 
12
13
  const ctx = getTreeCtx();
13
14
  const itemCtx = getTreeItemCtx();
14
15
  const open = $derived(ctx.isExpanded(itemCtx.itemId));
15
-
16
- function applyStyles(node: HTMLElement) {
17
- $effect(() => {
18
- node.style.cssText = style || '';
19
- node.style.setProperty('--_rows', open ? '1fr' : '0fr');
20
- });
21
- }
16
+ const branchItemId = itemCtx.itemId;
17
+ ctx.registerBranch(branchItemId);
18
+ onDestroy(() => {
19
+ ctx.unregisterBranch(branchItemId);
20
+ });
22
21
  </script>
23
22
 
24
23
  <div
@@ -28,7 +27,6 @@
28
27
  data-state={open ? 'open' : 'closed'}
29
28
  class={className}
30
29
  {...rest}
31
- {@attach applyStyles}
32
30
  >
33
31
  <div class="tree-item-inner">
34
32
  {@render children()}
@@ -42,8 +40,12 @@
42
40
 
43
41
  [data-part='children'] {
44
42
  display: grid;
45
- grid-template-rows: var(--_rows, 0fr);
43
+ grid-template-rows: 0fr;
46
44
  padding-left: var(--dry-tree-indent, var(--dry-space-4));
47
45
  transition: grid-template-rows var(--dry-duration-normal) var(--dry-ease-default);
48
46
  }
47
+
48
+ [data-part='children'][data-state='open'] {
49
+ grid-template-rows: 1fr;
50
+ }
49
51
  </style>
@@ -1,9 +1,9 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
- import type { HTMLButtonAttributes } from 'svelte/elements';
3
+ import type { HTMLAttributes } from 'svelte/elements';
4
4
  import { getTreeCtx, getTreeItemCtx } from './context.svelte.js';
5
5
 
6
- interface Props extends HTMLButtonAttributes {
6
+ interface Props extends HTMLAttributes<HTMLDivElement> {
7
7
  children: Snippet;
8
8
  }
9
9
 
@@ -13,24 +13,17 @@
13
13
  const itemCtx = getTreeItemCtx();
14
14
  </script>
15
15
 
16
- <button
17
- type="button"
16
+ <div
18
17
  data-part="label"
19
- data-tree-label
20
18
  data-selected={ctx.isSelected(itemCtx.itemId) || undefined}
21
19
  class={className}
22
- onclick={() => {
23
- ctx.selectItem(itemCtx.itemId);
24
- ctx.toggleItem(itemCtx.itemId);
25
- }}
26
20
  {...rest}
27
21
  >
28
22
  {@render children()}
29
- </button>
23
+ </div>
30
24
 
31
25
  <style>
32
26
  [data-part='label'] {
33
- appearance: none;
34
27
  display: grid;
35
28
  grid-auto-flow: column;
36
29
  grid-auto-columns: max-content;
@@ -44,9 +37,10 @@
44
37
  font-size: var(--dry-type-small-size, var(--dry-text-sm-size));
45
38
  font-family: var(--dry-font-sans);
46
39
  color: var(--dry-color-text-strong);
40
+ outline: var(--dry-tree-item-focus-ring, none);
41
+ outline-offset: var(--dry-tree-item-focus-offset, 0px);
47
42
  cursor: pointer;
48
43
  user-select: none;
49
- outline: none;
50
44
  transition: background var(--dry-duration-fast) var(--dry-ease-default);
51
45
  }
52
46
 
@@ -54,11 +48,6 @@
54
48
  background: var(--dry-tree-item-hover-bg, var(--dry-color-fill));
55
49
  }
56
50
 
57
- [data-part='label']:focus-visible {
58
- outline: var(--dry-focus-ring);
59
- outline-offset: 2px;
60
- }
61
-
62
51
  [data-part='label'][data-selected] {
63
52
  background: var(--dry-tree-item-selected-bg, var(--dry-color-fill-brand-weak));
64
53
  color: var(--dry-tree-item-selected-color, var(--dry-color-text-brand));
@@ -1,6 +1,6 @@
1
1
  import type { Snippet } from 'svelte';
2
- import type { HTMLButtonAttributes } from 'svelte/elements';
3
- interface Props extends HTMLButtonAttributes {
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ interface Props extends HTMLAttributes<HTMLDivElement> {
4
4
  children: Snippet;
5
5
  }
6
6
  declare const TreeItemLabel: import("svelte").Component<Props, {}, "">;
@@ -12,6 +12,8 @@
12
12
 
13
13
  const ctx = getTreeCtx();
14
14
  const expanded = $derived(ctx.isExpanded(itemId));
15
+ const focused = $derived(ctx.isFocused(itemId));
16
+ const hasChildren = $derived(ctx.hasChildren(itemId));
15
17
  const selected = $derived(ctx.isSelected(itemId));
16
18
 
17
19
  setTreeItemCtx({
@@ -23,14 +25,39 @@
23
25
 
24
26
  <div
25
27
  role="treeitem"
26
- aria-expanded={expanded}
28
+ tabindex={focused ? 0 : -1}
29
+ aria-expanded={hasChildren ? expanded : undefined}
27
30
  aria-selected={selected}
31
+ data-branch={hasChildren || undefined}
28
32
  data-part="item"
29
33
  data-expanded={expanded || undefined}
34
+ data-focused={focused || undefined}
30
35
  data-selected={selected || undefined}
31
36
  data-item-id={itemId}
32
37
  class={className}
38
+ onclick={(e) => {
39
+ (e.currentTarget as HTMLElement).focus();
40
+ ctx.selectItem(itemId);
41
+ }}
42
+ ondblclick={() => {
43
+ if (hasChildren) {
44
+ ctx.toggleItem(itemId);
45
+ }
46
+ }}
33
47
  {...rest}
34
48
  >
35
49
  {@render children()}
36
50
  </div>
51
+
52
+ <style>
53
+ [data-part='item'] {
54
+ outline: none;
55
+ --dry-tree-item-focus-ring: none;
56
+ --dry-tree-item-focus-offset: 0px;
57
+ }
58
+
59
+ [data-part='item']:focus-visible {
60
+ --dry-tree-item-focus-ring: var(--dry-focus-ring);
61
+ --dry-tree-item-focus-offset: 2px;
62
+ }
63
+ </style>