@dryui/ui 0.1.3 → 0.1.4

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 (54) hide show
  1. package/dist/accordion/accordion-trigger.svelte +5 -6
  2. package/dist/alert-dialog/alert-dialog-action.svelte +42 -6
  3. package/dist/alert-dialog/alert-dialog-cancel.svelte +44 -5
  4. package/dist/alert-dialog/alert-dialog-footer.svelte +3 -2
  5. package/dist/aurora/aurora.svelte.d.ts +6 -0
  6. package/dist/beam/beam.svelte +17 -10
  7. package/dist/chromatic-aberration/chromatic-aberration.svelte +60 -18
  8. package/dist/collapsible/collapsible-trigger.svelte +4 -7
  9. package/dist/color-picker/color-picker-eyedropper.svelte +4 -11
  10. package/dist/data-grid/data-grid-pagination.svelte +20 -2
  11. package/dist/data-grid/data-grid-root.svelte +1 -0
  12. package/dist/date-field/date-field-root.svelte +66 -20
  13. package/dist/date-field/date-field-segment.svelte +11 -9
  14. package/dist/date-field/date-field-separator.svelte +9 -1
  15. package/dist/date-picker/datepicker-calendar.svelte +168 -13
  16. package/dist/date-picker/datepicker-trigger.svelte +3 -8
  17. package/dist/date-range-picker/date-range-picker-calendar.svelte +177 -13
  18. package/dist/date-range-picker/date-range-picker-trigger.svelte +18 -12
  19. package/dist/dialog/dialog-content.svelte +1 -0
  20. package/dist/field/field-root.svelte +0 -1
  21. package/dist/file-select/file-select-clear.svelte +2 -8
  22. package/dist/file-upload/file-upload-item-delete.svelte +4 -7
  23. package/dist/flip-card/flip-card-back.svelte +2 -2
  24. package/dist/flip-card/flip-card-front.svelte +2 -2
  25. package/dist/flip-card/flip-card-root.svelte +2 -0
  26. package/dist/float-button/float-button-root.svelte +2 -1
  27. package/dist/image-comparison/image-comparison.svelte +16 -24
  28. package/dist/input-group/input-group-action.svelte +5 -0
  29. package/dist/input-group/input-group-input.svelte +7 -2
  30. package/dist/input-group/input-group-prefix.svelte +5 -0
  31. package/dist/input-group/input-group-root.svelte +10 -2
  32. package/dist/input-group/input-group-select.svelte +5 -0
  33. package/dist/input-group/input-group-separator.svelte +10 -0
  34. package/dist/input-group/input-group-suffix.svelte +5 -0
  35. package/dist/option-swatch-group/option-swatch-group-item.svelte +46 -0
  36. package/dist/option-swatch-group/option-swatch-group-label.svelte +10 -0
  37. package/dist/option-swatch-group/option-swatch-group-meta.svelte +10 -0
  38. package/dist/option-swatch-group/option-swatch-group-root.svelte +0 -79
  39. package/dist/option-swatch-group/option-swatch-group-swatch.svelte +25 -6
  40. package/dist/pin-input/pin-input-cell.svelte +4 -1
  41. package/dist/range-calendar/range-calendar-grid.svelte +219 -181
  42. package/dist/range-calendar/range-calendar-root.svelte +24 -10
  43. package/dist/select/select-trigger.svelte +5 -8
  44. package/dist/system-map/system-map.svelte +120 -674
  45. package/dist/tags-input/tags-input-input.svelte +3 -0
  46. package/dist/tags-input/tags-input-root.svelte +4 -13
  47. package/dist/tags-input/tags-input-tag.svelte +3 -0
  48. package/dist/themes/dark.css +6 -0
  49. package/dist/themes/default.css +3 -0
  50. package/dist/toast/toast-action.svelte +1 -0
  51. package/dist/toast/toast-close.svelte +4 -0
  52. package/dist/toast/toast-provider.svelte +5 -26
  53. package/dist/toast/toast-root.svelte +5 -10
  54. package/package.json +3 -3
