@tremolo-ui/vue 0.7.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/LICENSE +21 -0
- package/README.md +13 -0
- package/dist/index.cjs +2486 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1678 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +1678 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2444 -0
- package/dist/index.js.map +1 -0
- package/package.json +78 -0
- package/src/components/AnimationCanvas/AnimationCanvas.ts +105 -0
- package/src/components/AnimationCanvas/index.ts +5 -0
- package/src/components/DropZone/DropZone.ts +43 -0
- package/src/components/DropZone/index.ts +1 -0
- package/src/components/FileInput/FileInput.ts +82 -0
- package/src/components/FileInput/FileInputTrigger.ts +24 -0
- package/src/components/FileInput/context.ts +18 -0
- package/src/components/FileInput/index.ts +3 -0
- package/src/components/Knob/Knob.ts +237 -0
- package/src/components/Knob/KnobActiveLine.ts +31 -0
- package/src/components/Knob/KnobInactiveLine.ts +36 -0
- package/src/components/Knob/KnobSVGRoot.ts +25 -0
- package/src/components/Knob/KnobThumb.ts +53 -0
- package/src/components/Knob/context.ts +30 -0
- package/src/components/Knob/index.ts +6 -0
- package/src/components/NumberInput/NumberInput.ts +258 -0
- package/src/components/NumberInput/NumberInputDecrementStepper.ts +7 -0
- package/src/components/NumberInput/NumberInputField.ts +128 -0
- package/src/components/NumberInput/NumberInputIncrementStepper.ts +7 -0
- package/src/components/NumberInput/NumberInputStepper.ts +54 -0
- package/src/components/NumberInput/context.ts +63 -0
- package/src/components/NumberInput/index.ts +6 -0
- package/src/components/NumberInput/stepper-button.ts +65 -0
- package/src/components/Piano/Piano.ts +243 -0
- package/src/components/Piano/index.ts +1 -0
- package/src/components/PointsEditor/PointsEditor.ts +146 -0
- package/src/components/PointsEditor/PointsEditorBackground.ts +17 -0
- package/src/components/PointsEditor/PointsEditorContainer.ts +73 -0
- package/src/components/PointsEditor/PointsEditorPoint.ts +253 -0
- package/src/components/PointsEditor/PointsEditorSelectionBox.ts +36 -0
- package/src/components/PointsEditor/context.ts +39 -0
- package/src/components/PointsEditor/index.ts +9 -0
- package/src/components/Slider/Slider.ts +224 -0
- package/src/components/Slider/SliderMarks.ts +57 -0
- package/src/components/Slider/SliderMarksOption.ts +85 -0
- package/src/components/Slider/SliderThumb.ts +92 -0
- package/src/components/Slider/SliderTrack.ts +50 -0
- package/src/components/Slider/context.ts +33 -0
- package/src/components/Slider/index.ts +6 -0
- package/src/components/XYPad/XYPad.ts +250 -0
- package/src/components/XYPad/XYPadArea.ts +20 -0
- package/src/components/XYPad/XYPadThumb.ts +105 -0
- package/src/components/XYPad/context.ts +33 -0
- package/src/components/XYPad/index.ts +4 -0
- package/src/components/_util/context.ts +8 -0
- package/src/components/_util/placement.ts +36 -0
- package/src/components/_util/props.ts +32 -0
- package/src/components/_util/useCheckSteps.ts +22 -0
- package/src/components/_util/visually-hidden-range-input.ts +16 -0
- package/src/composables/useDrag.ts +22 -0
- package/src/composables/useDragValue.ts +20 -0
- package/src/composables/useDropZone.ts +32 -0
- package/src/composables/useInstance.ts +52 -0
- package/src/composables/useLongPress.ts +20 -0
- package/src/composables/useMIDIAccess.ts +58 -0
- package/src/composables/useMIDIInput.ts +33 -0
- package/src/composables/useMIDIMessage.ts +30 -0
- package/src/composables/useWheel.ts +23 -0
- package/src/index.ts +67 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { defineComponent, h, provide, useId } from 'vue'
|
|
2
|
+
|
|
3
|
+
import { partitionByAccept, visuallyHiddenStyle } from '@tremolo-ui/dom'
|
|
4
|
+
|
|
5
|
+
import { FileInputKey } from './context'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Picks files. The control is the native file input, out of sight but in the
|
|
9
|
+
* tab order, with `FileInputTrigger` as its label.
|
|
10
|
+
*/
|
|
11
|
+
export const FileInput = /* @__PURE__ */ defineComponent({
|
|
12
|
+
name: 'FileInput',
|
|
13
|
+
inheritAttrs: false,
|
|
14
|
+
props: {
|
|
15
|
+
/**
|
|
16
|
+
* Which files to take: extensions, MIME types and type groups. It is
|
|
17
|
+
* checked again when the files arrive, since the picker only takes it as
|
|
18
|
+
* a hint.
|
|
19
|
+
*/
|
|
20
|
+
accept: String,
|
|
21
|
+
/** Let several files be picked at once. */
|
|
22
|
+
multiple: Boolean,
|
|
23
|
+
/** Make the input unusable. The parts carry `data-disabled`. */
|
|
24
|
+
disabled: Boolean,
|
|
25
|
+
},
|
|
26
|
+
emits: {
|
|
27
|
+
/** The picked files that satisfy `accept`. */
|
|
28
|
+
change: (files: File[]) => Array.isArray(files),
|
|
29
|
+
/** The picked files that do not satisfy `accept`, after `change` for the same selection. */
|
|
30
|
+
reject: (files: File[]) => Array.isArray(files),
|
|
31
|
+
},
|
|
32
|
+
setup(props, { slots, emit, attrs }) {
|
|
33
|
+
const inputId = useId()
|
|
34
|
+
provide(FileInputKey, {
|
|
35
|
+
inputId,
|
|
36
|
+
get disabled() {
|
|
37
|
+
return props.disabled
|
|
38
|
+
},
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
function onChange(event: Event) {
|
|
42
|
+
const input = event.target as HTMLInputElement
|
|
43
|
+
const { accepted, rejected } = partitionByAccept(
|
|
44
|
+
Array.from(input.files ?? []),
|
|
45
|
+
props.accept,
|
|
46
|
+
)
|
|
47
|
+
// Picking the same file twice fires no second change while the value
|
|
48
|
+
// is still on the input, so it is cleared as soon as it is read.
|
|
49
|
+
input.value = ''
|
|
50
|
+
if (accepted.length > 0) emit('change', accepted)
|
|
51
|
+
if (rejected.length > 0) emit('reject', rejected)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return () => {
|
|
55
|
+
const {
|
|
56
|
+
'aria-label': ariaLabel,
|
|
57
|
+
'aria-labelledby': ariaLabelledby,
|
|
58
|
+
'aria-describedby': ariaDescribedby,
|
|
59
|
+
...rest
|
|
60
|
+
} = attrs
|
|
61
|
+
return h(
|
|
62
|
+
'div',
|
|
63
|
+
{ ...rest, 'data-disabled': props.disabled ? '' : undefined },
|
|
64
|
+
[
|
|
65
|
+
h('input', {
|
|
66
|
+
id: inputId,
|
|
67
|
+
type: 'file',
|
|
68
|
+
accept: props.accept,
|
|
69
|
+
multiple: props.multiple,
|
|
70
|
+
disabled: props.disabled,
|
|
71
|
+
style: visuallyHiddenStyle,
|
|
72
|
+
'aria-label': ariaLabel,
|
|
73
|
+
'aria-labelledby': ariaLabelledby,
|
|
74
|
+
'aria-describedby': ariaDescribedby,
|
|
75
|
+
onChange,
|
|
76
|
+
}),
|
|
77
|
+
slots.default?.(),
|
|
78
|
+
],
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { defineComponent, h } from 'vue'
|
|
2
|
+
|
|
3
|
+
import { useFileInputContext } from './context'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Opens the file picker and names the input. A `<label>` rather than a
|
|
7
|
+
* button: the browser forwards the click, the input stays the one thing in
|
|
8
|
+
* the tab order, and its name comes from this text.
|
|
9
|
+
*/
|
|
10
|
+
export const FileInputTrigger = /* @__PURE__ */ defineComponent({
|
|
11
|
+
name: 'FileInputTrigger',
|
|
12
|
+
setup(_, { slots }) {
|
|
13
|
+
const fileInput = useFileInputContext()
|
|
14
|
+
return () =>
|
|
15
|
+
h(
|
|
16
|
+
'label',
|
|
17
|
+
{
|
|
18
|
+
for: fileInput.inputId,
|
|
19
|
+
'data-disabled': fileInput.disabled ? '' : undefined,
|
|
20
|
+
},
|
|
21
|
+
slots.default?.(),
|
|
22
|
+
)
|
|
23
|
+
},
|
|
24
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type InjectionKey } from 'vue'
|
|
2
|
+
|
|
3
|
+
import { injectContext } from '../_util/context'
|
|
4
|
+
|
|
5
|
+
/** What `FileInput` shares with `FileInputTrigger`. */
|
|
6
|
+
export interface FileInputContextValue {
|
|
7
|
+
/** The id of the file input, for the trigger's `for`. */
|
|
8
|
+
readonly inputId: string
|
|
9
|
+
readonly disabled: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const FileInputKey: InjectionKey<FileInputContextValue> =
|
|
13
|
+
Symbol('FileInput')
|
|
14
|
+
|
|
15
|
+
/** The context of the enclosing `FileInput`, for a part of your own. */
|
|
16
|
+
export function useFileInputContext(): FileInputContextValue {
|
|
17
|
+
return injectContext(FileInputKey, 'FileInput')
|
|
18
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { computed, defineComponent, h, provide, ref, type PropType } from 'vue'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
applyDelta,
|
|
5
|
+
arrowKeyDirection,
|
|
6
|
+
cssLength,
|
|
7
|
+
DEFAULT_DRAG_SENSITIVITY,
|
|
8
|
+
DEFAULT_KEYBOARD_OPTIONS,
|
|
9
|
+
DEFAULT_WHEEL_OPTIONS,
|
|
10
|
+
knobAngles,
|
|
11
|
+
relativeMapping,
|
|
12
|
+
selectModifier,
|
|
13
|
+
wheelDirection,
|
|
14
|
+
type AxisOptions,
|
|
15
|
+
type InputEventOption,
|
|
16
|
+
type ModifierValue,
|
|
17
|
+
type XY,
|
|
18
|
+
} from '@tremolo-ui/dom'
|
|
19
|
+
import { linearScale, type Scale } from '@tremolo-ui/functions'
|
|
20
|
+
|
|
21
|
+
import { useDragValue } from '../../composables/useDragValue'
|
|
22
|
+
import { useWheel } from '../../composables/useWheel'
|
|
23
|
+
import { useCheckSteps } from '../_util/useCheckSteps'
|
|
24
|
+
|
|
25
|
+
import { KnobKey } from './context'
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Interactive rotary knob, drawn in SVG by `KnobSVGRoot` and the parts inside
|
|
29
|
+
* it. Bind the value with `v-model`.
|
|
30
|
+
*/
|
|
31
|
+
export const Knob = /* @__PURE__ */ defineComponent({
|
|
32
|
+
name: 'Knob',
|
|
33
|
+
props: {
|
|
34
|
+
/** The current value. Bind it with `v-model`. */
|
|
35
|
+
modelValue: { type: Number, required: true },
|
|
36
|
+
/** The value with the knob turned all the way down. */
|
|
37
|
+
min: { type: Number, required: true },
|
|
38
|
+
/** The value with the knob turned all the way up. */
|
|
39
|
+
max: { type: Number, required: true },
|
|
40
|
+
/** Granularity of the value. @default 1 */
|
|
41
|
+
step: { type: Number, default: 1 },
|
|
42
|
+
/** How the value is distributed across the travel. @default linearScale */
|
|
43
|
+
scale: { type: Object as PropType<Scale>, default: () => linearScale },
|
|
44
|
+
/** The value a double click restores. @default min */
|
|
45
|
+
defaultValue: Number,
|
|
46
|
+
/** Where the active arc starts. @default min */
|
|
47
|
+
startValue: Number,
|
|
48
|
+
/** Width and height of the knob. Sets `--knob-size`. */
|
|
49
|
+
size: [Number, String],
|
|
50
|
+
/** The cursor to show while dragging. @default { cursor: 'grabbing' } */
|
|
51
|
+
externalStyles: Object as PropType<{ cursor?: string }>,
|
|
52
|
+
/** How much one notch of the wheel moves the value. `null` turns it off. */
|
|
53
|
+
wheel: {
|
|
54
|
+
type: [Array, Object] as PropType<ModifierValue<InputEventOption> | null>,
|
|
55
|
+
default: () => DEFAULT_WHEEL_OPTIONS,
|
|
56
|
+
},
|
|
57
|
+
/** How much one arrow key press moves the value. `null` turns it off. */
|
|
58
|
+
keyboard: {
|
|
59
|
+
type: [Array, Object] as PropType<ModifierValue<InputEventOption> | null>,
|
|
60
|
+
default: () => DEFAULT_KEYBOARD_OPTIONS,
|
|
61
|
+
},
|
|
62
|
+
/** How much a drag moves the value, per modifier key. */
|
|
63
|
+
dragSensitivity: {
|
|
64
|
+
type: [Number, Object] as PropType<ModifierValue<number>>,
|
|
65
|
+
default: () => DEFAULT_DRAG_SENSITIVITY,
|
|
66
|
+
},
|
|
67
|
+
/** Hide the cursor while dragging and read the movement directly. */
|
|
68
|
+
pointerLock: Boolean,
|
|
69
|
+
/** Restore `defaultValue` on a double click. @default true */
|
|
70
|
+
enableDoubleClickDefault: { type: Boolean, default: true },
|
|
71
|
+
/** Make the knob unchangeable and remove it from the tab order. */
|
|
72
|
+
disabled: Boolean,
|
|
73
|
+
/** Make the knob unchangeable while leaving it focusable. */
|
|
74
|
+
readonly: Boolean,
|
|
75
|
+
/** How far the knob turns from `min` to `max`, in degrees. @default 270 */
|
|
76
|
+
angleRange: { type: Number, default: 270 },
|
|
77
|
+
},
|
|
78
|
+
emits: {
|
|
79
|
+
'update:modelValue': (value: number) => typeof value === 'number',
|
|
80
|
+
},
|
|
81
|
+
setup(props, { slots, emit, expose }) {
|
|
82
|
+
const root = ref<HTMLDivElement | null>(null)
|
|
83
|
+
const dragging = ref(false)
|
|
84
|
+
const inactive = computed(() => props.disabled || props.readonly)
|
|
85
|
+
const range = computed(() => ({
|
|
86
|
+
min: props.min,
|
|
87
|
+
max: props.max,
|
|
88
|
+
step: props.step,
|
|
89
|
+
scale: props.scale,
|
|
90
|
+
}))
|
|
91
|
+
const angles = computed(() =>
|
|
92
|
+
knobAngles({
|
|
93
|
+
value: props.modelValue,
|
|
94
|
+
min: props.min,
|
|
95
|
+
max: props.max,
|
|
96
|
+
scale: props.scale,
|
|
97
|
+
startValue: props.startValue ?? props.min,
|
|
98
|
+
angleRange: props.angleRange,
|
|
99
|
+
}),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
useCheckSteps(() => ({
|
|
103
|
+
component: 'Knob',
|
|
104
|
+
range: range.value,
|
|
105
|
+
keyboard: props.keyboard,
|
|
106
|
+
wheel: props.wheel,
|
|
107
|
+
}))
|
|
108
|
+
|
|
109
|
+
provide(KnobKey, {
|
|
110
|
+
get value() {
|
|
111
|
+
return props.modelValue
|
|
112
|
+
},
|
|
113
|
+
get min() {
|
|
114
|
+
return props.min
|
|
115
|
+
},
|
|
116
|
+
get max() {
|
|
117
|
+
return props.max
|
|
118
|
+
},
|
|
119
|
+
get step() {
|
|
120
|
+
return props.step
|
|
121
|
+
},
|
|
122
|
+
get scale() {
|
|
123
|
+
return props.scale
|
|
124
|
+
},
|
|
125
|
+
get startValue() {
|
|
126
|
+
return props.startValue ?? props.min
|
|
127
|
+
},
|
|
128
|
+
get angleRange() {
|
|
129
|
+
return props.angleRange
|
|
130
|
+
},
|
|
131
|
+
get angles() {
|
|
132
|
+
return angles.value
|
|
133
|
+
},
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
const change = (next: number) => emit('update:modelValue', next)
|
|
137
|
+
|
|
138
|
+
// The knob has no travel of its own: 100px of movement spans the whole
|
|
139
|
+
// range, and only the vertical axis carries a value, reversed so that
|
|
140
|
+
// dragging up raises it.
|
|
141
|
+
useDragValue(root, () => ({
|
|
142
|
+
axis: [range.value, { ...range.value, reverse: true }] as XY<AxisOptions>,
|
|
143
|
+
mapping: relativeMapping({
|
|
144
|
+
pixelRange: 100,
|
|
145
|
+
sensitivity: (state) =>
|
|
146
|
+
selectModifier(props.dragSensitivity, state.event).value,
|
|
147
|
+
}),
|
|
148
|
+
getValue: (): XY<number> => [props.modelValue, props.modelValue],
|
|
149
|
+
threshold: 1,
|
|
150
|
+
cursor: inactive.value
|
|
151
|
+
? undefined
|
|
152
|
+
: (props.externalStyles?.cursor ?? 'grabbing'),
|
|
153
|
+
pointerLock: inactive.value ? false : props.pointerLock,
|
|
154
|
+
shouldStart: () => !inactive.value,
|
|
155
|
+
onChange: (v) => {
|
|
156
|
+
if (!inactive.value) change(v[1])
|
|
157
|
+
},
|
|
158
|
+
onDragStart: () => {
|
|
159
|
+
dragging.value = true
|
|
160
|
+
},
|
|
161
|
+
onDragEnd: () => {
|
|
162
|
+
dragging.value = false
|
|
163
|
+
},
|
|
164
|
+
}))
|
|
165
|
+
|
|
166
|
+
useWheel(
|
|
167
|
+
root,
|
|
168
|
+
(event) => {
|
|
169
|
+
if (!props.wheel || inactive.value) return
|
|
170
|
+
// A notch the knob does not read — a sideways scroll — is left to
|
|
171
|
+
// the page rather than swallowed.
|
|
172
|
+
const direction = wheelDirection(event)
|
|
173
|
+
if (direction === null) return
|
|
174
|
+
event.preventDefault()
|
|
175
|
+
change(
|
|
176
|
+
applyDelta(
|
|
177
|
+
props.modelValue,
|
|
178
|
+
direction,
|
|
179
|
+
props.wheel,
|
|
180
|
+
range.value,
|
|
181
|
+
event,
|
|
182
|
+
),
|
|
183
|
+
)
|
|
184
|
+
},
|
|
185
|
+
{ requireFocus: true },
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
expose({
|
|
189
|
+
/** Move the focus to the knob. */
|
|
190
|
+
focus: () => {
|
|
191
|
+
if (!props.disabled) root.value?.focus()
|
|
192
|
+
},
|
|
193
|
+
/** Take the focus away from the knob. */
|
|
194
|
+
blur: () => root.value?.blur(),
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
return () =>
|
|
198
|
+
h(
|
|
199
|
+
'div',
|
|
200
|
+
{
|
|
201
|
+
ref: root,
|
|
202
|
+
role: 'slider',
|
|
203
|
+
tabindex: props.disabled ? -1 : 0,
|
|
204
|
+
'aria-valuenow': props.modelValue,
|
|
205
|
+
'aria-valuemin': props.min,
|
|
206
|
+
'aria-valuemax': props.max,
|
|
207
|
+
'aria-disabled': props.disabled,
|
|
208
|
+
'aria-readonly': props.readonly,
|
|
209
|
+
'data-disabled': props.disabled ? '' : undefined,
|
|
210
|
+
'data-readonly': props.readonly ? '' : undefined,
|
|
211
|
+
'data-dragging': dragging.value ? '' : undefined,
|
|
212
|
+
style: { '--knob-size': cssLength(props.size) },
|
|
213
|
+
onKeydown: (event: KeyboardEvent) => {
|
|
214
|
+
if (!props.keyboard || inactive.value) return
|
|
215
|
+
const direction = arrowKeyDirection(event.key)
|
|
216
|
+
if (direction === null) return
|
|
217
|
+
event.preventDefault()
|
|
218
|
+
change(
|
|
219
|
+
applyDelta(
|
|
220
|
+
props.modelValue,
|
|
221
|
+
direction,
|
|
222
|
+
props.keyboard,
|
|
223
|
+
range.value,
|
|
224
|
+
event,
|
|
225
|
+
),
|
|
226
|
+
)
|
|
227
|
+
},
|
|
228
|
+
onDblclick: () => {
|
|
229
|
+
if (!inactive.value && props.enableDoubleClickDefault) {
|
|
230
|
+
change(props.defaultValue ?? props.min)
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
slots.default?.(),
|
|
235
|
+
)
|
|
236
|
+
},
|
|
237
|
+
})
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { defineComponent, h } from 'vue'
|
|
2
|
+
|
|
3
|
+
import { knobArcPath, knobArcRadius } from '@tremolo-ui/dom'
|
|
4
|
+
|
|
5
|
+
import { checkPlacement } from '../_util/placement'
|
|
6
|
+
|
|
7
|
+
import { useKnobContext } from './context'
|
|
8
|
+
|
|
9
|
+
/** The arc between `startValue` and the value, as an SVG `<path>`. */
|
|
10
|
+
export const KnobActiveLine = /* @__PURE__ */ defineComponent({
|
|
11
|
+
name: 'KnobActiveLine',
|
|
12
|
+
props: {
|
|
13
|
+
stroke: { type: String, default: 'currentColor' },
|
|
14
|
+
strokeWidth: { type: [Number, String], default: 6 },
|
|
15
|
+
},
|
|
16
|
+
setup(props) {
|
|
17
|
+
checkPlacement('KnobActiveLine', 'KnobSVGRoot')
|
|
18
|
+
const knob = useKnobContext()
|
|
19
|
+
return () =>
|
|
20
|
+
h('path', {
|
|
21
|
+
d: knobArcPath(
|
|
22
|
+
knob.angles.r2,
|
|
23
|
+
knob.angles.r3,
|
|
24
|
+
knobArcRadius(props.strokeWidth),
|
|
25
|
+
),
|
|
26
|
+
fill: 'none',
|
|
27
|
+
stroke: props.stroke,
|
|
28
|
+
'stroke-width': props.strokeWidth,
|
|
29
|
+
})
|
|
30
|
+
},
|
|
31
|
+
})
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { defineComponent, h } from 'vue'
|
|
2
|
+
|
|
3
|
+
import { knobArcPath, knobArcRadius } from '@tremolo-ui/dom'
|
|
4
|
+
|
|
5
|
+
import { checkPlacement } from '../_util/placement'
|
|
6
|
+
|
|
7
|
+
import { useKnobContext } from './context'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The travel outside the value, as SVG `<path>`s: one on each side of the
|
|
11
|
+
* active arc that has any length.
|
|
12
|
+
*/
|
|
13
|
+
export const KnobInactiveLine = /* @__PURE__ */ defineComponent({
|
|
14
|
+
name: 'KnobInactiveLine',
|
|
15
|
+
inheritAttrs: false,
|
|
16
|
+
props: {
|
|
17
|
+
stroke: { type: String, default: 'currentColor' },
|
|
18
|
+
strokeWidth: { type: [Number, String], default: 6 },
|
|
19
|
+
},
|
|
20
|
+
setup(props, { attrs }) {
|
|
21
|
+
checkPlacement('KnobInactiveLine', 'KnobSVGRoot')
|
|
22
|
+
const knob = useKnobContext()
|
|
23
|
+
const path = (from: number, to: number) =>
|
|
24
|
+
h('path', {
|
|
25
|
+
...attrs,
|
|
26
|
+
d: knobArcPath(from, to, knobArcRadius(props.strokeWidth)),
|
|
27
|
+
fill: 'none',
|
|
28
|
+
stroke: props.stroke,
|
|
29
|
+
'stroke-width': props.strokeWidth,
|
|
30
|
+
})
|
|
31
|
+
return () => [
|
|
32
|
+
knob.startValue > knob.min ? path(knob.angles.r1, knob.angles.r2) : null,
|
|
33
|
+
knob.startValue < knob.max ? path(knob.angles.r3, knob.angles.r4) : null,
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
})
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { defineComponent, h } from 'vue'
|
|
2
|
+
|
|
3
|
+
import { KNOB_VIEWBOX_SIZE } from '@tremolo-ui/dom'
|
|
4
|
+
|
|
5
|
+
import { providePlacement } from '../_util/placement'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The `<svg>` the drawing parts go in, in the order you want them painted.
|
|
9
|
+
* It sets the view box they are drawn against.
|
|
10
|
+
*/
|
|
11
|
+
export const KnobSVGRoot = /* @__PURE__ */ defineComponent({
|
|
12
|
+
name: 'KnobSVGRoot',
|
|
13
|
+
setup(_, { slots }) {
|
|
14
|
+
providePlacement('KnobSVGRoot')
|
|
15
|
+
return () =>
|
|
16
|
+
h(
|
|
17
|
+
'svg',
|
|
18
|
+
{
|
|
19
|
+
viewBox: `0 0 ${KNOB_VIEWBOX_SIZE} ${KNOB_VIEWBOX_SIZE}`,
|
|
20
|
+
style: { display: 'block' },
|
|
21
|
+
},
|
|
22
|
+
slots.default?.(),
|
|
23
|
+
)
|
|
24
|
+
},
|
|
25
|
+
})
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { defineComponent, h, type PropType } from 'vue'
|
|
2
|
+
|
|
3
|
+
import { KNOB_VIEWBOX_SIZE } from '@tremolo-ui/dom'
|
|
4
|
+
import { clamp } from '@tremolo-ui/functions'
|
|
5
|
+
|
|
6
|
+
import { checkPlacement } from '../_util/placement'
|
|
7
|
+
|
|
8
|
+
import { useKnobContext } from './context'
|
|
9
|
+
|
|
10
|
+
/** The circle that turns with the value, and the line on it that points at it. */
|
|
11
|
+
export const KnobThumb = /* @__PURE__ */ defineComponent({
|
|
12
|
+
name: 'KnobThumb',
|
|
13
|
+
props: {
|
|
14
|
+
/** Fill colour of the circle. @default 'currentColor' */
|
|
15
|
+
thumb: { type: String, default: 'currentColor' },
|
|
16
|
+
/** Colour of the line that points at the value. @default 'currentColor' */
|
|
17
|
+
thumbLine: { type: String, default: 'currentColor' },
|
|
18
|
+
/** Diameter of the circle, as a percentage of the knob. @default 84 */
|
|
19
|
+
thumbSize: { type: Number, default: 84 },
|
|
20
|
+
/** Thickness of the line, as a percentage of the knob. @default 6 */
|
|
21
|
+
thumbLineWeight: { type: Number, default: 6 },
|
|
22
|
+
/** How far down the line reaches, as a percentage. @default 35 */
|
|
23
|
+
thumbLineLength: { type: Number, default: 35 },
|
|
24
|
+
/** Classes for the line that points at the value. */
|
|
25
|
+
classes: Object as PropType<{ thumbLine?: string }>,
|
|
26
|
+
},
|
|
27
|
+
setup(props) {
|
|
28
|
+
checkPlacement('KnobThumb', 'KnobSVGRoot')
|
|
29
|
+
const knob = useKnobContext()
|
|
30
|
+
return () =>
|
|
31
|
+
h('svg', null, [
|
|
32
|
+
h('circle', {
|
|
33
|
+
cx: '50%',
|
|
34
|
+
cy: '50%',
|
|
35
|
+
r: `${props.thumbSize / 2}%`,
|
|
36
|
+
fill: props.thumb,
|
|
37
|
+
}),
|
|
38
|
+
h('line', {
|
|
39
|
+
class: props.classes?.thumbLine,
|
|
40
|
+
x1: '50%',
|
|
41
|
+
y1: `${(KNOB_VIEWBOX_SIZE - clamp(props.thumbSize, 0, 100)) / 2}%`,
|
|
42
|
+
x2: '50%',
|
|
43
|
+
y2: `${props.thumbLineLength}%`,
|
|
44
|
+
stroke: props.thumbLine,
|
|
45
|
+
'stroke-width': `${props.thumbLineWeight}%`,
|
|
46
|
+
style: {
|
|
47
|
+
transform: `rotate(${knob.angles.r1 + knob.angles.p * knob.angleRange}deg)`,
|
|
48
|
+
transformOrigin: '50% 50%',
|
|
49
|
+
},
|
|
50
|
+
}),
|
|
51
|
+
])
|
|
52
|
+
},
|
|
53
|
+
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type InjectionKey } from 'vue'
|
|
2
|
+
|
|
3
|
+
import { type KnobAngles } from '@tremolo-ui/dom'
|
|
4
|
+
import { type Scale } from '@tremolo-ui/functions'
|
|
5
|
+
|
|
6
|
+
import { injectContext } from '../_util/context'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* What `Knob` shares with its parts. The fields are getters, so reading one in
|
|
10
|
+
* a render or a `computed` follows the value as it changes.
|
|
11
|
+
*/
|
|
12
|
+
export interface KnobContextValue {
|
|
13
|
+
readonly value: number
|
|
14
|
+
readonly min: number
|
|
15
|
+
readonly max: number
|
|
16
|
+
readonly step: number
|
|
17
|
+
readonly scale: Scale
|
|
18
|
+
readonly startValue: number
|
|
19
|
+
/** How far the knob turns from `min` to `max`, in degrees. */
|
|
20
|
+
readonly angleRange: number
|
|
21
|
+
/** The angles the parts are drawn with. See `knobAngles` in `@tremolo-ui/dom`. */
|
|
22
|
+
readonly angles: KnobAngles
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const KnobKey: InjectionKey<KnobContextValue> = Symbol('Knob')
|
|
26
|
+
|
|
27
|
+
/** The context of the enclosing `Knob`, for a part of your own. */
|
|
28
|
+
export function useKnobContext(): KnobContextValue {
|
|
29
|
+
return injectContext(KnobKey, 'Knob')
|
|
30
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { useKnobContext, type KnobContextValue } from './context'
|
|
2
|
+
export { Knob } from './Knob'
|
|
3
|
+
export { KnobActiveLine } from './KnobActiveLine'
|
|
4
|
+
export { KnobInactiveLine } from './KnobInactiveLine'
|
|
5
|
+
export { KnobSVGRoot } from './KnobSVGRoot'
|
|
6
|
+
export { KnobThumb } from './KnobThumb'
|