twintrinsic 0.0.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.
- package/LICENSE +674 -0
- package/README.md +150 -0
- package/dist/App/App.svelte +54 -0
- package/dist/App/App.svelte.d.ts +65 -0
- package/dist/Section.svelte +25 -0
- package/dist/Section.svelte.d.ts +34 -0
- package/dist/actions/clickOutside.d.ts +9 -0
- package/dist/actions/clickOutside.js +19 -0
- package/dist/actions/index.d.ts +1 -0
- package/dist/actions/index.js +1 -0
- package/dist/components/Accordion/Accordion.svelte +75 -0
- package/dist/components/Accordion/Accordion.svelte.d.ts +39 -0
- package/dist/components/Accordion/AccordionItem.svelte +150 -0
- package/dist/components/Accordion/AccordionItem.svelte.d.ts +30 -0
- package/dist/components/App/App.story.md +8 -0
- package/dist/components/App/App.story.svelte +170 -0
- package/dist/components/App/App.story.svelte.d.ts +22 -0
- package/dist/components/App/App.svelte +77 -0
- package/dist/components/App/App.svelte.d.ts +66 -0
- package/dist/components/App/Split.svelte +346 -0
- package/dist/components/App/Split.svelte.d.ts +54 -0
- package/dist/components/App/index.d.ts +2 -0
- package/dist/components/App/index.js +3 -0
- package/dist/components/AppHeader/AppHeader.svelte +439 -0
- package/dist/components/AppHeader/AppHeader.svelte.d.ts +24 -0
- package/dist/components/Avatar/Avatar.svelte +300 -0
- package/dist/components/Avatar/Avatar.svelte.d.ts +48 -0
- package/dist/components/Avatar/AvatarGroup.svelte +185 -0
- package/dist/components/Avatar/AvatarGroup.svelte.d.ts +46 -0
- package/dist/components/Badge/Badge.svelte +186 -0
- package/dist/components/Badge/Badge.svelte.d.ts +51 -0
- package/dist/components/BottomBar/BottomBar.svelte +146 -0
- package/dist/components/BottomBar/BottomBar.svelte.d.ts +38 -0
- package/dist/components/Breadcrumb/Breadcrumb.svelte +77 -0
- package/dist/components/Breadcrumb/Breadcrumb.svelte.d.ts +42 -0
- package/dist/components/Breadcrumb/BreadcrumbItem.svelte +171 -0
- package/dist/components/Breadcrumb/BreadcrumbItem.svelte.d.ts +38 -0
- package/dist/components/Button/Button.svelte +252 -0
- package/dist/components/Button/Button.svelte.d.ts +80 -0
- package/dist/components/Button/ButtonGroup.svelte +127 -0
- package/dist/components/Button/ButtonGroup.svelte.d.ts +44 -0
- package/dist/components/Card/Card.svelte +152 -0
- package/dist/components/Card/Card.svelte.d.ts +55 -0
- package/dist/components/Carousel/Carousel.svelte +461 -0
- package/dist/components/Carousel/Carousel.svelte.d.ts +79 -0
- package/dist/components/Carousel/CarouselItem.svelte +149 -0
- package/dist/components/Carousel/CarouselItem.svelte.d.ts +35 -0
- package/dist/components/Chip/Chip.svelte +288 -0
- package/dist/components/Chip/Chip.svelte.d.ts +71 -0
- package/dist/components/Chip/ChipGroup.svelte +190 -0
- package/dist/components/Chip/ChipGroup.svelte.d.ts +71 -0
- package/dist/components/CodeBlock/CodeBlock.svelte +356 -0
- package/dist/components/CodeBlock/CodeBlock.svelte.d.ts +44 -0
- package/dist/components/CodeBlock/index.d.ts +1 -0
- package/dist/components/CodeBlock/index.js +1 -0
- package/dist/components/CodeBlockSpeed/CodeBlockSpeed.svelte +145 -0
- package/dist/components/CodeBlockSpeed/CodeBlockSpeed.svelte.d.ts +44 -0
- package/dist/components/CodeEditor/CodeEditor.svelte +229 -0
- package/dist/components/CodeEditor/CodeEditor.svelte.d.ts +23 -0
- package/dist/components/Combobox/Combobox.svelte +279 -0
- package/dist/components/Combobox/Combobox.svelte.d.ts +34 -0
- package/dist/components/Container/Container.svelte +45 -0
- package/dist/components/Container/Container.svelte.d.ts +36 -0
- package/dist/components/DataTable/DataTable.svelte +879 -0
- package/dist/components/DataTable/DataTable.svelte.d.ts +102 -0
- package/dist/components/Form/AutoComplete.svelte +357 -0
- package/dist/components/Form/AutoComplete.svelte.d.ts +73 -0
- package/dist/components/Form/Calendar.svelte +429 -0
- package/dist/components/Form/Calendar.svelte.d.ts +53 -0
- package/dist/components/Form/Checkbox.svelte +196 -0
- package/dist/components/Form/Checkbox.svelte.d.ts +50 -0
- package/dist/components/Form/ColorPicker.svelte +396 -0
- package/dist/components/Form/ColorPicker.svelte.d.ts +43 -0
- package/dist/components/Form/Combobox.svelte +645 -0
- package/dist/components/Form/Combobox.svelte.d.ts +93 -0
- package/dist/components/Form/Dropdown.svelte +773 -0
- package/dist/components/Form/Dropdown.svelte.d.ts +81 -0
- package/dist/components/Form/FileUpload.svelte +796 -0
- package/dist/components/Form/FileUpload.svelte.d.ts +78 -0
- package/dist/components/Form/FloatLabel.svelte +245 -0
- package/dist/components/Form/FloatLabel.svelte.d.ts +44 -0
- package/dist/components/Form/Form.svelte +281 -0
- package/dist/components/Form/Form.svelte.d.ts +54 -0
- package/dist/components/Form/FormField.svelte +218 -0
- package/dist/components/Form/FormField.svelte.d.ts +47 -0
- package/dist/components/Form/Input.svelte +340 -0
- package/dist/components/Form/Input.svelte.d.ts +79 -0
- package/dist/components/Form/InputSwitch.svelte +189 -0
- package/dist/components/Form/InputSwitch.svelte.d.ts +46 -0
- package/dist/components/Form/InvalidState.svelte +97 -0
- package/dist/components/Form/InvalidState.svelte.d.ts +37 -0
- package/dist/components/Form/Knob.svelte +537 -0
- package/dist/components/Form/Knob.svelte.d.ts +78 -0
- package/dist/components/Form/ListInput.svelte +469 -0
- package/dist/components/Form/ListInput.svelte.d.ts +70 -0
- package/dist/components/Form/Listbox.svelte +513 -0
- package/dist/components/Form/Listbox.svelte.d.ts +74 -0
- package/dist/components/Form/NumberInput.svelte +452 -0
- package/dist/components/Form/NumberInput.svelte.d.ts +82 -0
- package/dist/components/Form/Radio.svelte +192 -0
- package/dist/components/Form/Radio.svelte.d.ts +53 -0
- package/dist/components/Form/RadioGroup.svelte +155 -0
- package/dist/components/Form/RadioGroup.svelte.d.ts +48 -0
- package/dist/components/Form/Rating.svelte +380 -0
- package/dist/components/Form/Rating.svelte.d.ts +64 -0
- package/dist/components/Form/Select.svelte +436 -0
- package/dist/components/Form/Select.svelte.d.ts +49 -0
- package/dist/components/Form/SelectGroup.svelte +34 -0
- package/dist/components/Form/SelectGroup.svelte.d.ts +33 -0
- package/dist/components/Form/Slider.svelte +622 -0
- package/dist/components/Form/Slider.svelte.d.ts +73 -0
- package/dist/components/Form/Switch.svelte +192 -0
- package/dist/components/Form/Switch.svelte.d.ts +46 -0
- package/dist/components/Form/TextInput.svelte +274 -0
- package/dist/components/Form/TextInput.svelte.d.ts +74 -0
- package/dist/components/Form/Textarea.svelte +207 -0
- package/dist/components/Form/Textarea.svelte.d.ts +62 -0
- package/dist/components/Icon/Icon.svelte +140 -0
- package/dist/components/Icon/Icon.svelte.d.ts +25 -0
- package/dist/components/Icon/index.d.ts +1 -0
- package/dist/components/Icon/index.js +1 -0
- package/dist/components/Lazy/Lazy.svelte +158 -0
- package/dist/components/Lazy/Lazy.svelte.d.ts +42 -0
- package/dist/components/Masonry/Masonry.svelte +299 -0
- package/dist/components/Masonry/Masonry.svelte.d.ts +55 -0
- package/dist/components/Menu/Menu/Menu.svelte +65 -0
- package/dist/components/Menu/Menu/Menu.svelte.d.ts +17 -0
- package/dist/components/Menu/Menu/MenuItem.svelte +90 -0
- package/dist/components/Menu/Menu/MenuItem.svelte.d.ts +27 -0
- package/dist/components/Modal/Modal.svelte +334 -0
- package/dist/components/Modal/Modal.svelte.d.ts +55 -0
- package/dist/components/Panel/Card.svelte +141 -0
- package/dist/components/Panel/Card.svelte.d.ts +52 -0
- package/dist/components/Panel/Hero/Hero.story.md +9 -0
- package/dist/components/Panel/Hero/Hero.story.svelte +49 -0
- package/dist/components/Panel/Hero/Hero.story.svelte.d.ts +21 -0
- package/dist/components/Panel/Hero/Hero.svelte +24 -0
- package/dist/components/Panel/Hero/Hero.svelte.d.ts +32 -0
- package/dist/components/Panel/LazyPanel.svelte +110 -0
- package/dist/components/Panel/LazyPanel.svelte.d.ts +46 -0
- package/dist/components/Panel/Panel.svelte +205 -0
- package/dist/components/Panel/Panel.svelte.d.ts +23 -0
- package/dist/components/Progress/Progress.svelte +220 -0
- package/dist/components/Progress/Progress.svelte.d.ts +61 -0
- package/dist/components/Separator/Separator.svelte +109 -0
- package/dist/components/Separator/Separator.svelte.d.ts +35 -0
- package/dist/components/Sidebar/Sidebar.svelte +213 -0
- package/dist/components/Sidebar/Sidebar.svelte.d.ts +60 -0
- package/dist/components/Skeleton/Skeleton.svelte +170 -0
- package/dist/components/Skeleton/Skeleton.svelte.d.ts +48 -0
- package/dist/components/Stepper/Stepper.svelte +111 -0
- package/dist/components/Stepper/Stepper.svelte.d.ts +54 -0
- package/dist/components/Stepper/StepperStep.svelte +369 -0
- package/dist/components/Stepper/StepperStep.svelte.d.ts +63 -0
- package/dist/components/Table/Table.svelte +167 -0
- package/dist/components/Table/Table.svelte.d.ts +56 -0
- package/dist/components/Table/TableBody.svelte +41 -0
- package/dist/components/Table/TableBody.svelte.d.ts +33 -0
- package/dist/components/Table/TableCell.svelte +76 -0
- package/dist/components/Table/TableCell.svelte.d.ts +36 -0
- package/dist/components/Table/TableHead.svelte +41 -0
- package/dist/components/Table/TableHead.svelte.d.ts +32 -0
- package/dist/components/Table/TableHeader.svelte +148 -0
- package/dist/components/Table/TableHeader.svelte.d.ts +42 -0
- package/dist/components/Table/TableRow.svelte +99 -0
- package/dist/components/Table/TableRow.svelte.d.ts +40 -0
- package/dist/components/Tabs/Tab.svelte +145 -0
- package/dist/components/Tabs/Tab.svelte.d.ts +36 -0
- package/dist/components/Tabs/TabList.svelte +60 -0
- package/dist/components/Tabs/TabList.svelte.d.ts +32 -0
- package/dist/components/Tabs/TabPanel.svelte +118 -0
- package/dist/components/Tabs/TabPanel.svelte.d.ts +38 -0
- package/dist/components/Tabs/Tabs.svelte +287 -0
- package/dist/components/Tabs/Tabs.svelte.d.ts +50 -0
- package/dist/components/Tag/Tag.svelte +260 -0
- package/dist/components/Tag/Tag.svelte.d.ts +54 -0
- package/dist/components/Tag/TagGroup.svelte +147 -0
- package/dist/components/Tag/TagGroup.svelte.d.ts +62 -0
- package/dist/components/ThemeToggle/ThemeToggle.svelte +93 -0
- package/dist/components/ThemeToggle/ThemeToggle.svelte.d.ts +12 -0
- package/dist/components/Timeline/Timeline.svelte +144 -0
- package/dist/components/Timeline/Timeline.svelte.d.ts +48 -0
- package/dist/components/Timeline/TimelineItem.svelte +391 -0
- package/dist/components/Timeline/TimelineItem.svelte.d.ts +63 -0
- package/dist/components/Toast/Toast.svelte +313 -0
- package/dist/components/Toast/Toast.svelte.d.ts +44 -0
- package/dist/components/Toast/toastStore.d.ts +40 -0
- package/dist/components/Toast/toastStore.js +293 -0
- package/dist/components/Tooltip/Tooltip.svelte +282 -0
- package/dist/components/Tooltip/Tooltip.svelte.d.ts +55 -0
- package/dist/components/Tree/Tree.svelte +129 -0
- package/dist/components/Tree/Tree.svelte.d.ts +61 -0
- package/dist/components/Tree/TreeNode.svelte +332 -0
- package/dist/components/Tree/TreeNode.svelte.d.ts +55 -0
- package/dist/components/icons/TwintrinsicLogo.svelte +73 -0
- package/dist/components/icons/TwintrinsicLogo.svelte.d.ts +17 -0
- package/dist/components/icons/twintrinsic-source.svg +73 -0
- package/dist/components/icons/twintrinsic.svg +38 -0
- package/dist/docs/EventsTable.svelte +86 -0
- package/dist/docs/EventsTable.svelte.d.ts +27 -0
- package/dist/docs/PropsTable.svelte +103 -0
- package/dist/docs/PropsTable.svelte.d.ts +28 -0
- package/dist/docs/index.d.ts +2 -0
- package/dist/docs/index.js +2 -0
- package/dist/helpers/detectLanguage.d.ts +6 -0
- package/dist/helpers/detectLanguage.js +60 -0
- package/dist/helpers/index.d.ts +1 -0
- package/dist/helpers/index.js +1 -0
- package/dist/index.d.ts +86 -0
- package/dist/index.js +94 -0
- package/dist/twintrinsic.css +347 -0
- package/package.json +98 -0
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
Knob - A circular progress slider component for selecting numeric values.
|
|
4
|
+
Provides an intuitive circular interface with drag interaction and keyboard controls.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
```svelte
|
|
8
|
+
<Knob
|
|
9
|
+
name="volume"
|
|
10
|
+
value={50}
|
|
11
|
+
min={0}
|
|
12
|
+
max={100}
|
|
13
|
+
/>
|
|
14
|
+
|
|
15
|
+
<Knob
|
|
16
|
+
name="progress"
|
|
17
|
+
value={75}
|
|
18
|
+
showValue
|
|
19
|
+
valueTemplate="{value}%"
|
|
20
|
+
size="lg"
|
|
21
|
+
/>
|
|
22
|
+
|
|
23
|
+
<FormField label="Temperature">
|
|
24
|
+
<Knob
|
|
25
|
+
name="temperature"
|
|
26
|
+
value={22}
|
|
27
|
+
min={15}
|
|
28
|
+
max={30}
|
|
29
|
+
step={0.5}
|
|
30
|
+
color="var(--color-primary-500)"
|
|
31
|
+
/>
|
|
32
|
+
</FormField>
|
|
33
|
+
```
|
|
34
|
+
-->
|
|
35
|
+
<script>
|
|
36
|
+
import { getContext, onMount } from "svelte"
|
|
37
|
+
|
|
38
|
+
const {
|
|
39
|
+
/** @type {string} - Additional CSS classes */
|
|
40
|
+
class: className = "",
|
|
41
|
+
|
|
42
|
+
/** @type {string} - HTML id for accessibility */
|
|
43
|
+
id = crypto.randomUUID(),
|
|
44
|
+
|
|
45
|
+
/** @type {string} - Input name */
|
|
46
|
+
name,
|
|
47
|
+
|
|
48
|
+
/** @type {number} - Current value */
|
|
49
|
+
value = 0,
|
|
50
|
+
|
|
51
|
+
/** @type {number} - Minimum value */
|
|
52
|
+
min = 0,
|
|
53
|
+
|
|
54
|
+
/** @type {number} - Maximum value */
|
|
55
|
+
max = 100,
|
|
56
|
+
|
|
57
|
+
/** @type {number} - Step increment */
|
|
58
|
+
step = 1,
|
|
59
|
+
|
|
60
|
+
/** @type {string} - Size of the knob (sm, md, lg, xl) */
|
|
61
|
+
size = "md",
|
|
62
|
+
|
|
63
|
+
/** @type {boolean} - Whether the knob is disabled */
|
|
64
|
+
disabled = false,
|
|
65
|
+
|
|
66
|
+
/** @type {boolean} - Whether to show the current value */
|
|
67
|
+
showValue = false,
|
|
68
|
+
|
|
69
|
+
/** @type {string} - Template for displaying the value, use {value} as placeholder */
|
|
70
|
+
valueTemplate = "{value}",
|
|
71
|
+
|
|
72
|
+
/** @type {string} - Color of the progress arc */
|
|
73
|
+
color,
|
|
74
|
+
|
|
75
|
+
/** @type {number} - Thickness of the progress arc (1-10) */
|
|
76
|
+
thickness = 4,
|
|
77
|
+
|
|
78
|
+
/** @type {boolean} - Whether to show tick marks */
|
|
79
|
+
showTicks = false,
|
|
80
|
+
|
|
81
|
+
/** @type {(event: CustomEvent) => void} - Input event handler */
|
|
82
|
+
oninput,
|
|
83
|
+
/** @type {(event: CustomEvent) => void} - Change event handler */
|
|
84
|
+
onchange,
|
|
85
|
+
|
|
86
|
+
/** @type {number} - Number of tick marks to display */
|
|
87
|
+
tickCount = 10,
|
|
88
|
+
|
|
89
|
+
/** @type {string} - ARIA label for accessibility */
|
|
90
|
+
ariaLabel,
|
|
91
|
+
} = $props()
|
|
92
|
+
|
|
93
|
+
// Get form context if available
|
|
94
|
+
const formContext = getContext("form")
|
|
95
|
+
|
|
96
|
+
// Component state
|
|
97
|
+
let currentValue = $state(value)
|
|
98
|
+
let isDragging = $state(false)
|
|
99
|
+
let knobElement = $state()
|
|
100
|
+
let radius = $state(0)
|
|
101
|
+
let center = $state({ x: 0, y: 0 })
|
|
102
|
+
|
|
103
|
+
// Register with form if available
|
|
104
|
+
let fieldApi = $state()
|
|
105
|
+
|
|
106
|
+
$effect(() => {
|
|
107
|
+
if (formContext && name) {
|
|
108
|
+
fieldApi = formContext.registerField(name, value)
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
// Update value when form field changes
|
|
113
|
+
$effect(() => {
|
|
114
|
+
if (fieldApi) {
|
|
115
|
+
const formValue = fieldApi.getValue()
|
|
116
|
+
if (formValue !== undefined && formValue !== currentValue) {
|
|
117
|
+
currentValue = formValue
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
// Update internal value when prop changes
|
|
123
|
+
$effect(() => {
|
|
124
|
+
currentValue = value
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Constrains a value to min/max bounds and applies step
|
|
129
|
+
* @param {number} val - Value to constrain
|
|
130
|
+
* @returns {number} - Constrained value
|
|
131
|
+
*/
|
|
132
|
+
function constrainValue(val) {
|
|
133
|
+
// Apply min/max constraints
|
|
134
|
+
let constrained = Math.max(min, Math.min(max, val))
|
|
135
|
+
|
|
136
|
+
// Apply step
|
|
137
|
+
if (step !== 0) {
|
|
138
|
+
constrained = Math.round((constrained - min) / step) * step + min
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return constrained
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Converts a value to a percentage (0-100)
|
|
146
|
+
* @param {number} val - Value to convert
|
|
147
|
+
* @returns {number} - Percentage
|
|
148
|
+
*/
|
|
149
|
+
function valueToPercentage(val) {
|
|
150
|
+
return ((val - min) / (max - min)) * 100
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Converts a percentage to a value
|
|
155
|
+
* @param {number} percentage - Percentage (0-100)
|
|
156
|
+
* @returns {number} - Value
|
|
157
|
+
*/
|
|
158
|
+
function percentageToValue(percentage) {
|
|
159
|
+
return (percentage / 100) * (max - min) + min
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Formats the current value using the template
|
|
164
|
+
* @returns {string} - Formatted value
|
|
165
|
+
*/
|
|
166
|
+
function formatValue() {
|
|
167
|
+
return valueTemplate.replace("{value}", currentValue.toString())
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Calculates the SVG path for the progress arc
|
|
172
|
+
* @param {number} percentage - Percentage (0-100)
|
|
173
|
+
* @returns {string} - SVG path
|
|
174
|
+
*/
|
|
175
|
+
function calculateArc(percentage) {
|
|
176
|
+
const r = 50 - thickness / 2 // Adjust radius for thickness
|
|
177
|
+
const circumference = 2 * Math.PI * r
|
|
178
|
+
const arcLength = (percentage / 100) * circumference
|
|
179
|
+
const dashArray = `${arcLength} ${circumference}`
|
|
180
|
+
|
|
181
|
+
return dashArray
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Calculates tick positions
|
|
186
|
+
* @returns {Array} - Array of tick positions
|
|
187
|
+
*/
|
|
188
|
+
function calculateTicks() {
|
|
189
|
+
const ticks = []
|
|
190
|
+
const r = 50 - thickness / 2 // Adjust radius for thickness
|
|
191
|
+
|
|
192
|
+
for (let i = 0; i < tickCount; i++) {
|
|
193
|
+
const percentage = (i / (tickCount - 1)) * 100
|
|
194
|
+
const angle = (percentage / 100) * 360 - 90 // -90 to start at top
|
|
195
|
+
const angleRad = (angle * Math.PI) / 180
|
|
196
|
+
|
|
197
|
+
const x1 = 50 + (r - 2) * Math.cos(angleRad)
|
|
198
|
+
const y1 = 50 + (r - 2) * Math.sin(angleRad)
|
|
199
|
+
const x2 = 50 + (r + 2) * Math.cos(angleRad)
|
|
200
|
+
const y2 = 50 + (r + 2) * Math.sin(angleRad)
|
|
201
|
+
|
|
202
|
+
ticks.push({ x1, y1, x2, y2 })
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return ticks
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Calculates the angle from center to a point
|
|
210
|
+
* @param {number} x - X coordinate
|
|
211
|
+
* @param {number} y - Y coordinate
|
|
212
|
+
* @returns {number} - Angle in degrees (0-360)
|
|
213
|
+
*/
|
|
214
|
+
function calculateAngle(x, y) {
|
|
215
|
+
const dx = x - center.x
|
|
216
|
+
const dy = y - center.y
|
|
217
|
+
|
|
218
|
+
// Calculate angle in radians
|
|
219
|
+
let angle = Math.atan2(dy, dx)
|
|
220
|
+
|
|
221
|
+
// Convert to degrees and adjust to start from top (0 degrees)
|
|
222
|
+
angle = (angle * 180) / Math.PI + 90
|
|
223
|
+
|
|
224
|
+
// Ensure angle is between 0-360
|
|
225
|
+
if (angle < 0) angle += 360
|
|
226
|
+
|
|
227
|
+
return angle
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Converts an angle to a percentage
|
|
232
|
+
* @param {number} angle - Angle in degrees (0-360)
|
|
233
|
+
* @returns {number} - Percentage (0-100)
|
|
234
|
+
*/
|
|
235
|
+
function angleToPercentage(angle) {
|
|
236
|
+
return (angle / 360) * 100
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Updates the value based on mouse/touch position
|
|
241
|
+
* @param {MouseEvent|TouchEvent} event - Mouse or touch event
|
|
242
|
+
*/
|
|
243
|
+
function updateValueFromEvent(event) {
|
|
244
|
+
if (disabled) return
|
|
245
|
+
|
|
246
|
+
// Get coordinates
|
|
247
|
+
const clientX = event.type.includes("touch") ? event.touches[0].clientX : event.clientX
|
|
248
|
+
const clientY = event.type.includes("touch") ? event.touches[0].clientY : event.clientY
|
|
249
|
+
|
|
250
|
+
// Get element position
|
|
251
|
+
const rect = knobElement.getBoundingClientRect()
|
|
252
|
+
const x = clientX - rect.left
|
|
253
|
+
const y = clientY - rect.top
|
|
254
|
+
|
|
255
|
+
// Calculate angle and percentage
|
|
256
|
+
const angle = calculateAngle(x, y)
|
|
257
|
+
const percentage = angleToPercentage(angle)
|
|
258
|
+
|
|
259
|
+
// Update value
|
|
260
|
+
const newValue = constrainValue(percentageToValue(percentage))
|
|
261
|
+
currentValue = newValue
|
|
262
|
+
|
|
263
|
+
// Update form field if available
|
|
264
|
+
if (fieldApi) {
|
|
265
|
+
fieldApi.setValue(newValue)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
oninput?.(new CustomEvent("input", { detail: { value: newValue } }))
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Starts dragging
|
|
273
|
+
* @param {MouseEvent|TouchEvent} event - Mouse or touch event
|
|
274
|
+
*/
|
|
275
|
+
function startDrag(event) {
|
|
276
|
+
if (disabled) return
|
|
277
|
+
|
|
278
|
+
isDragging = true
|
|
279
|
+
updateValueFromEvent(event)
|
|
280
|
+
|
|
281
|
+
// Add event listeners for drag
|
|
282
|
+
if (event.type === "mousedown") {
|
|
283
|
+
document.addEventListener("mousemove", updateValueFromEvent)
|
|
284
|
+
document.addEventListener("mouseup", stopDrag)
|
|
285
|
+
} else if (event.type === "touchstart") {
|
|
286
|
+
document.addEventListener("touchmove", updateValueFromEvent)
|
|
287
|
+
document.addEventListener("touchend", stopDrag)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Stops dragging
|
|
293
|
+
*/
|
|
294
|
+
function stopDrag() {
|
|
295
|
+
if (!isDragging) return
|
|
296
|
+
|
|
297
|
+
isDragging = false
|
|
298
|
+
|
|
299
|
+
// Remove event listeners
|
|
300
|
+
document.removeEventListener("mousemove", updateValueFromEvent)
|
|
301
|
+
document.removeEventListener("mouseup", stopDrag)
|
|
302
|
+
document.removeEventListener("touchmove", updateValueFromEvent)
|
|
303
|
+
document.removeEventListener("touchend", stopDrag)
|
|
304
|
+
|
|
305
|
+
onchange?.(new CustomEvent("change", { detail: { value: currentValue } }))
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Handles keyboard navigation
|
|
310
|
+
* @param {KeyboardEvent} event - Keydown event
|
|
311
|
+
*/
|
|
312
|
+
function handleKeydown(event) {
|
|
313
|
+
if (disabled) return
|
|
314
|
+
|
|
315
|
+
let newValue = currentValue
|
|
316
|
+
|
|
317
|
+
switch (event.key) {
|
|
318
|
+
case "ArrowUp":
|
|
319
|
+
case "ArrowRight":
|
|
320
|
+
event.preventDefault()
|
|
321
|
+
newValue = constrainValue(currentValue + step)
|
|
322
|
+
break
|
|
323
|
+
|
|
324
|
+
case "ArrowDown":
|
|
325
|
+
case "ArrowLeft":
|
|
326
|
+
event.preventDefault()
|
|
327
|
+
newValue = constrainValue(currentValue - step)
|
|
328
|
+
break
|
|
329
|
+
|
|
330
|
+
case "Home":
|
|
331
|
+
event.preventDefault()
|
|
332
|
+
newValue = min
|
|
333
|
+
break
|
|
334
|
+
|
|
335
|
+
case "End":
|
|
336
|
+
event.preventDefault()
|
|
337
|
+
newValue = max
|
|
338
|
+
break
|
|
339
|
+
|
|
340
|
+
case "PageUp":
|
|
341
|
+
event.preventDefault()
|
|
342
|
+
newValue = constrainValue(currentValue + step * 10)
|
|
343
|
+
break
|
|
344
|
+
|
|
345
|
+
case "PageDown":
|
|
346
|
+
event.preventDefault()
|
|
347
|
+
newValue = constrainValue(currentValue - step * 10)
|
|
348
|
+
break
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (newValue !== currentValue) {
|
|
352
|
+
currentValue = newValue
|
|
353
|
+
|
|
354
|
+
// Update form field if available
|
|
355
|
+
if (fieldApi) {
|
|
356
|
+
fieldApi.setValue(newValue)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
dispatch("input", { value: newValue })
|
|
360
|
+
dispatch("change", { value: newValue })
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Initialize component
|
|
365
|
+
onMount(() => {
|
|
366
|
+
if (knobElement) {
|
|
367
|
+
const rect = knobElement.getBoundingClientRect()
|
|
368
|
+
radius = rect.width / 2
|
|
369
|
+
center = { x: radius, y: radius }
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return () => {
|
|
373
|
+
// Clean up event listeners
|
|
374
|
+
document.removeEventListener("mousemove", updateValueFromEvent)
|
|
375
|
+
document.removeEventListener("mouseup", stopDrag)
|
|
376
|
+
document.removeEventListener("touchmove", updateValueFromEvent)
|
|
377
|
+
document.removeEventListener("touchend", stopDrag)
|
|
378
|
+
}
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
// Computed values
|
|
382
|
+
const percentage = $derived(valueToPercentage(currentValue))
|
|
383
|
+
const arcDashArray = $derived(calculateArc(percentage))
|
|
384
|
+
const ticks = $derived(showTicks ? calculateTicks() : [])
|
|
385
|
+
|
|
386
|
+
// Determine size classes
|
|
387
|
+
const sizeClasses = $derived(
|
|
388
|
+
{
|
|
389
|
+
sm: "w-16 h-16",
|
|
390
|
+
md: "w-24 h-24",
|
|
391
|
+
lg: "w-32 h-32",
|
|
392
|
+
xl: "w-40 h-40",
|
|
393
|
+
}[size] || "w-24 h-24"
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
// Determine font size classes
|
|
397
|
+
const fontSizeClasses = $derived(
|
|
398
|
+
{
|
|
399
|
+
sm: "text-xs",
|
|
400
|
+
md: "text-sm",
|
|
401
|
+
lg: "text-base",
|
|
402
|
+
xl: "text-lg",
|
|
403
|
+
}[size] || "text-sm"
|
|
404
|
+
)
|
|
405
|
+
</script>
|
|
406
|
+
|
|
407
|
+
<div
|
|
408
|
+
{id}
|
|
409
|
+
class="knob {sizeClasses} {className}"
|
|
410
|
+
class:disabled
|
|
411
|
+
tabindex={disabled ? undefined : 0}
|
|
412
|
+
role="slider"
|
|
413
|
+
aria-valuemin={min}
|
|
414
|
+
aria-valuemax={max}
|
|
415
|
+
aria-valuenow={currentValue}
|
|
416
|
+
aria-label={ariaLabel || name}
|
|
417
|
+
onkeydown={handleKeydown}
|
|
418
|
+
bind:this={knobElement}
|
|
419
|
+
>
|
|
420
|
+
<svg
|
|
421
|
+
viewBox="0 0 100 100"
|
|
422
|
+
class="knob-svg"
|
|
423
|
+
onmousedown={startDrag}
|
|
424
|
+
ontouchstart={startDrag}
|
|
425
|
+
>
|
|
426
|
+
<!-- Background circle -->
|
|
427
|
+
<circle
|
|
428
|
+
cx="50"
|
|
429
|
+
cy="50"
|
|
430
|
+
r={50 - thickness / 2}
|
|
431
|
+
class="knob-track"
|
|
432
|
+
stroke-width={thickness}
|
|
433
|
+
/>
|
|
434
|
+
|
|
435
|
+
<!-- Progress arc -->
|
|
436
|
+
<circle
|
|
437
|
+
cx="50"
|
|
438
|
+
cy="50"
|
|
439
|
+
r={50 - thickness / 2}
|
|
440
|
+
class="knob-progress"
|
|
441
|
+
stroke-width={thickness}
|
|
442
|
+
stroke-dasharray={arcDashArray}
|
|
443
|
+
style={color ? `stroke: ${color}` : ''}
|
|
444
|
+
transform="rotate(-90 50 50)"
|
|
445
|
+
/>
|
|
446
|
+
|
|
447
|
+
<!-- Tick marks -->
|
|
448
|
+
{#if showTicks}
|
|
449
|
+
{#each ticks as tick}
|
|
450
|
+
<line
|
|
451
|
+
x1={tick.x1}
|
|
452
|
+
y1={tick.y1}
|
|
453
|
+
x2={tick.x2}
|
|
454
|
+
y2={tick.y2}
|
|
455
|
+
class="knob-tick"
|
|
456
|
+
/>
|
|
457
|
+
{/each}
|
|
458
|
+
{/if}
|
|
459
|
+
|
|
460
|
+
<!-- Indicator dot -->
|
|
461
|
+
{#if !showValue}
|
|
462
|
+
<circle
|
|
463
|
+
cx={50 + (50 - thickness / 2 - 3) * Math.cos((percentage / 100 * 360 - 90) * Math.PI / 180)}
|
|
464
|
+
cy={50 + (50 - thickness / 2 - 3) * Math.sin((percentage / 100 * 360 - 90) * Math.PI / 180)}
|
|
465
|
+
r="3"
|
|
466
|
+
class="knob-indicator"
|
|
467
|
+
style={color ? `fill: ${color}` : ''}
|
|
468
|
+
/>
|
|
469
|
+
{/if}
|
|
470
|
+
|
|
471
|
+
<!-- Value text -->
|
|
472
|
+
{#if showValue}
|
|
473
|
+
<text
|
|
474
|
+
x="50"
|
|
475
|
+
y="50"
|
|
476
|
+
text-anchor="middle"
|
|
477
|
+
dominant-baseline="middle"
|
|
478
|
+
class="knob-value {fontSizeClasses}"
|
|
479
|
+
>
|
|
480
|
+
{formatValue()}
|
|
481
|
+
</text>
|
|
482
|
+
{/if}
|
|
483
|
+
</svg>
|
|
484
|
+
|
|
485
|
+
<!-- Hidden input for form submission -->
|
|
486
|
+
<input
|
|
487
|
+
type="hidden"
|
|
488
|
+
{name}
|
|
489
|
+
value={currentValue}
|
|
490
|
+
{disabled}
|
|
491
|
+
/>
|
|
492
|
+
</div>
|
|
493
|
+
|
|
494
|
+
<style>
|
|
495
|
+
@reference "../../twintrinsic.css";
|
|
496
|
+
|
|
497
|
+
.knob {
|
|
498
|
+
@apply relative inline-block;
|
|
499
|
+
@apply focus:outline-none focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400 rounded-full;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
.knob.disabled {
|
|
503
|
+
@apply opacity-50 cursor-not-allowed;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
.knob-svg {
|
|
507
|
+
@apply w-full h-full cursor-pointer;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
.knob.disabled .knob-svg {
|
|
511
|
+
@apply cursor-not-allowed;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
.knob-track {
|
|
515
|
+
@apply fill-none stroke-border dark:stroke-border;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
.knob-progress {
|
|
519
|
+
@apply fill-none stroke-primary-500 dark:stroke-primary-400;
|
|
520
|
+
stroke-linecap: round;
|
|
521
|
+
transform-origin: center;
|
|
522
|
+
stroke-dashoffset: 0;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
.knob-indicator {
|
|
526
|
+
@apply fill-primary-500 dark:fill-primary-400;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
.knob-tick {
|
|
530
|
+
@apply stroke-border dark:stroke-border;
|
|
531
|
+
stroke-width: 1;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
.knob-value {
|
|
535
|
+
@apply fill-text dark:fill-text font-medium;
|
|
536
|
+
}
|
|
537
|
+
</style>
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export default Knob;
|
|
2
|
+
type Knob = {
|
|
3
|
+
$on?(type: string, callback: (e: any) => void): () => void;
|
|
4
|
+
$set?(props: Partial<$$ComponentProps>): void;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Knob - A circular progress slider component for selecting numeric values.
|
|
8
|
+
* Provides an intuitive circular interface with drag interaction and keyboard controls.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* ```svelte
|
|
12
|
+
* <Knob
|
|
13
|
+
* name="volume"
|
|
14
|
+
* value={50}
|
|
15
|
+
* min={0}
|
|
16
|
+
* max={100}
|
|
17
|
+
* />
|
|
18
|
+
*
|
|
19
|
+
* <Knob
|
|
20
|
+
* name="progress"
|
|
21
|
+
* value={75}
|
|
22
|
+
* showValue
|
|
23
|
+
* valueTemplate="{value}%"
|
|
24
|
+
* size="lg"
|
|
25
|
+
* />
|
|
26
|
+
*
|
|
27
|
+
* <FormField label="Temperature">
|
|
28
|
+
* <Knob
|
|
29
|
+
* name="temperature"
|
|
30
|
+
* value={22}
|
|
31
|
+
* min={15}
|
|
32
|
+
* max={30}
|
|
33
|
+
* step={0.5}
|
|
34
|
+
* color="var(--color-primary-500)"
|
|
35
|
+
* />
|
|
36
|
+
* </FormField>
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
declare const Knob: import("svelte").Component<{
|
|
40
|
+
class?: string;
|
|
41
|
+
id?: any;
|
|
42
|
+
name: any;
|
|
43
|
+
value?: number;
|
|
44
|
+
min?: number;
|
|
45
|
+
max?: number;
|
|
46
|
+
step?: number;
|
|
47
|
+
size?: string;
|
|
48
|
+
disabled?: boolean;
|
|
49
|
+
showValue?: boolean;
|
|
50
|
+
valueTemplate?: string;
|
|
51
|
+
color: any;
|
|
52
|
+
thickness?: number;
|
|
53
|
+
showTicks?: boolean;
|
|
54
|
+
oninput: any;
|
|
55
|
+
onchange: any;
|
|
56
|
+
tickCount?: number;
|
|
57
|
+
ariaLabel: any;
|
|
58
|
+
}, {}, "">;
|
|
59
|
+
type $$ComponentProps = {
|
|
60
|
+
class?: string;
|
|
61
|
+
id?: any;
|
|
62
|
+
name: any;
|
|
63
|
+
value?: number;
|
|
64
|
+
min?: number;
|
|
65
|
+
max?: number;
|
|
66
|
+
step?: number;
|
|
67
|
+
size?: string;
|
|
68
|
+
disabled?: boolean;
|
|
69
|
+
showValue?: boolean;
|
|
70
|
+
valueTemplate?: string;
|
|
71
|
+
color: any;
|
|
72
|
+
thickness?: number;
|
|
73
|
+
showTicks?: boolean;
|
|
74
|
+
oninput: any;
|
|
75
|
+
onchange: any;
|
|
76
|
+
tickCount?: number;
|
|
77
|
+
ariaLabel: any;
|
|
78
|
+
};
|