@@ -26,6 +26,7 @@
26
26
  {...rest}
27
27
  >
28
28
  {@render children()}
29
+ <svg data-indicator xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg>
29
30
  </button>
30
31
 
31
32
  <style>
@@ -33,6 +34,7 @@
33
34
  display: grid;
34
35
  grid-template-columns: 1fr 1rem;
35
36
  align-items: center;
37
+ gap: var(--dry-space-2);
36
38
  padding: var(--dry-space-4);
37
39
  background: none;
38
40
  border: none;
@@ -65,17 +67,14 @@
65
67
  cursor: not-allowed;
66
68
  }
67
69
 
68
- &[data-state='open']::after {
70
+ &[data-state='open'] [data-indicator] {
69
71
  transform: rotate(180deg);
70
72
  }
71
73
 
72
- &::after {
73
- content: '';
74
+ & [data-indicator] {
74
75
  height: 1rem;
75
76
  aspect-ratio: 1;
76
- background: currentColor;
77
- mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
78
- mask-size: contain;
77
+ opacity: 0.5;
79
78
  transition: transform var(--dry-duration-fast) var(--dry-ease-default);
80
79
  }
81
80
  }
@@ -15,24 +15,60 @@
15
15
 
16
16
  <style>
17
17
  [data-alert-dialog-action] {
18
+ --dry-btn-accent: var(--dry-color-fill-error);
19
+ --dry-btn-accent-hover: var(--dry-color-fill-error-hover);
20
+ --dry-btn-accent-active: var(--dry-color-fill-error-hover);
21
+ --dry-btn-on-accent: var(--dry-color-on-error);
22
+ --dry-btn-border: transparent;
23
+
18
24
  cursor: pointer;
19
- background: var(--dry-color-fill-error);
20
- color: var(--dry-color-on-brand);
21
- border: none;
22
- padding: var(--dry-space-2) var(--dry-space-4);
25
+ background: var(--dry-btn-accent);
26
+ color: var(--dry-btn-on-accent);
27
+ border: 1px solid var(--dry-btn-border);
28
+ padding: var(--dry-space-2_5) var(--dry-space-4);
23
29
  border-radius: var(--dry-radius-md);
30
+ box-shadow: var(--dry-shadow-raised);
31
+ font-family: var(--dry-font-sans);
32
+ font-size: var(--dry-type-small-size, var(--dry-text-sm-size));
24
33
  font-weight: 500;
25
- min-height: var(--dry-space-11);
34
+ line-height: 1.25;
35
+ letter-spacing: -0.01em;
36
+ min-height: var(--dry-space-12);
26
37
  display: inline-grid;
38
+ grid-auto-flow: column;
27
39
  place-items: center;
40
+ gap: var(--dry-space-2);
41
+ text-decoration: none;
42
+ white-space: nowrap;
43
+ user-select: none;
44
+ transition:
45
+ background var(--dry-duration-fast) var(--dry-ease-default),
46
+ border-color var(--dry-duration-fast) var(--dry-ease-default),
47
+ box-shadow var(--dry-duration-fast) var(--dry-ease-default),
48
+ color var(--dry-duration-fast) var(--dry-ease-default),
49
+ opacity var(--dry-duration-fast) var(--dry-ease-default);
28
50
  }
29
51
 
30
52
  [data-alert-dialog-action]:hover {
31
- background: var(--dry-color-fill-error-hover);
53
+ background: var(--dry-btn-accent-hover);
54
+ }
55
+
56
+ [data-alert-dialog-action]:active:not(:disabled) {
57
+ background: var(--dry-btn-accent-active);
58
+ transform: translateY(1px);
32
59
  }
33
60
 
34
61
  [data-alert-dialog-action]:focus-visible {
35
62
  outline: 2px solid var(--dry-color-focus-ring);
36
63
  outline-offset: 2px;
64
+ box-shadow: 0 0 0 1px var(--dry-color-stroke-focus);
65
+ }
66
+
67
+ [data-alert-dialog-action]:disabled {
68
+ background: var(--dry-color-fill-disabled);
69
+ color: var(--dry-color-text-disabled);
70
+ border-color: var(--dry-color-stroke-disabled);
71
+ cursor: not-allowed;
72
+ box-shadow: none;
37
73
  }
38
74
  </style>
@@ -24,23 +24,62 @@
24
24
 
25
25
  <style>
26
26
  [data-alert-dialog-cancel] {
27
+ --dry-btn-accent-fg: var(--dry-color-text-brand);
28
+ --dry-btn-accent-stroke: var(--dry-color-stroke-brand);
29
+
27
30
  cursor: pointer;
28
- background: none;
29
- border: none;
30
- padding: var(--dry-space-2);
31
- color: var(--dry-color-text-weak);
31
+ background: transparent;
32
+ border: 1px solid var(--dry-btn-accent-stroke);
33
+ padding: var(--dry-space-2_5) var(--dry-space-4);
34
+ color: var(--dry-btn-accent-fg);
32
35
  border-radius: var(--dry-radius-sm);
33
- min-height: var(--dry-space-11);
36
+ box-shadow: var(--dry-shadow-raised);
37
+ font-family: var(--dry-font-sans);
38
+ font-size: var(--dry-type-small-size, var(--dry-text-sm-size));
39
+ font-weight: 500;
40
+ line-height: 1.25;
41
+ letter-spacing: -0.01em;
42
+ min-height: var(--dry-space-12);
34
43
  display: inline-grid;
44
+ grid-auto-flow: column;
35
45
  place-items: center;
46
+ gap: var(--dry-space-2);
47
+ text-decoration: none;
48
+ white-space: nowrap;
49
+ user-select: none;
50
+ transition:
51
+ background var(--dry-duration-fast) var(--dry-ease-default),
52
+ border-color var(--dry-duration-fast) var(--dry-ease-default),
53
+ box-shadow var(--dry-duration-fast) var(--dry-ease-default),
54
+ color var(--dry-duration-fast) var(--dry-ease-default),
55
+ opacity var(--dry-duration-fast) var(--dry-ease-default);
36
56
  }
37
57
 
38
58
  [data-alert-dialog-cancel]:hover {
59
+ background: var(--dry-color-bg-alternate);
60
+ border-color: var(--dry-color-stroke-strong);
61
+ color: var(--dry-color-text-strong);
62
+ box-shadow: var(--dry-shadow-sm);
63
+ }
64
+
65
+ [data-alert-dialog-cancel]:active:not(:disabled) {
66
+ background: var(--dry-color-fill);
67
+ border-color: var(--dry-color-stroke-strong);
39
68
  color: var(--dry-color-text-strong);
69
+ transform: translateY(1px);
40
70
  }
41
71
 
42
72
  [data-alert-dialog-cancel]:focus-visible {
43
73
  outline: 2px solid var(--dry-color-focus-ring);
44
74
  outline-offset: 2px;
75
+ box-shadow: 0 0 0 1px var(--dry-color-stroke-focus);
76
+ }
77
+
78
+ [data-alert-dialog-cancel]:disabled {
79
+ background: var(--dry-color-fill-disabled);
80
+ color: var(--dry-color-text-disabled);
81
+ border-color: var(--dry-color-stroke-disabled);
82
+ cursor: not-allowed;
83
+ box-shadow: none;
45
84
  }
46
85
  </style>
@@ -22,7 +22,8 @@
22
22
  display: grid;
23
23
  grid-auto-flow: column;
24
24
  grid-auto-columns: max-content;
25
- gap: var(--dry-space-2);
26
- justify-self: end;
25
+ gap: var(--dry-space-4);
26
+ justify-content: start;
27
+ justify-items: start;
27
28
  }
