@dryui/primitives 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.
@@ -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: ${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', gradientString);
54
+ node.style.cssText = layerStyle;
48
55
  });
49
56
  }
50
57
  </script>
51
58
 
52
- <div class={['beam', className]} {...rest} use:applyRootStyles>
53
- <div class="beam-layer" use:applyLayerStyles></div>
59
+ <div class={['beam', className]} {...rest} {@attach applyRootStyles}>
60
+ <div class="beam-layer" {@attach applyLayerStyles}></div>
54
61
  {#if children}
55
62
  {@render children()}
56
63
  {/if}
@@ -65,5 +72,26 @@
65
72
  position: absolute;
66
73
  inset: 0;
67
74
  pointer-events: none;
75
+ background-size: 300% 300%;
76
+ opacity: calc(var(--dry-beam-intensity, 70) / 100);
77
+ mix-blend-mode: var(--dry-beam-blend, var(--dry-beam-default-blend, multiply));
78
+ filter: blur(calc(var(--dry-beam-width, 2px) * 2));
79
+ animation: beam-sweep var(--dry-beam-speed, 3s) ease-in-out infinite;
80
+ }
81
+
82
+ @keyframes beam-sweep {
83
+ 0% {
84
+ background-position: -50% -50%;
85
+ }
86
+ 100% {
87
+ background-position: 150% 150%;
88
+ }
89
+ }
90
+
91
+ @media (prefers-reduced-motion: reduce) {
92
+ .beam-layer {
93
+ animation: none;
94
+ background-position: 50% 50%;
95
+ }
68
96
  }
69
97
  </style>
@@ -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 { createId } from '../utils/create-id.js';
4
5
 
