dialkit 0.2.2 → 1.1.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.
- package/README.md +135 -2
- package/dist/index.cjs +512 -176
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +40 -8
- package/dist/index.d.ts +40 -8
- package/dist/index.js +484 -150
- package/dist/index.js.map +1 -1
- package/dist/solid/index.cjs +2612 -0
- package/dist/solid/index.cjs.map +1 -0
- package/dist/solid/index.d.cts +228 -0
- package/dist/solid/index.d.ts +228 -0
- package/dist/solid/index.js +2573 -0
- package/dist/solid/index.js.map +1 -0
- package/dist/store/index.cjs +450 -0
- package/dist/store/index.cjs.map +1 -0
- package/dist/store/index.d.cts +124 -0
- package/dist/store/index.d.ts +124 -0
- package/dist/store/index.js +425 -0
- package/dist/store/index.js.map +1 -0
- package/dist/styles.css +38 -1
- package/dist/svelte/Portal.svelte +30 -0
- package/dist/svelte/Portal.svelte.d.ts +9 -0
- package/dist/svelte/Portal.svelte.d.ts.map +1 -0
- package/dist/svelte/components/ButtonGroup.svelte +16 -0
- package/dist/svelte/components/ButtonGroup.svelte.d.ts +11 -0
- package/dist/svelte/components/ButtonGroup.svelte.d.ts.map +1 -0
- package/dist/svelte/components/ColorControl.svelte +81 -0
- package/dist/svelte/components/ColorControl.svelte.d.ts +9 -0
- package/dist/svelte/components/ColorControl.svelte.d.ts.map +1 -0
- package/dist/svelte/components/ControlRenderer.svelte +84 -0
- package/dist/svelte/components/ControlRenderer.svelte.d.ts +11 -0
- package/dist/svelte/components/ControlRenderer.svelte.d.ts.map +1 -0
- package/dist/svelte/components/DialRoot.svelte +65 -0
- package/dist/svelte/components/DialRoot.svelte.d.ts +11 -0
- package/dist/svelte/components/DialRoot.svelte.d.ts.map +1 -0
- package/dist/svelte/components/EasingVisualization.svelte +50 -0
- package/dist/svelte/components/EasingVisualization.svelte.d.ts +10 -0
- package/dist/svelte/components/EasingVisualization.svelte.d.ts.map +1 -0
- package/dist/svelte/components/Folder.svelte +204 -0
- package/dist/svelte/components/Folder.svelte.d.ts +14 -0
- package/dist/svelte/components/Folder.svelte.d.ts.map +1 -0
- package/dist/svelte/components/Panel.svelte +144 -0
- package/dist/svelte/components/Panel.svelte.d.ts +10 -0
- package/dist/svelte/components/Panel.svelte.d.ts.map +1 -0
- package/dist/svelte/components/PresetManager.svelte +156 -0
- package/dist/svelte/components/PresetManager.svelte.d.ts +10 -0
- package/dist/svelte/components/PresetManager.svelte.d.ts.map +1 -0
- package/dist/svelte/components/SegmentedControl.svelte +106 -0
- package/dist/svelte/components/SegmentedControl.svelte.d.ts +13 -0
- package/dist/svelte/components/SegmentedControl.svelte.d.ts.map +1 -0
- package/dist/svelte/components/SelectControl.svelte +151 -0
- package/dist/svelte/components/SelectControl.svelte.d.ts +14 -0
- package/dist/svelte/components/SelectControl.svelte.d.ts.map +1 -0
- package/dist/svelte/components/Slider.svelte +345 -0
- package/dist/svelte/components/Slider.svelte.d.ts +13 -0
- package/dist/svelte/components/Slider.svelte.d.ts.map +1 -0
- package/dist/svelte/components/SpringControl.svelte +124 -0
- package/dist/svelte/components/SpringControl.svelte.d.ts +12 -0
- package/dist/svelte/components/SpringControl.svelte.d.ts.map +1 -0
- package/dist/svelte/components/SpringVisualization.svelte +115 -0
- package/dist/svelte/components/SpringVisualization.svelte.d.ts +9 -0
- package/dist/svelte/components/SpringVisualization.svelte.d.ts.map +1 -0
- package/dist/svelte/components/TextControl.svelte +19 -0
- package/dist/svelte/components/TextControl.svelte.d.ts +10 -0
- package/dist/svelte/components/TextControl.svelte.d.ts.map +1 -0
- package/dist/svelte/components/Toggle.svelte +21 -0
- package/dist/svelte/components/Toggle.svelte.d.ts +9 -0
- package/dist/svelte/components/Toggle.svelte.d.ts.map +1 -0
- package/dist/svelte/components/TransitionControl.svelte +200 -0
- package/dist/svelte/components/TransitionControl.svelte.d.ts +12 -0
- package/dist/svelte/components/TransitionControl.svelte.d.ts.map +1 -0
- package/dist/svelte/components/transitions.d.ts +6 -0
- package/dist/svelte/components/transitions.d.ts.map +1 -0
- package/dist/svelte/components/transitions.js +14 -0
- package/dist/svelte/createDialKit.svelte.d.ts +7 -0
- package/dist/svelte/createDialKit.svelte.d.ts.map +1 -0
- package/dist/svelte/createDialKit.svelte.js +82 -0
- package/dist/svelte/index.d.ts +19 -0
- package/dist/svelte/index.d.ts.map +1 -0
- package/dist/svelte/index.js +19 -0
- package/dist/svelte/shims.d.ts +9 -0
- package/dist/svelte/theme-css.d.ts +2 -0
- package/dist/svelte/theme-css.d.ts.map +1 -0
- package/dist/svelte/theme-css.js +1097 -0
- package/package.json +56 -10
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Spring } from 'svelte/motion';
|
|
3
|
+
import Portal from '../Portal.svelte';
|
|
4
|
+
import { dropdownTransition } from './transitions';
|
|
5
|
+
|
|
6
|
+
type SelectOption = string | { value: string; label: string };
|
|
7
|
+
|
|
8
|
+
let { label, value, options, onChange } = $props<{
|
|
9
|
+
label: string;
|
|
10
|
+
value: string;
|
|
11
|
+
options: SelectOption[];
|
|
12
|
+
onChange: (value: string) => void;
|
|
13
|
+
}>();
|
|
14
|
+
|
|
15
|
+
let isOpen = $state(false);
|
|
16
|
+
let pos = $state<{ top: number; left: number; width: number; above: boolean } | null>(null);
|
|
17
|
+
let portalTarget = $state<HTMLElement | null>(null);
|
|
18
|
+
let triggerRef: HTMLButtonElement | undefined;
|
|
19
|
+
let dropdownRef: HTMLDivElement | undefined;
|
|
20
|
+
|
|
21
|
+
const chevronRotation = new Spring(0, { stiffness: 0.2, damping: 0.6 });
|
|
22
|
+
|
|
23
|
+
const toTitleCase = (s: string) => s.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
24
|
+
|
|
25
|
+
const normalized = $derived(
|
|
26
|
+
options.map((opt: SelectOption) =>
|
|
27
|
+
typeof opt === 'string' ? { value: opt, label: toTitleCase(opt) } : opt
|
|
28
|
+
)
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const selectedOption = $derived(normalized.find((o: { value: string; label: string }) => o.value === value));
|
|
32
|
+
|
|
33
|
+
const updatePos = () => {
|
|
34
|
+
if (!triggerRef || typeof window === 'undefined') return;
|
|
35
|
+
const rect = triggerRef.getBoundingClientRect();
|
|
36
|
+
const dropdownHeight = 8 + normalized.length * 36;
|
|
37
|
+
const spaceBelow = window.innerHeight - rect.bottom - 4;
|
|
38
|
+
const above = spaceBelow < dropdownHeight && rect.top > spaceBelow;
|
|
39
|
+
|
|
40
|
+
pos = {
|
|
41
|
+
top: above ? rect.top - 4 : rect.bottom + 4,
|
|
42
|
+
left: rect.left,
|
|
43
|
+
width: rect.width,
|
|
44
|
+
above,
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const openDropdown = () => {
|
|
49
|
+
updatePos();
|
|
50
|
+
isOpen = true;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const closeDropdown = () => {
|
|
54
|
+
isOpen = false;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const dropdownStyle = $derived.by(() => {
|
|
58
|
+
if (!pos || typeof window === 'undefined') return '';
|
|
59
|
+
|
|
60
|
+
if (pos.above) {
|
|
61
|
+
return `position:fixed;left:${pos.left}px;width:${pos.width}px;bottom:${window.innerHeight - pos.top}px;transform-origin:bottom;`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return `position:fixed;left:${pos.left}px;width:${pos.width}px;top:${pos.top}px;transform-origin:top;`;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
$effect(() => {
|
|
68
|
+
if (typeof document === 'undefined' || !triggerRef) return;
|
|
69
|
+
|
|
70
|
+
portalTarget = (triggerRef.closest('.dialkit-root') as HTMLElement | null) ?? document.body;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
$effect(() => {
|
|
74
|
+
chevronRotation.set(isOpen ? 180 : 0);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
$effect(() => {
|
|
78
|
+
if (!isOpen || typeof window === 'undefined') return;
|
|
79
|
+
|
|
80
|
+
const handleViewportChange = () => updatePos();
|
|
81
|
+
|
|
82
|
+
const handleClick = (e: MouseEvent) => {
|
|
83
|
+
const target = e.target as Node;
|
|
84
|
+
if (triggerRef?.contains(target) || dropdownRef?.contains(target)) return;
|
|
85
|
+
closeDropdown();
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
updatePos();
|
|
89
|
+
document.addEventListener('mousedown', handleClick);
|
|
90
|
+
window.addEventListener('resize', handleViewportChange);
|
|
91
|
+
window.addEventListener('scroll', handleViewportChange, true);
|
|
92
|
+
|
|
93
|
+
return () => {
|
|
94
|
+
document.removeEventListener('mousedown', handleClick);
|
|
95
|
+
window.removeEventListener('resize', handleViewportChange);
|
|
96
|
+
window.removeEventListener('scroll', handleViewportChange, true);
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
</script>
|
|
100
|
+
|
|
101
|
+
<div class="dialkit-select-row">
|
|
102
|
+
<button
|
|
103
|
+
bind:this={triggerRef}
|
|
104
|
+
class="dialkit-select-trigger"
|
|
105
|
+
onclick={() => (isOpen ? closeDropdown() : openDropdown())}
|
|
106
|
+
data-open={String(isOpen)}
|
|
107
|
+
>
|
|
108
|
+
<span class="dialkit-select-label">{label}</span>
|
|
109
|
+
<div class="dialkit-select-right">
|
|
110
|
+
<span class="dialkit-select-value">{selectedOption?.label ?? value}</span>
|
|
111
|
+
<svg
|
|
112
|
+
class="dialkit-select-chevron"
|
|
113
|
+
viewBox="0 0 24 24"
|
|
114
|
+
fill="none"
|
|
115
|
+
stroke="currentColor"
|
|
116
|
+
stroke-width="2.5"
|
|
117
|
+
stroke-linecap="round"
|
|
118
|
+
stroke-linejoin="round"
|
|
119
|
+
style:transform={`rotate(${chevronRotation.current}deg)`}
|
|
120
|
+
>
|
|
121
|
+
<path d="M6 9.5L12 15.5L18 9.5" />
|
|
122
|
+
</svg>
|
|
123
|
+
</div>
|
|
124
|
+
</button>
|
|
125
|
+
|
|
126
|
+
{#if portalTarget}
|
|
127
|
+
<Portal target={portalTarget}>
|
|
128
|
+
{#if isOpen && pos}
|
|
129
|
+
<div
|
|
130
|
+
bind:this={dropdownRef}
|
|
131
|
+
class="dialkit-select-dropdown"
|
|
132
|
+
style={dropdownStyle}
|
|
133
|
+
transition:dropdownTransition={{ above: pos.above }}
|
|
134
|
+
>
|
|
135
|
+
{#each normalized as option (option.value)}
|
|
136
|
+
<button
|
|
137
|
+
class="dialkit-select-option"
|
|
138
|
+
data-selected={String(option.value === value)}
|
|
139
|
+
onclick={() => {
|
|
140
|
+
onChange(option.value);
|
|
141
|
+
closeDropdown();
|
|
142
|
+
}}
|
|
143
|
+
>
|
|
144
|
+
{option.label}
|
|
145
|
+
</button>
|
|
146
|
+
{/each}
|
|
147
|
+
</div>
|
|
148
|
+
{/if}
|
|
149
|
+
</Portal>
|
|
150
|
+
{/if}
|
|
151
|
+
</div>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
type SelectOption = string | {
|
|
2
|
+
value: string;
|
|
3
|
+
label: string;
|
|
4
|
+
};
|
|
5
|
+
type $$ComponentProps = {
|
|
6
|
+
label: string;
|
|
7
|
+
value: string;
|
|
8
|
+
options: SelectOption[];
|
|
9
|
+
onChange: (value: string) => void;
|
|
10
|
+
};
|
|
11
|
+
declare const SelectControl: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
12
|
+
type SelectControl = ReturnType<typeof SelectControl>;
|
|
13
|
+
export default SelectControl;
|
|
14
|
+
//# sourceMappingURL=SelectControl.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SelectControl.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/SelectControl.svelte.ts"],"names":[],"mappings":"AAQE,KAAK,YAAY,GAAG,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAC/D,KAAK,gBAAgB,GAAG;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC,CAAC;AA8HJ,QAAA,MAAM,aAAa,sDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { tick } from 'svelte';
|
|
3
|
+
import { Spring } from 'svelte/motion';
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
label,
|
|
7
|
+
value,
|
|
8
|
+
onChange,
|
|
9
|
+
min = 0,
|
|
10
|
+
max = 1,
|
|
11
|
+
step = 0.01,
|
|
12
|
+
} = $props<{
|
|
13
|
+
label: string;
|
|
14
|
+
value: number;
|
|
15
|
+
onChange: (value: number) => void;
|
|
16
|
+
min?: number;
|
|
17
|
+
max?: number;
|
|
18
|
+
step?: number;
|
|
19
|
+
unit?: string;
|
|
20
|
+
}>();
|
|
21
|
+
|
|
22
|
+
const CLICK_THRESHOLD = 3;
|
|
23
|
+
const DEAD_ZONE = 32;
|
|
24
|
+
const MAX_CURSOR_RANGE = 200;
|
|
25
|
+
const MAX_STRETCH = 8;
|
|
26
|
+
const HANDLE_BUFFER = 8;
|
|
27
|
+
const LABEL_CSS_LEFT = 10;
|
|
28
|
+
const VALUE_CSS_RIGHT = 10;
|
|
29
|
+
|
|
30
|
+
let wrapperRef: HTMLDivElement | undefined;
|
|
31
|
+
let labelRef: HTMLSpanElement | undefined;
|
|
32
|
+
let valueSpanRef: HTMLSpanElement | undefined;
|
|
33
|
+
let inputRef: HTMLInputElement | undefined;
|
|
34
|
+
|
|
35
|
+
let isInteracting = $state(false);
|
|
36
|
+
let isDragging = $state(false);
|
|
37
|
+
let isHovered = $state(false);
|
|
38
|
+
let isValueHovered = $state(false);
|
|
39
|
+
let isValueEditable = $state(false);
|
|
40
|
+
let showInput = $state(false);
|
|
41
|
+
let inputValue = $state('');
|
|
42
|
+
|
|
43
|
+
const fillPercent = new Spring(((value - min) / (max - min)) * 100, { stiffness: 0.25, damping: 0.7 });
|
|
44
|
+
const rubberStretchPx = new Spring(0, { stiffness: 0.2, damping: 0.65 });
|
|
45
|
+
const handleOpacityMv = new Spring(0, { stiffness: 0.3, damping: 0.75 });
|
|
46
|
+
const handleScaleXMv = new Spring(0.25, { stiffness: 0.2, damping: 0.6 });
|
|
47
|
+
const handleScaleYMv = new Spring(1, { stiffness: 0.2, damping: 0.6 });
|
|
48
|
+
|
|
49
|
+
let pointerDownPos: { x: number; y: number } | null = null;
|
|
50
|
+
let isClickFlag = true;
|
|
51
|
+
let wrapperRect: DOMRect | null = null;
|
|
52
|
+
let scaleVal = 1;
|
|
53
|
+
let hoverTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
54
|
+
|
|
55
|
+
const decimalsForStep = (s: number) => {
|
|
56
|
+
const text = s.toString();
|
|
57
|
+
const dot = text.indexOf('.');
|
|
58
|
+
return dot === -1 ? 0 : text.length - dot - 1;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const roundValue = (val: number, s: number) => {
|
|
62
|
+
const raw = Math.round(val / s) * s;
|
|
63
|
+
return Number.parseFloat(raw.toFixed(decimalsForStep(s)));
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const snapToDecile = (rawValue: number, minValue: number, maxValue: number) => {
|
|
67
|
+
const normalized = (rawValue - minValue) / (maxValue - minValue);
|
|
68
|
+
const nearest = Math.round(normalized * 10) / 10;
|
|
69
|
+
|
|
70
|
+
if (Math.abs(normalized - nearest) <= 0.03125) {
|
|
71
|
+
return minValue + nearest * (maxValue - minValue);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return rawValue;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const percentFromValue = (nextValue: number) => ((nextValue - min) / (max - min)) * 100;
|
|
78
|
+
|
|
79
|
+
const positionToValue = (clientX: number) => {
|
|
80
|
+
if (!wrapperRect || !wrapperRef) return value;
|
|
81
|
+
const screenX = clientX - wrapperRect.left;
|
|
82
|
+
const sceneX = screenX / scaleVal;
|
|
83
|
+
const nativeWidth = wrapperRef.offsetWidth || wrapperRect.width;
|
|
84
|
+
const percent = Math.max(0, Math.min(1, sceneX / nativeWidth));
|
|
85
|
+
const rawValue = min + percent * (max - min);
|
|
86
|
+
return Math.max(min, Math.min(max, rawValue));
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const computeRubberStretch = (clientX: number, sign: number) => {
|
|
90
|
+
if (!wrapperRect) return 0;
|
|
91
|
+
const distancePast = sign < 0 ? wrapperRect.left - clientX : clientX - wrapperRect.right;
|
|
92
|
+
const overflow = Math.max(0, distancePast - DEAD_ZONE);
|
|
93
|
+
return sign * MAX_STRETCH * Math.sqrt(Math.min(overflow / MAX_CURSOR_RANGE, 1));
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
$effect(() => {
|
|
97
|
+
value;
|
|
98
|
+
if (!isInteracting) {
|
|
99
|
+
fillPercent.set(((value - min) / (max - min)) * 100, { instant: true });
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
$effect(() => {
|
|
104
|
+
if (hoverTimeout) {
|
|
105
|
+
clearTimeout(hoverTimeout);
|
|
106
|
+
hoverTimeout = null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (isValueHovered && !showInput && !isValueEditable) {
|
|
110
|
+
hoverTimeout = setTimeout(() => {
|
|
111
|
+
isValueEditable = true;
|
|
112
|
+
}, 800);
|
|
113
|
+
} else if (!isValueHovered && !showInput) {
|
|
114
|
+
isValueEditable = false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return () => {
|
|
118
|
+
if (hoverTimeout) {
|
|
119
|
+
clearTimeout(hoverTimeout);
|
|
120
|
+
hoverTimeout = null;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
$effect(() => {
|
|
126
|
+
if (showInput) {
|
|
127
|
+
tick().then(() => {
|
|
128
|
+
inputRef?.focus();
|
|
129
|
+
inputRef?.select();
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const percentage = $derived(((value - min) / (max - min)) * 100);
|
|
135
|
+
const isActive = $derived(isInteracting || isHovered);
|
|
136
|
+
|
|
137
|
+
const leftThreshold = $derived.by(() => {
|
|
138
|
+
const trackWidth = wrapperRef?.offsetWidth;
|
|
139
|
+
if (trackWidth && labelRef) {
|
|
140
|
+
return ((LABEL_CSS_LEFT + labelRef.offsetWidth + HANDLE_BUFFER) / trackWidth) * 100;
|
|
141
|
+
}
|
|
142
|
+
return 30;
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const rightThreshold = $derived.by(() => {
|
|
146
|
+
const trackWidth = wrapperRef?.offsetWidth;
|
|
147
|
+
if (trackWidth && valueSpanRef) {
|
|
148
|
+
return ((trackWidth - VALUE_CSS_RIGHT - valueSpanRef.offsetWidth - HANDLE_BUFFER) / trackWidth) * 100;
|
|
149
|
+
}
|
|
150
|
+
return 78;
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const valueDodge = $derived(percentage < leftThreshold || percentage > rightThreshold);
|
|
154
|
+
|
|
155
|
+
const handleOpacity = $derived.by(() => {
|
|
156
|
+
if (!isActive) return 0;
|
|
157
|
+
if (valueDodge) return 0.1;
|
|
158
|
+
if (isDragging) return 0.9;
|
|
159
|
+
return 0.5;
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
$effect(() => {
|
|
163
|
+
handleOpacityMv.set(handleOpacity);
|
|
164
|
+
handleScaleXMv.set(isActive ? 1 : 0.25);
|
|
165
|
+
handleScaleYMv.set(isActive && valueDodge ? 0.75 : 1);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const fillBackground = $derived(
|
|
169
|
+
isActive ? 'rgba(255, 255, 255, 0.15)' : 'rgba(255, 255, 255, 0.11)'
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
const discreteSteps = $derived((max - min) / step);
|
|
173
|
+
|
|
174
|
+
const hashMarks = $derived.by(() => {
|
|
175
|
+
if (discreteSteps <= 10) {
|
|
176
|
+
return Array.from({ length: Math.max(discreteSteps - 1, 0) }, (_, i) => ({
|
|
177
|
+
key: `d-${i + 1}`,
|
|
178
|
+
left: (((i + 1) * step) / (max - min)) * 100,
|
|
179
|
+
}));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return Array.from({ length: 9 }, (_, i) => ({
|
|
183
|
+
key: `t-${i + 1}`,
|
|
184
|
+
left: (i + 1) * 10,
|
|
185
|
+
}));
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const handlePointerDown = (e: PointerEvent) => {
|
|
189
|
+
if (showInput) return;
|
|
190
|
+
e.preventDefault();
|
|
191
|
+
|
|
192
|
+
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
|
193
|
+
pointerDownPos = { x: e.clientX, y: e.clientY };
|
|
194
|
+
isClickFlag = true;
|
|
195
|
+
isInteracting = true;
|
|
196
|
+
|
|
197
|
+
if (wrapperRef) {
|
|
198
|
+
wrapperRect = wrapperRef.getBoundingClientRect();
|
|
199
|
+
scaleVal = wrapperRect.width / wrapperRef.offsetWidth;
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const handlePointerMove = (e: PointerEvent) => {
|
|
204
|
+
if (!isInteracting || !pointerDownPos) return;
|
|
205
|
+
|
|
206
|
+
const dx = e.clientX - pointerDownPos.x;
|
|
207
|
+
const dy = e.clientY - pointerDownPos.y;
|
|
208
|
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
209
|
+
|
|
210
|
+
if (isClickFlag && distance > CLICK_THRESHOLD) {
|
|
211
|
+
isClickFlag = false;
|
|
212
|
+
isDragging = true;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (!isClickFlag) {
|
|
216
|
+
if (wrapperRect) {
|
|
217
|
+
if (e.clientX < wrapperRect.left) {
|
|
218
|
+
rubberStretchPx.set(computeRubberStretch(e.clientX, -1), { instant: true });
|
|
219
|
+
} else if (e.clientX > wrapperRect.right) {
|
|
220
|
+
rubberStretchPx.set(computeRubberStretch(e.clientX, 1), { instant: true });
|
|
221
|
+
} else {
|
|
222
|
+
rubberStretchPx.set(0, { instant: true });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const newValue = positionToValue(e.clientX);
|
|
227
|
+
fillPercent.set(percentFromValue(newValue), { instant: true });
|
|
228
|
+
onChange(roundValue(newValue, step));
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const handlePointerUp = (e: PointerEvent) => {
|
|
233
|
+
if (!isInteracting) return;
|
|
234
|
+
|
|
235
|
+
if (isClickFlag) {
|
|
236
|
+
const rawValue = positionToValue(e.clientX);
|
|
237
|
+
const steps = (max - min) / step;
|
|
238
|
+
const snappedValue = steps <= 10
|
|
239
|
+
? Math.max(min, Math.min(max, min + Math.round((rawValue - min) / step) * step))
|
|
240
|
+
: snapToDecile(rawValue, min, max);
|
|
241
|
+
|
|
242
|
+
fillPercent.set(percentFromValue(snappedValue));
|
|
243
|
+
onChange(roundValue(snappedValue, step));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (rubberStretchPx.current !== 0) {
|
|
247
|
+
rubberStretchPx.set(0);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
isInteracting = false;
|
|
251
|
+
isDragging = false;
|
|
252
|
+
pointerDownPos = null;
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const handlePointerCancel = () => {
|
|
256
|
+
if (!isInteracting) return;
|
|
257
|
+
isInteracting = false;
|
|
258
|
+
isDragging = false;
|
|
259
|
+
rubberStretchPx.set(0, { instant: true });
|
|
260
|
+
pointerDownPos = null;
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const handleInputSubmit = () => {
|
|
264
|
+
const parsed = Number.parseFloat(inputValue);
|
|
265
|
+
if (!Number.isNaN(parsed)) {
|
|
266
|
+
const clamped = Math.max(min, Math.min(max, parsed));
|
|
267
|
+
onChange(roundValue(clamped, step));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
showInput = false;
|
|
271
|
+
isValueHovered = false;
|
|
272
|
+
isValueEditable = false;
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const handleValueClick = (e: MouseEvent) => {
|
|
276
|
+
if (!isValueEditable) return;
|
|
277
|
+
e.stopPropagation();
|
|
278
|
+
e.preventDefault();
|
|
279
|
+
showInput = true;
|
|
280
|
+
inputValue = value.toFixed(decimalsForStep(step));
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const displayValue = $derived(value.toFixed(decimalsForStep(step)));
|
|
284
|
+
|
|
285
|
+
const trackStyle = $derived(`width:calc(100% + ${Math.abs(rubberStretchPx.current)}px);transform:translateX(${rubberStretchPx.current < 0 ? rubberStretchPx.current : 0}px);`);
|
|
286
|
+
const fillStyle = $derived(`background:${fillBackground};width:${fillPercent.current}%;transition:background 0.15s;`);
|
|
287
|
+
const handleStyle = $derived(`left:max(5px, calc(${fillPercent.current}% - 9px));opacity:${handleOpacityMv.current};transform:translateY(-50%) scaleX(${handleScaleXMv.current}) scaleY(${handleScaleYMv.current});background:rgba(255, 255, 255, 0.9);`);
|
|
288
|
+
</script>
|
|
289
|
+
|
|
290
|
+
<div bind:this={wrapperRef} class="dialkit-slider-wrapper">
|
|
291
|
+
<div
|
|
292
|
+
class={`dialkit-slider ${isActive ? 'dialkit-slider-active' : ''}`}
|
|
293
|
+
style={trackStyle}
|
|
294
|
+
onpointerdown={handlePointerDown}
|
|
295
|
+
onpointermove={handlePointerMove}
|
|
296
|
+
onpointerup={handlePointerUp}
|
|
297
|
+
onpointercancel={handlePointerCancel}
|
|
298
|
+
onmouseenter={() => (isHovered = true)}
|
|
299
|
+
onmouseleave={() => (isHovered = false)}
|
|
300
|
+
>
|
|
301
|
+
<div class="dialkit-slider-hashmarks">
|
|
302
|
+
{#each hashMarks as mark (mark.key)}
|
|
303
|
+
<div class="dialkit-slider-hashmark" style:left={`${mark.left}%`} />
|
|
304
|
+
{/each}
|
|
305
|
+
</div>
|
|
306
|
+
|
|
307
|
+
<div class="dialkit-slider-fill" style={fillStyle} />
|
|
308
|
+
|
|
309
|
+
<div class="dialkit-slider-handle" style={handleStyle} />
|
|
310
|
+
|
|
311
|
+
<span bind:this={labelRef} class="dialkit-slider-label">{label}</span>
|
|
312
|
+
|
|
313
|
+
{#if showInput}
|
|
314
|
+
<input
|
|
315
|
+
bind:this={inputRef}
|
|
316
|
+
type="text"
|
|
317
|
+
class="dialkit-slider-input"
|
|
318
|
+
value={inputValue}
|
|
319
|
+
oninput={(e) => (inputValue = (e.currentTarget as HTMLInputElement).value)}
|
|
320
|
+
onkeydown={(e) => {
|
|
321
|
+
if (e.key === 'Enter') handleInputSubmit();
|
|
322
|
+
else if (e.key === 'Escape') {
|
|
323
|
+
showInput = false;
|
|
324
|
+
isValueHovered = false;
|
|
325
|
+
}
|
|
326
|
+
}}
|
|
327
|
+
onblur={handleInputSubmit}
|
|
328
|
+
onclick={(e) => e.stopPropagation()}
|
|
329
|
+
onmousedown={(e) => e.stopPropagation()}
|
|
330
|
+
/>
|
|
331
|
+
{:else}
|
|
332
|
+
<span
|
|
333
|
+
bind:this={valueSpanRef}
|
|
334
|
+
class={`dialkit-slider-value ${isValueEditable ? 'dialkit-slider-value-editable' : ''}`}
|
|
335
|
+
onmouseenter={() => (isValueHovered = true)}
|
|
336
|
+
onmouseleave={() => (isValueHovered = false)}
|
|
337
|
+
onclick={handleValueClick}
|
|
338
|
+
onmousedown={(e) => isValueEditable && e.stopPropagation()}
|
|
339
|
+
style:cursor={isValueEditable ? 'text' : 'default'}
|
|
340
|
+
>
|
|
341
|
+
{displayValue}
|
|
342
|
+
</span>
|
|
343
|
+
{/if}
|
|
344
|
+
</div>
|
|
345
|
+
</div>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
label: string;
|
|
3
|
+
value: number;
|
|
4
|
+
onChange: (value: number) => void;
|
|
5
|
+
min?: number;
|
|
6
|
+
max?: number;
|
|
7
|
+
step?: number;
|
|
8
|
+
unit?: string;
|
|
9
|
+
};
|
|
10
|
+
declare const Slider: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
11
|
+
type Slider = ReturnType<typeof Slider>;
|
|
12
|
+
export default Slider;
|
|
13
|
+
//# sourceMappingURL=Slider.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Slider.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/Slider.svelte.ts"],"names":[],"mappings":"AAMC,KAAK,gBAAgB,GAAG;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AA0TJ,QAAA,MAAM,MAAM,sDAAwC,CAAC;AACrD,KAAK,MAAM,GAAG,UAAU,CAAC,OAAO,MAAM,CAAC,CAAC;AACxC,eAAe,MAAM,CAAC"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { DialStore } from 'dialkit/store';
|
|
3
|
+
import type { SpringConfig } from 'dialkit/store';
|
|
4
|
+
import Folder from './Folder.svelte';
|
|
5
|
+
import Slider from './Slider.svelte';
|
|
6
|
+
import SegmentedControl from './SegmentedControl.svelte';
|
|
7
|
+
import SpringVisualization from './SpringVisualization.svelte';
|
|
8
|
+
|
|
9
|
+
let { panelId, path, label, spring, onChange } = $props<{
|
|
10
|
+
panelId: string;
|
|
11
|
+
path: string;
|
|
12
|
+
label: string;
|
|
13
|
+
spring: SpringConfig;
|
|
14
|
+
onChange: (spring: SpringConfig) => void;
|
|
15
|
+
}>();
|
|
16
|
+
|
|
17
|
+
let mode = $state<'simple' | 'advanced'>(DialStore.getSpringMode(panelId, path));
|
|
18
|
+
|
|
19
|
+
$effect(() => {
|
|
20
|
+
const unsub = DialStore.subscribe(panelId, () => {
|
|
21
|
+
mode = DialStore.getSpringMode(panelId, path);
|
|
22
|
+
});
|
|
23
|
+
return unsub;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const isSimpleMode = $derived(mode === 'simple');
|
|
27
|
+
|
|
28
|
+
const handleModeChange = (newMode: string) => {
|
|
29
|
+
const typedMode = newMode as 'simple' | 'advanced';
|
|
30
|
+
DialStore.updateSpringMode(panelId, path, typedMode);
|
|
31
|
+
|
|
32
|
+
if (typedMode === 'simple') {
|
|
33
|
+
const { stiffness, damping, mass, ...rest } = spring;
|
|
34
|
+
onChange({
|
|
35
|
+
...rest,
|
|
36
|
+
type: 'spring',
|
|
37
|
+
visualDuration: spring.visualDuration ?? 0.3,
|
|
38
|
+
bounce: spring.bounce ?? 0.2,
|
|
39
|
+
});
|
|
40
|
+
} else {
|
|
41
|
+
const { visualDuration, bounce, ...rest } = spring;
|
|
42
|
+
onChange({
|
|
43
|
+
...rest,
|
|
44
|
+
type: 'spring',
|
|
45
|
+
stiffness: spring.stiffness ?? 200,
|
|
46
|
+
damping: spring.damping ?? 25,
|
|
47
|
+
mass: spring.mass ?? 1,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const handleUpdate = (key: keyof SpringConfig, value: number) => {
|
|
53
|
+
if (isSimpleMode) {
|
|
54
|
+
const { stiffness, damping, mass, ...rest } = spring;
|
|
55
|
+
onChange({ ...rest, [key]: value });
|
|
56
|
+
} else {
|
|
57
|
+
const { visualDuration, bounce, ...rest } = spring;
|
|
58
|
+
onChange({ ...rest, [key]: value });
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
</script>
|
|
62
|
+
|
|
63
|
+
<Folder title={label} defaultOpen={true}>
|
|
64
|
+
<div style="display: flex; flex-direction: column; gap: 6px;">
|
|
65
|
+
<SpringVisualization {spring} isSimpleMode={isSimpleMode} />
|
|
66
|
+
|
|
67
|
+
<div class="dialkit-labeled-control">
|
|
68
|
+
<span class="dialkit-labeled-control-label">Type</span>
|
|
69
|
+
<SegmentedControl
|
|
70
|
+
options={[
|
|
71
|
+
{ value: 'simple', label: 'Time' },
|
|
72
|
+
{ value: 'advanced', label: 'Physics' },
|
|
73
|
+
]}
|
|
74
|
+
value={mode}
|
|
75
|
+
onChange={handleModeChange}
|
|
76
|
+
/>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
{#if isSimpleMode}
|
|
80
|
+
<Slider
|
|
81
|
+
label="Duration"
|
|
82
|
+
value={spring.visualDuration ?? 0.3}
|
|
83
|
+
onChange={(v) => handleUpdate('visualDuration', v)}
|
|
84
|
+
min={0.1}
|
|
85
|
+
max={1}
|
|
86
|
+
step={0.05}
|
|
87
|
+
unit="s"
|
|
88
|
+
/>
|
|
89
|
+
<Slider
|
|
90
|
+
label="Bounce"
|
|
91
|
+
value={spring.bounce ?? 0.2}
|
|
92
|
+
onChange={(v) => handleUpdate('bounce', v)}
|
|
93
|
+
min={0}
|
|
94
|
+
max={1}
|
|
95
|
+
step={0.05}
|
|
96
|
+
/>
|
|
97
|
+
{:else}
|
|
98
|
+
<Slider
|
|
99
|
+
label="Stiffness"
|
|
100
|
+
value={spring.stiffness ?? 400}
|
|
101
|
+
onChange={(v) => handleUpdate('stiffness', v)}
|
|
102
|
+
min={1}
|
|
103
|
+
max={1000}
|
|
104
|
+
step={10}
|
|
105
|
+
/>
|
|
106
|
+
<Slider
|
|
107
|
+
label="Damping"
|
|
108
|
+
value={spring.damping ?? 17}
|
|
109
|
+
onChange={(v) => handleUpdate('damping', v)}
|
|
110
|
+
min={1}
|
|
111
|
+
max={100}
|
|
112
|
+
step={1}
|
|
113
|
+
/>
|
|
114
|
+
<Slider
|
|
115
|
+
label="Mass"
|
|
116
|
+
value={spring.mass ?? 1}
|
|
117
|
+
onChange={(v) => handleUpdate('mass', v)}
|
|
118
|
+
min={0.1}
|
|
119
|
+
max={10}
|
|
120
|
+
step={0.1}
|
|
121
|
+
/>
|
|
122
|
+
{/if}
|
|
123
|
+
</div>
|
|
124
|
+
</Folder>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { SpringConfig } from 'dialkit/store';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
panelId: string;
|
|
4
|
+
path: string;
|
|
5
|
+
label: string;
|
|
6
|
+
spring: SpringConfig;
|
|
7
|
+
onChange: (spring: SpringConfig) => void;
|
|
8
|
+
};
|
|
9
|
+
declare const SpringControl: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
10
|
+
type SpringControl = ReturnType<typeof SpringControl>;
|
|
11
|
+
export default SpringControl;
|
|
12
|
+
//# sourceMappingURL=SpringControl.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SpringControl.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/SpringControl.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAMjD,KAAK,gBAAgB,GAAG;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,YAAY,CAAC;IACrB,QAAQ,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,IAAI,CAAC;CAC1C,CAAC;AAkFJ,QAAA,MAAM,aAAa,sDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|