28
29
  </style>
@@ -1,8 +1,14 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import type { HTMLAttributes } from 'svelte/elements';
3
+ import type { BlendMode } from '@dryui/primitives/aurora';
3
4
  interface Props extends HTMLAttributes<HTMLDivElement> {
4
5
  palette?: 'sunrise' | 'ocean' | 'forest' | 'cosmic' | readonly [string, string, string];
5
6
  speed?: 'slow' | 'normal' | 'fast' | number;
7
+ intensity?: number;
8
+ waviness?: number;
9
+ colorSpace?: 'srgb' | 'oklch' | 'oklab';
10
+ blendMode?: BlendMode;
11
+ layerOpacity?: number;
6
12
  children?: Snippet;
7
13
  }
8
14
  declare const Aurora: import('svelte').Component<Props, {}, ''>;
@@ -19,7 +19,7 @@
19
19
  angle = 45,
20
20
  speed = 3,
21
21
  intensity = 70,
22
- blendMode = 'screen',
22
+ blendMode,
23
23
  children,
24
24
  class: className,
25
25
  style,
@@ -31,26 +31,33 @@
31
31
  const gradientString = $derived(
32
32
  `linear-gradient(${angle}deg, transparent 0%, transparent calc(50% - ${width}px), ${color} 50%, transparent calc(50% + ${width}px), transparent 100%)`
33
33
  );