5
6
  interface Props extends HTMLAttributes<HTMLDivElement> {
6
7
  offset?: number;
@@ -8,61 +9,101 @@
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 class="chromatic-aberration-svg" aria-hidden="true">
25
+ <svg class="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 class={['chromatic-aberration', className]} use:applyFilterStyles {...rest}>
97
+ <div class={['chromatic-aberration', className]} {@attach applyFilterStyles} {...rest}>
57
98
  {@render children()}
58
99
  </div>
59
100
 
60
101
  <style>
61
102
  .chromatic-aberration-svg {
62
103
  position: absolute;
63
- width: 0;
64
104
  height: 0;
65
105
  overflow: hidden;
106
+ pointer-events: none;
66
107
  }
67
108
 
68
109
  .chromatic-aberration {
@@ -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> {
@@ -13,6 +14,12 @@
13
14
  children: Snippet;
14
15
  }
15
16
 
17
+ interface SegmentValues {
18
+ month: number | null;
19
+ day: number | null;
20
+ year: number | null;
21
+ }
22
+
16
23
  let {
17
24
  value = $bindable<Date | null>(null),
18
25
  name,
@@ -24,37 +31,61 @@
24
31
  ...rest
25
32
  }: Props = $props();
26
33
 
27
- let month = $state<number | null>(value ? value.getMonth() + 1 : null);
28
- let day = $state<number | null>(value ? value.getDate() : null);
29
- let year = $state<number | null>(value ? value.getFullYear() : null);
34
+ let draftValues = $state<SegmentValues | null>(null);
30
35
 
31
- // Sync from value prop
32
- $effect(() => {
33
- if (value) {
34
- month = value.getMonth() + 1;
35
- day = value.getDate();
36
- year = value.getFullYear();
36
+ function getSegmentValues(date: Date | null): SegmentValues {
37
+ if (!date) {
38
+ return { month: null, day: null, year: null };
37
39
  }
38
- });
39
40
 
40
- function tryBuildDate() {
41
- if (month !== null && day !== null && year !== null && year >= 1000) {
42
- const d = new Date(year, month - 1, day);
43
- if (d.getMonth() === month - 1 && d.getDate() === day) {
41
+ return {
42
+ month: date.getMonth() + 1,
43
+ day: date.getDate(),
44
+ year: date.getFullYear()
45
+ };
46
+ }
47
+
48
+ function commitSegments(nextValues: SegmentValues) {
49
+ if (
50
+ nextValues.month !== null &&
51
+ nextValues.day !== null &&
52
+ nextValues.year !== null &&
53
+ nextValues.year >= 1000
54
+ ) {
55
+ const d = new Date(nextValues.year, nextValues.month - 1, nextValues.day);
56
+ if (d.getMonth() === nextValues.month - 1 && d.getDate() === nextValues.day) {
44
57
  value = d;
58
+ draftValues = null;
59
+ return;
45
60
  }
46
61
  }
62
+
63
+ draftValues = nextValues;
47
64
  }
48
65
 
49
66
  const localeFormat = $derived(getLocaleFormat(locale));
67
+ const committedValues = $derived(getSegmentValues(value));
68
+ const activeValues = $derived(draftValues ?? committedValues);
50
69
 
51
70
  const segments = $derived(
52
71
  localeFormat.order.map((type) => ({
53
72
  type,
54
- value: type === 'month' ? month : type === 'day' ? day : year
73
+ value:
74
+ type === 'month'
75
+ ? activeValues.month
76
+ : type === 'day'
77
+ ? activeValues.day
78
+ : activeValues.year
55
79
  }))
56
80
  );
57
81
 
82
+ function focusPreferredSegment() {
83
+ const targetType =
84
+ segments.find((segment) => segment.value === null)?.type ?? localeFormat.order[0];
85
+ if (!targetType) return;
86
+ segmentElements.get(targetType)?.focus();
87
+ }
88
+
58
89
  function serializeDateValue(date: Date | null): string {
59
90
  if (!date) return '';
60
91
 
@@ -66,7 +97,7 @@
66
97
  }
67
98
 
68
99
  // Segment element registry for index-based navigation
69
- const segmentElements = new Map<DateSegmentType, HTMLElement>();
100
+ const segmentElements = new SvelteMap<DateSegmentType, HTMLElement>();
70
101
 
71
102
  setDateFieldCtx({
72
103
  get value() {
@@ -94,10 +125,11 @@
94
125
  return segments;
95
126
  },
96
127
  updateSegment(type: DateSegmentType, val: number) {
97
- if (type === 'month') month = val;
98
- else if (type === 'day') day = val;
99
- else if (type === 'year') year = val;
100
- tryBuildDate();
128
+ commitSegments({
129
+ month: type === 'month' ? val : activeValues.month,
130
+ day: type === 'day' ? val : activeValues.day,
131
+ year: type === 'year' ? val : activeValues.year
132
+ });
101
133
  },
102
134
  registerSegment(type: DateSegmentType, el: HTMLElement) {
103
135
  segmentElements.set(type, el);
@@ -116,9 +148,27 @@
116
148
  }
117
149
  }
118
150
  });
151
+
152
+ function handleMousedown(event: MouseEvent) {
153
+ if (disabled) return;
154
+ const target = event.target;
155
+ if (!(target instanceof HTMLElement)) return;
156
+ if (target.closest('[data-df-segment]')) return;
157
+
158
+ event.preventDefault();
159
+ focusPreferredSegment();
160
+ }
119
161
  </script>
120
162
 
121
- <div role="group" aria-label="Date" data-disabled={disabled || undefined} {...rest}>
163
+ <div
164
+ role="group"
165
+ aria-label="Date"
166
+ data-df-wrapper
167
+ data-df-root
168
+ data-disabled={disabled || undefined}
169
+ onmousedown={handleMousedown}
170
+ {...rest}
171
+ >
122
172
  {@render children()}
123
173
 
124
174
  {#if name}
@@ -1,7 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { HTMLAttributes } from 'svelte/elements';
3
3
  import { getDateFieldCtx, type DateSegmentType } from './context.svelte.js';
4
- import { onMount } from 'svelte';
5
4
 
6
5
  interface Props extends HTMLAttributes<HTMLSpanElement> {
7
6
  type: DateSegmentType;
@@ -11,13 +10,6 @@
11
10
 
12
11
  const ctx = getDateFieldCtx();
13
12
 
14
- let el: HTMLSpanElement;
15
-
16
- onMount(() => {
17
- ctx.registerSegment(type, el);
18
- return () => ctx.unregisterSegment(type);
19
- });
20
-
21
13
  const segmentData = $derived(ctx.segments.find((s) => s.type === type));
22
14
 
23
15
  const minValue = $derived(type === 'month' ? 1 : type === 'day' ? 1 : 1000);
@@ -35,6 +27,15 @@
35
27
  let inputBuffer = '';
36
28
  let bufferTimeout: ReturnType<typeof setTimeout>;
37
29
 
30
+ function registerSegment(node: HTMLSpanElement) {
31
+ ctx.registerSegment(type, node);
32
+ return {
33
+ destroy() {
34
+ ctx.unregisterSegment(type);
35
+ }
36
+ };
37
+ }
38
+
38
39
  function increment() {
39
40
  const current = segmentData?.value ?? minValue - 1;
40
41
  const next = current >= maxValue ? minValue : current + 1;
@@ -111,7 +112,7 @@
111
112
  </script>
112
113
 
113
114
  <span
114
- bind:this={el}
115
+ {@attach registerSegment}
115
116
  role="spinbutton"
116
117
  tabindex={ctx.disabled ? undefined : 0}
117
118
  aria-label={type}
@@ -119,6 +120,7 @@
119
120
  aria-valuemax={maxValue}
120
121
  aria-valuenow={segmentData?.value ?? undefined}
121
122
  aria-valuetext={displayValue}
123
+ data-df-segment
122
124
  data-segment={type}
123
125
  data-placeholder={segmentData?.value === null ? '' : undefined}
124
126
  data-disabled={ctx.disabled || undefined}
@@ -13,4 +13,4 @@
13
13
  const display = $derived(separator ?? ctx.separator);
14
14
  </script>
15
15
 
16
- <span aria-hidden="true" data-separator="" {...rest}>{display}</span>
16
+ <span aria-hidden="true" data-df-separator data-separator="" {...rest}>{display}</span>
@@ -21,20 +21,15 @@
21
21
  ...rest
22
22
  }: Props = $props();
23
23
 
24
- let containerEl = $state<HTMLDivElement>();
25
24
  let dragging = $state(false);
26
25
 
27
- function setContainer(node: HTMLDivElement) {
28
- containerEl = node;
29
- return {
30
- destroy() {
31
- if (containerEl === node) containerEl = undefined;
32
- }
33
- };
26
+ function syncStyles(node: HTMLElement) {
27
+ $effect(() => {
28
+ node.style.cssText = `${style ? `${style}; ` : ''}--dry-image-comparison-position: ${position}%`;
29
+ });
34
30
  }
35
31
 
36
- function updatePosition(clientX: number, clientY: number) {
37
- if (!containerEl) return;
32
+ function updatePosition(clientX: number, clientY: number, containerEl: HTMLElement) {
38
33
  const rect = containerEl.getBoundingClientRect();
39
34
  let pct: number;
40
35
  if (orientation === 'horizontal') {
@@ -47,13 +42,16 @@
47
42
 
48
43
  function onPointerDown(e: PointerEvent) {
49
44
  dragging = true;
50
- (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
51
- updatePosition(e.clientX, e.clientY);
45
+ const handleEl = e.currentTarget as HTMLElement;
46
+ handleEl.setPointerCapture(e.pointerId);
47
+ const containerEl = handleEl.closest<HTMLElement>('.root');
48
+ if (!containerEl) return;
49
+ updatePosition(e.clientX, e.clientY, containerEl);
52
50
  }
53
51
 
54
52
  function onPointerMove(e: PointerEvent) {
55
53
  if (!dragging) return;
56
- updatePosition(e.clientX, e.clientY);
54
+ updatePosition(e.clientX, e.clientY, e.currentTarget as HTMLElement);
57
55
  }
58
56
 
59
57
  function onPointerUp() {
@@ -70,24 +68,16 @@
70
68
  position = Math.min(position + step, 100);
71
69
  }
72
70
  }
73
-
74
- function applyStyles(node: HTMLElement) {
75
- $effect(() => {
76
- node.style.cssText = style || '';
77
- node.style.setProperty('--dry-image-comparison-position', `${position}%`);
78
- });
79
- }
80
71
  </script>
81
72
 
82
73
  <div
83
- {@attach setContainer}
74
+ {@attach syncStyles}
84
75
  data-orientation={orientation}
85
76
  data-dragging={dragging || undefined}
86
77
  class={['root', className]}
87
78
  onpointermove={onPointerMove}
88
79
  onpointerup={onPointerUp}
89
80
  {...rest}
90
- use:applyStyles
91
81
  >
92
82
  <div data-part="after" class="layer">
93
83
  {@render after()}
@@ -119,6 +109,8 @@
119
109
  .root {
120
110
  --dry-image-comparison-handle-z-index: 1;
121
111
  position: relative;
112
+ display: grid;
113
+ height: 100%;
122
114
  overflow: hidden;
123
115
  touch-action: none;
124
116
  user-select: none;
@@ -130,8 +122,8 @@
130
122
  }
131
123
 
132
124
  .layer {
133
- position: absolute;
134
- inset: 0;
125
+ grid-area: 1 / 1;
126
+ min-height: 0;
135
127
  }
136
128
 
137
129
  .root[data-orientation='horizontal'] .before {
@@ -144,9 +136,8 @@
144
136
 
145
137
  .handle {
146
138
  position: absolute;
147
- display: flex;
148
- align-items: center;
149
- justify-content: center;
139
+ display: grid;
140
+ place-items: center;
150
141
  z-index: var(--dry-image-comparison-handle-z-index);
151
142
  }
152
143
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dryui/primitives",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "author": "Rob Balfre",
5
5
  "license": "MIT",
6
6
  "repository": {