34
+ const rootStyle = $derived.by(() => {
35
+ const declarations = [
36
+ style,
37
+ `--dry-beam-speed: ${speedValue}`,
38
+ `--dry-beam-intensity: ${clampedIntensity}`,
39
+ `--dry-beam-width: ${width}px`,
40
+ blendMode ? `--dry-beam-blend: ${blendMode}` : null
41
+ ].filter(Boolean);
42
+ return declarations.join('; ');
43
+ });
44
+ const layerStyle = $derived(`background-image: ${gradientString};`);
34
45
 
35
46
  function applyRootStyles(node: HTMLElement) {
36
47
  $effect(() => {
37
- node.style.cssText = style || '';
38
- node.style.setProperty('--dry-beam-speed', speedValue);
39
- node.style.setProperty('--dry-beam-intensity', clampedIntensity);
40
- node.style.setProperty('--dry-beam-blend', blendMode);
41
- node.style.setProperty('--dry-beam-width', `${width}px`);
48
+ node.style.cssText = rootStyle;
42
49
  });
43
50
  }
44
51
 
45
52
  function applyLayerStyles(node: HTMLElement) {
46
53
  $effect(() => {
47
- node.style.setProperty('background-image', gradientString);
54
+ node.style.cssText = layerStyle;
48
55
  });
49
56
  }
50
57
  </script>
51
58
 
52
- <div data-beam class={className} {...rest} use:applyRootStyles>
53
- <div data-beam-layer use:applyLayerStyles></div>
59
+ <div data-beam class={className} {...rest} {@attach applyRootStyles}>
60
+ <div data-beam-layer {@attach applyLayerStyles}></div>
54
61
  {#if children}
55
62
  {@render children()}
56
63
  {/if}
@@ -69,7 +76,7 @@
69
76
  pointer-events: none;
70
77
  background-size: 300% 300%;
71
78
  opacity: calc(var(--dry-beam-intensity, 70) / 100);
72
- mix-blend-mode: var(--dry-beam-blend, screen);
79
+ mix-blend-mode: var(--dry-beam-blend, var(--dry-beam-default-blend, multiply));
73
80
  filter: blur(calc(var(--dry-beam-width, 2px) * 2));
74
81
  animation: beam-sweep var(--dry-beam-speed, 3s) ease-in-out infinite;
75
82
  }
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { createId } from '@dryui/primitives';
2
3
  import type { Snippet } from 'svelte';
3
4
  import type { HTMLAttributes } from 'svelte/elements';
4
5
 
@@ -8,52 +9,92 @@
8
9
  children: Snippet;
9
10
  }
10
11
 
11
- let instanceCounter = 0;
12
-
13
12
  let { offset = 3, angle = 0, children, class: className, style, ...rest }: Props = $props();
14
13
 
15
- const filterId = `dry-chromatic-${instanceCounter++}`;
14
+ const filterId = createId('dry-chromatic');
16
15
 
17
16
  const offsetX = $derived(Math.round(offset * Math.cos((angle * Math.PI) / 180)));
18
17
  const offsetY = $derived(Math.round(offset * Math.sin((angle * Math.PI) / 180)));
19
18
 
20
19
  function applyFilterStyles(node: HTMLElement) {
21
- $effect(() => {
22
- node.style.cssText = style || '';
23
- node.style.setProperty('filter', `url(#${filterId})`);
24
- });
20
+ node.style.cssText = style || '';
21
+ node.style.setProperty('filter', `url(#${filterId})`);
25
22
  }
26
23
  </script>
27
24
 
28
- <svg data-chromatic-aberration-svg aria-hidden="true">
25
+ <svg data-chromatic-aberration-svg width="0" height="0" aria-hidden="true">
29
26
  <defs>
30
27
  <filter id={filterId} color-interpolation-filters="sRGB">
31
- <feOffset in="SourceGraphic" dx={offsetX} dy={offsetY} result="red" />
28
+ <feOffset in="SourceGraphic" dx={offsetX} dy={offsetY} result="lightRed" />
32
29
  <feColorMatrix
33
- in="red"
30
+ in="lightRed"
34
31
  type="matrix"
35
32
  values="1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0"
36
- result="redOnly"
33
+ result="lightRedOnly"
37
34
  />
38
- <feOffset in="SourceGraphic" dx={-offsetX} dy={-offsetY} result="blue" />
35
+ <feOffset in="SourceGraphic" dx={-offsetX} dy={-offsetY} result="lightBlue" />
39
36
  <feColorMatrix
40
- in="blue"
37
+ in="lightBlue"
41
38
  type="matrix"
42
39
  values="0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0"
43
- result="blueOnly"
40
+ result="lightBlueOnly"
41
+ />
42
+ <feColorMatrix
43
+ in="SourceGraphic"
44
+ type="matrix"
45
+ values="0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0"
46
+ result="lightGreenOnly"
44
47
  />
48
+ <feBlend in="lightRedOnly" in2="lightGreenOnly" mode="screen" result="lightRG" />
49
+ <feBlend in="lightRG" in2="lightBlueOnly" mode="screen" result="lightSplit" />
45
50
  <feColorMatrix
46
51
  in="SourceGraphic"
47
52
  type="matrix"
53
+ values="-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 0 0 0 1 0"
54
+ result="darkSource"
55
+ />
56
+ <feOffset in="darkSource" dx={offsetX} dy={offsetY} result="darkRed" />
57
+ <feColorMatrix
58
+ in="darkRed"
59
+ type="matrix"
60
+ values="1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0"
61
+ result="darkRedOnly"
62
+ />
63
+ <feOffset in="darkSource" dx={-offsetX} dy={-offsetY} result="darkBlue" />
64
+ <feColorMatrix
65
+ in="darkBlue"
66
+ type="matrix"
67
+ values="0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0"
68
+ result="darkBlueOnly"
69
+ />
70
+ <feColorMatrix
71
+ in="darkSource"
72
+ type="matrix"
48
73
  values="0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0"
49
- result="greenOnly"
74
+ result="darkGreenOnly"
75
+ />
76
+ <feBlend in="darkRedOnly" in2="darkGreenOnly" mode="screen" result="darkRG" />
77
+ <feBlend in="darkRG" in2="darkBlueOnly" mode="screen" result="darkSplitInverted" />
78
+ <feColorMatrix
79
+ in="darkSplitInverted"
80
+ type="matrix"
81
+ values="-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 0 0 0 1 0"
82
+ result="darkSplit"
50
83
  />
51
- <feBlend in="redOnly" in2="greenOnly" mode="screen" result="rg" />
52
- <feBlend in="rg" in2="blueOnly" mode="screen" />
84
+ <feColorMatrix in="SourceGraphic" type="luminanceToAlpha" result="lightMask" />
85
+ <feComponentTransfer in="lightMask" result="darkMask">
86
+ <feFuncA type="table" tableValues="1 0" />
87
+ </feComponentTransfer>
88
+ <feComposite in="lightSplit" in2="lightMask" operator="in" result="lightApplied" />
89
+ <feComposite in="darkSplit" in2="darkMask" operator="in" result="darkApplied" />
90
+ <feMerge>
91
+ <feMergeNode in="lightApplied" />
92
+ <feMergeNode in="darkApplied" />
93
+ </feMerge>
53
94
  </filter>
54
95
  </defs>
55
96
  </svg>
56
- <div data-chromatic-aberration class={className} use:applyFilterStyles {...rest}>
97
+ <div data-chromatic-aberration class={className} {@attach applyFilterStyles} {...rest}>
57
98
  {@render children()}
58
99
  </div>
59
100
 
@@ -62,6 +103,7 @@
62
103
  position: absolute;
63
104
  height: 0;
64
105
  overflow: hidden;
106
+ pointer-events: none;
65
107
  }
66
108
 
67
109
  [data-chromatic-aberration] {
@@ -25,6 +25,7 @@
25
25
  {...rest}
26
26
  >
27
27
  {@render children()}
28
+ <svg data-indicator xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg>
28
29
  </button>
29
30
 
30
31
  <style>
@@ -32,6 +33,7 @@
32
33
  display: grid;
33
34
  grid-template-columns: 1fr 1rem;
34
35
  align-items: center;
36
+ gap: var(--dry-space-2);
35
37
  padding: var(--dry-space-3) var(--dry-space-4);
36
38
  background: none;
37
39
  border: 1px solid var(--dry-color-stroke-weak);
@@ -41,19 +43,14 @@
41
43
  cursor: pointer;
42
44
  color: var(--dry-color-text-strong);
43
45
 
44
- &::after {
45
- content: '';
46
+ & [data-indicator] {
46
47
  height: 1rem;
47
48
  aspect-ratio: 1;
48
- background: currentColor;
49
- mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
50
- mask-size: contain;
51
- mask-repeat: no-repeat;
52
49
  opacity: 0.5;
53
50
  transition: transform var(--dry-duration-fast) var(--dry-ease-default);
54
51
  }
55
52
 
56
- &[data-state='open']::after {
53
+ &[data-state='open'] [data-indicator] {
57
54
  transform: rotate(180deg);
58
55
  }
59
56
 
@@ -42,6 +42,8 @@
42
42
  >
43
43
  {#if children}
44
44
  {@render children()}
45
+ {:else}
46
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 3H5a2 2 0 0 0-2 2v4m6-6h10a2 2 0 0 1 2 2v4M9 3l8 8M3 9l8 8m-8-8 1.5 1.5M11 17l-1 4-4 1 1-4"/></svg>
45
47
  {/if}
46
48
  </button>
47
49
  {/if}
@@ -65,18 +67,9 @@
65
67
  border-color var(--dry-duration-fast) var(--dry-ease-default);
66
68
  }
67
69
 
68
- [data-cp-eyedropper]::before {
69
- content: '';
70
- display: inline-block;
71
- height: 16px;
70
+ [data-cp-eyedropper] svg {
71
+ height: 1rem;
72
72
  aspect-ratio: 1;
73
- background-color: currentColor;
74
- -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 3H5a2 2 0 0 0-2 2v4m6-6h10a2 2 0 0 1 2 2v4M9 3l8 8M3 9l8 8m-8-8 1.5 1.5M11 17l-1 4-4 1 1-4'/%3E%3C/svg%3E");
75
- mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 3H5a2 2 0 0 0-2 2v4m6-6h10a2 2 0 0 1 2 2v4M9 3l8 8M3 9l8 8m-8-8 1.5 1.5M11 17l-1 4-4 1 1-4'/%3E%3C/svg%3E");
76
- -webkit-mask-repeat: no-repeat;
77
- mask-repeat: no-repeat;
78
- -webkit-mask-size: contain;
79
- mask-size: contain;
80
73
  }
81
74
 
82
75
  [data-cp-eyedropper]:hover:not([data-disabled]):not(:disabled) {
@@ -82,8 +82,10 @@
82
82
  <style>
83
83
  [data-dg-pagination] {
84
84
  display: grid;
85
- grid-template-columns: max-content 1fr max-content;
85
+ grid-template-columns: minmax(0, 1fr) max-content minmax(0, 1fr);
86
+ grid-template-areas: 'prev info next';
86
87
  align-items: center;
88
+ column-gap: var(--dry-data-grid-pagination-gap);
87
89
  padding: var(--dry-space-3) var(--dry-data-grid-padding-x);
88
90
  border-top: 1px solid var(--dry-data-grid-border);
89
91
  font-size: var(--dry-type-small-size, var(--dry-type-small-size));
@@ -122,18 +124,34 @@
122
124
  outline-offset: 2px;
123
125
  }
124
126
 
127
+ [data-dg-pagination] button[data-pagination-prev] {
128
+ grid-area: prev;
129
+ justify-self: start;
130
+ }
131
+
132
+ [data-dg-pagination] button[data-pagination-next] {
133
+ grid-area: next;
134
+ justify-self: end;
135
+ }
136
+
125
137
  [data-dg-pagination] button:disabled {
126
138
  opacity: 0.4;
127
139
  cursor: not-allowed;
128
140
  }
129
141
 
130
142
  [data-dg-pagination] [data-pagination-info] {
143
+ grid-area: info;
144
+ justify-self: center;
145
+ text-align: center;
131
146
  font-variant-numeric: tabular-nums;
132
147
  }
133
148
 
134
149
  @container (max-width: 400px) {
135
150
  [data-dg-pagination] {
136
- grid-template-columns: 1fr;
151
+ grid-template-columns: repeat(2, minmax(0, 1fr));
152
+ grid-template-areas:
153
+ 'info info'
154
+ 'prev next';
137
155
  gap: var(--dry-space-2);
138
156
  }
139
157
  }
@@ -214,6 +214,7 @@
214
214
  --dry-data-grid-sort-color: var(--dry-color-fill-brand);
215
215
  --dry-data-grid-pagination-gap: var(--dry-space-2);
216
216
 
217
+ display: grid;
217
218
  container-type: inline-size;
218
219
  overflow-x: auto;
219
220
  }
@@ -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 { SvelteMap } from 'svelte/reactivity';
4
5
  import { setDateFieldCtx, getLocaleFormat, type DateSegmentType } from './context.svelte.js';
5
6
 
6
7
  interface Props extends HTMLAttributes<HTMLDivElement> {
@@ -14,6 +15,12 @@
14
15
  children: Snippet;
15
16
  }
16
17
 
18
+ interface SegmentValues {
19
+ month: number | null;
20
+ day: number | null;
21
+ year: number | null;
22
+ }
23
+
17
24
  let {
18
25
  value = $bindable<Date | null>(null),
19
26
  name,
@@ -27,37 +34,61 @@
27
34
  ...rest
28
35
  }: Props = $props();
29
36
 
30
- let month = $state<number | null>(value ? value.getMonth() + 1 : null);
31
- let day = $state<number | null>(value ? value.getDate() : null);
32
- let year = $state<number | null>(value ? value.getFullYear() : null);
37
+ let draftValues = $state<SegmentValues | null>(null);
33
38
 
34
- // Sync from value prop
35
- $effect(() => {
36
- if (value) {
37
- month = value.getMonth() + 1;
38
- day = value.getDate();
39
- year = value.getFullYear();
39
+ function getSegmentValues(date: Date | null): SegmentValues {
40
+ if (!date) {
41
+ return { month: null, day: null, year: null };
40
42
  }
41
- });
42
43
 
43
- function tryBuildDate() {
44
- if (month !== null && day !== null && year !== null && year >= 1000) {
45
- const d = new Date(year, month - 1, day);
46
- if (d.getMonth() === month - 1 && d.getDate() === day) {
44
+ return {
45
+ month: date.getMonth() + 1,
46
+ day: date.getDate(),
47
+ year: date.getFullYear()
48
+ };
49
+ }
50
+
51
+ function commitSegments(nextValues: SegmentValues) {
52
+ if (
53
+ nextValues.month !== null &&
54
+ nextValues.day !== null &&
55
+ nextValues.year !== null &&
56
+ nextValues.year >= 1000
57
+ ) {
58
+ const d = new Date(nextValues.year, nextValues.month - 1, nextValues.day);
59
+ if (d.getMonth() === nextValues.month - 1 && d.getDate() === nextValues.day) {
47
60
  value = d;
61
+ draftValues = null;
62
+ return;
48
63
  }
49
64
  }
65
+
66
+ draftValues = nextValues;
50
67
  }
51
68
 
52
69
  const localeFormat = $derived(getLocaleFormat(locale));
70
+ const committedValues = $derived(getSegmentValues(value));
71
+ const activeValues = $derived(draftValues ?? committedValues);
53
72
 
54
73
  const segments = $derived(
55
74
  localeFormat.order.map((type) => ({
56
75
  type,
57
- value: type === 'month' ? month : type === 'day' ? day : year
76
+ value:
77
+ type === 'month'
78
+ ? activeValues.month
79
+ : type === 'day'
80
+ ? activeValues.day
81
+ : activeValues.year
58
82
  }))
59
83
  );
60
84
 
85
+ function focusPreferredSegment() {
86
+ const targetType =
87
+ segments.find((segment) => segment.value === null)?.type ?? localeFormat.order[0];
88
+ if (!targetType) return;
89
+ segmentElements.get(targetType)?.focus();
90
+ }
91
+
61
92
  function serializeDateValue(date: Date | null): string {
62
93
  if (!date) return '';
63
94
 
@@ -69,7 +100,7 @@
69
100
  }
70
101
 
71
102
  // Segment element registry for index-based navigation
72
- const segmentElements = new Map<DateSegmentType, HTMLElement>();
103
+ const segmentElements = new SvelteMap<DateSegmentType, HTMLElement>();
73
104
 
74
105
  setDateFieldCtx({
75
106
  get value() {
@@ -97,10 +128,11 @@
97
128
  return segments;
98
129
  },
99
130
  updateSegment(type: DateSegmentType, val: number) {
100
- if (type === 'month') month = val;
101
- else if (type === 'day') day = val;
102
- else if (type === 'year') year = val;
103
- tryBuildDate();
131
+ commitSegments({
132
+ month: type === 'month' ? val : activeValues.month,
133
+ day: type === 'day' ? val : activeValues.day,
134
+ year: type === 'year' ? val : activeValues.year
135
+ });
104
136
  },
105
137
  registerSegment(type: DateSegmentType, el: HTMLElement) {
106
138
  segmentElements.set(type, el);
@@ -119,12 +151,26 @@
119
151
  }
120
152
  }
121
153
  });
154
+
155
+ function handleMousedown(event: MouseEvent) {
156
+ if (disabled) return;
157
+ const target = event.target;
158
+ if (!(target instanceof HTMLElement)) return;
159
+ if (target.closest('[data-df-segment]')) return;
160
+
161
+ event.preventDefault();
162
+ focusPreferredSegment();
163
+ }
122
164
  </script>
123
165
 
124
166
  <div
125
167
  role="group"
126
168
  aria-label="Date"
169
+ data-df-wrapper
170
+ data-df-root
171
+ data-size={size}
127
172
  data-disabled={disabled || undefined}
173
+ onmousedown={handleMousedown}
128
174
  {...rest}
129
175
  class={className}
130
176
  >