noph-ui 0.46.0 → 0.47.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 +2 -1
- package/dist/button/ButtonGroup.svelte +1 -3
- package/dist/date-picker/DockedDatePicker.svelte +2 -3
- package/dist/date-picker/DockedDateTimePicker.svelte +2 -3
- package/dist/date-picker/dateUtils.d.ts +2 -13
- package/dist/date-picker/dateUtils.js +4 -66
- package/dist/date-picker/formatters.d.ts +4 -0
- package/dist/date-picker/formatters.js +26 -0
- package/dist/date-picker/timeUtils.d.ts +34 -0
- package/dist/date-picker/timeUtils.js +139 -0
- package/dist/dialog/Dialog.svelte +4 -2
- package/dist/dialog/types.d.ts +1 -0
- package/dist/fab/FabMenu.svelte +1 -2
- package/dist/form-reset.d.ts +2 -1
- package/dist/form-reset.js +2 -2
- package/dist/icons/KeyboardIcon.svelte +5 -0
- package/dist/icons/KeyboardIcon.svelte.d.ts +26 -0
- package/dist/icons/ScheduleIcon.svelte +5 -0
- package/dist/icons/ScheduleIcon.svelte.d.ts +26 -0
- package/dist/icons/index.d.ts +2 -0
- package/dist/icons/index.js +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/keyboard-nav.js +3 -2
- package/dist/list/index.d.ts +1 -0
- package/dist/list/index.js +1 -0
- package/dist/media.d.ts +4 -0
- package/dist/media.js +4 -0
- package/dist/menu/Menu.svelte +2 -1
- package/dist/progress/CircularProgress.svelte +9 -9
- package/dist/progress/LinearProgress.svelte +1 -2
- package/dist/ripple/Ripple.svelte +7 -8
- package/dist/search/Search.svelte +14 -4
- package/dist/search/Search.svelte.d.ts +1 -1
- package/dist/select/Select.svelte +9 -10
- package/dist/select/VirtualList.svelte +12 -13
- package/dist/sheet/Sheet.svelte +4 -1
- package/dist/sheet/types.d.ts +1 -0
- package/dist/slider/Slider.svelte +5 -6
- package/dist/tabs/Tabs.svelte +21 -14
- package/dist/time-picker/ClockDial.svelte +528 -0
- package/dist/time-picker/ClockDial.svelte.d.ts +4 -0
- package/dist/time-picker/DockedTimePicker.svelte +378 -0
- package/dist/time-picker/DockedTimePicker.svelte.d.ts +7 -0
- package/dist/time-picker/PeriodSelector.svelte +156 -0
- package/dist/time-picker/PeriodSelector.svelte.d.ts +4 -0
- package/dist/time-picker/TimeInputs.svelte +233 -0
- package/dist/time-picker/TimeInputs.svelte.d.ts +4 -0
- package/dist/time-picker/TimePickerDialog.svelte +229 -0
- package/dist/time-picker/TimePickerDialog.svelte.d.ts +7 -0
- package/dist/time-picker/TimePickerPanel.svelte +203 -0
- package/dist/time-picker/TimePickerPanel.svelte.d.ts +4 -0
- package/dist/time-picker/TimeSelectors.svelte +157 -0
- package/dist/time-picker/TimeSelectors.svelte.d.ts +4 -0
- package/dist/time-picker/index.d.ts +4 -0
- package/dist/time-picker/index.js +4 -0
- package/dist/time-picker/timePickerState.svelte.d.ts +27 -0
- package/dist/time-picker/timePickerState.svelte.js +63 -0
- package/dist/time-picker/types.d.ts +148 -0
- package/dist/time-picker/types.js +1 -0
- package/dist/tooltip/Tooltip.svelte +2 -4
- package/dist/types.d.ts +1 -0
- package/dist/types.js +1 -0
- package/dist/validity.d.ts +2 -0
- package/dist/validity.js +4 -0
- package/package.json +1 -1
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
formatMinutes,
|
|
4
|
+
formatTwoDigits,
|
|
5
|
+
isMinuteWithin,
|
|
6
|
+
MINUTES_IN_HOUR,
|
|
7
|
+
snapToStep,
|
|
8
|
+
} from '../date-picker/timeUtils.js'
|
|
9
|
+
import { tick } from 'svelte'
|
|
10
|
+
import type { ClockDialProps } from './types.js'
|
|
11
|
+
|
|
12
|
+
let {
|
|
13
|
+
value,
|
|
14
|
+
selection = 'hour',
|
|
15
|
+
hour12 = false,
|
|
16
|
+
minuteStep = 1,
|
|
17
|
+
min,
|
|
18
|
+
max,
|
|
19
|
+
locale,
|
|
20
|
+
disabled = false,
|
|
21
|
+
element = $bindable(),
|
|
22
|
+
isTimeEnabled,
|
|
23
|
+
selectHourLabel = 'Select hour',
|
|
24
|
+
selectMinuteLabel = 'Select minute',
|
|
25
|
+
hourOptionLabel = (hourValue, total) =>
|
|
26
|
+
`${hourValue} ${hourValue === '1' ? 'hour' : 'hours'} of ${total}`,
|
|
27
|
+
minuteOptionLabel = (minuteValue, total) =>
|
|
28
|
+
`${minuteValue} ${minuteValue === '1' ? 'minute' : 'minutes'} of ${total}`,
|
|
29
|
+
onselect,
|
|
30
|
+
onselectionend,
|
|
31
|
+
...attributes
|
|
32
|
+
}: ClockDialProps = $props()
|
|
33
|
+
|
|
34
|
+
const OUTER_RATIO = 0.454
|
|
35
|
+
const INNER_RATIO = 0.3021
|
|
36
|
+
const DRAG_THRESHOLD = 3
|
|
37
|
+
const FULL_CIRCLE = 360
|
|
38
|
+
|
|
39
|
+
let tracking = $state(false)
|
|
40
|
+
let announcement = $state('')
|
|
41
|
+
let handleElement = $state<HTMLDivElement>()
|
|
42
|
+
|
|
43
|
+
let step = $derived(Math.max(1, Math.trunc(minuteStep) || 1))
|
|
44
|
+
let hour = $derived(Math.floor(value / MINUTES_IN_HOUR))
|
|
45
|
+
let minute = $derived(value % MINUTES_IN_HOUR)
|
|
46
|
+
let isPm = $derived(hour >= 12)
|
|
47
|
+
|
|
48
|
+
const usable = (minutes: number) =>
|
|
49
|
+
isMinuteWithin(minutes, min, max) && (isTimeEnabled?.(minutes) ?? true)
|
|
50
|
+
|
|
51
|
+
const hourReachable = (candidate: number) => {
|
|
52
|
+
for (let each = 0; each < MINUTES_IN_HOUR; each += step) {
|
|
53
|
+
if (usable(candidate * MINUTES_IN_HOUR + each)) return true
|
|
54
|
+
}
|
|
55
|
+
return false
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Minutes for an hour ring position, keeping the minute that is already selected. */
|
|
59
|
+
const hourMinutes = (position: number) =>
|
|
60
|
+
(hour12 ? (position % 12) + (isPm ? 12 : 0) : position) * MINUTES_IN_HOUR + minute
|
|
61
|
+
|
|
62
|
+
interface DialOption {
|
|
63
|
+
/** Minutes since midnight this option selects. */
|
|
64
|
+
minutes: number
|
|
65
|
+
/** Ring position, 0 at the top going clockwise. */
|
|
66
|
+
position: number
|
|
67
|
+
/** How many positions the ring has. */
|
|
68
|
+
positions: number
|
|
69
|
+
label?: string
|
|
70
|
+
inner: boolean
|
|
71
|
+
disabled: boolean
|
|
72
|
+
accessibleLabel: string
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let options = $derived.by<DialOption[]>(() => {
|
|
76
|
+
if (selection === 'hour') {
|
|
77
|
+
const total = hour12 ? 12 : 24
|
|
78
|
+
return Array.from({ length: total }, (_, index) => {
|
|
79
|
+
const minutes = hourMinutes(index)
|
|
80
|
+
const inner = !hour12 && index >= 12
|
|
81
|
+
const shown = hour12 ? index % 12 || 12 : index
|
|
82
|
+
const label = hour12 ? `${shown}` : index === 0 ? '00' : `${index}`
|
|
83
|
+
return {
|
|
84
|
+
minutes,
|
|
85
|
+
position: index % 12,
|
|
86
|
+
positions: 12,
|
|
87
|
+
label,
|
|
88
|
+
inner,
|
|
89
|
+
disabled: !hourReachable(hour12 ? (index % 12) + (isPm ? 12 : 0) : index),
|
|
90
|
+
accessibleLabel: hourOptionLabel(`${shown}`, total),
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
const positions = Math.floor(MINUTES_IN_HOUR / step)
|
|
95
|
+
const total = positions
|
|
96
|
+
return Array.from({ length: positions }, (_, index) => {
|
|
97
|
+
const each = index * step
|
|
98
|
+
const minutes = hour * MINUTES_IN_HOUR + each
|
|
99
|
+
return {
|
|
100
|
+
minutes,
|
|
101
|
+
position: (each / MINUTES_IN_HOUR) * 12,
|
|
102
|
+
positions: 12,
|
|
103
|
+
// Only every fifth minute carries a number, as on a real clock face.
|
|
104
|
+
label: each % 5 === 0 ? formatTwoDigits(each, locale) : undefined,
|
|
105
|
+
inner: false,
|
|
106
|
+
disabled: !usable(minutes),
|
|
107
|
+
accessibleLabel: minuteOptionLabel(`${each}`, total),
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
/** The option the handle is resting on, or the nearest one when it sits between two. */
|
|
113
|
+
let selectedIndex = $derived.by(() => {
|
|
114
|
+
let nearest = -1
|
|
115
|
+
let distance = Infinity
|
|
116
|
+
options.forEach((option, index) => {
|
|
117
|
+
const gap = Math.abs(option.minutes - value)
|
|
118
|
+
if (gap < distance) {
|
|
119
|
+
distance = gap
|
|
120
|
+
nearest = index
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
return nearest
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
let tabStopIndex = $derived.by(() => {
|
|
127
|
+
if (selectedIndex >= 0 && !options[selectedIndex].disabled) return selectedIndex
|
|
128
|
+
const offered = options.findIndex((option) => !option.disabled)
|
|
129
|
+
return offered >= 0 ? offered : Math.max(selectedIndex, 0)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
const angleOf = (position: number, positions: number) => (position / positions) * FULL_CIRCLE - 90
|
|
133
|
+
|
|
134
|
+
const ratioFor = (inner: boolean) => (inner ? INNER_RATIO : OUTER_RATIO)
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* How far the numbers and the handle sit from the centre. The handle's own radius comes off the
|
|
138
|
+
* ring, the way Compose reads the ratios, which keeps a 48dp handle inside the 256dp dial instead
|
|
139
|
+
* of hanging over its edge.
|
|
140
|
+
*/
|
|
141
|
+
const radiusOf = (inner: boolean) =>
|
|
142
|
+
`calc(var(--_size) * ${ratioFor(inner)} - var(--_handle) / 2)`
|
|
143
|
+
|
|
144
|
+
/** The angle the handle points at, from the live value rather than the nearest labelled option. */
|
|
145
|
+
let handleAngle = $derived(
|
|
146
|
+
selection === 'hour' ? angleOf(hour % 12, 12) : angleOf((minute / MINUTES_IN_HOUR) * 12, 12),
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
let handleInner = $derived(selection === 'hour' && !hour12 && hour >= 12)
|
|
150
|
+
|
|
151
|
+
// Rotating to the nearest equivalent angle keeps 11 -> 12 going forwards by 30 degrees instead of
|
|
152
|
+
// spinning back 330. The running total is a plain variable so reading it creates no dependency.
|
|
153
|
+
let unwrapped = 0
|
|
154
|
+
let rotation = $state(0)
|
|
155
|
+
|
|
156
|
+
$effect(() => {
|
|
157
|
+
const target = handleAngle
|
|
158
|
+
const delta = ((((target - unwrapped) % FULL_CIRCLE) + FULL_CIRCLE * 1.5) % FULL_CIRCLE) - 180
|
|
159
|
+
unwrapped += delta
|
|
160
|
+
rotation = unwrapped
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
const geometry = () => {
|
|
164
|
+
const rect = element?.getBoundingClientRect()
|
|
165
|
+
if (!rect) return undefined
|
|
166
|
+
// Measured rather than assumed, so the ratios hold at any root font size or dial size.
|
|
167
|
+
const handleRadius = (handleElement?.offsetWidth ?? 0) / 2
|
|
168
|
+
return {
|
|
169
|
+
cx: rect.left + rect.width / 2,
|
|
170
|
+
cy: rect.top + rect.height / 2,
|
|
171
|
+
radius: (inner: boolean) => rect.width * ratioFor(inner) - handleRadius,
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Reads a pointer position as minutes since midnight. */
|
|
176
|
+
const fromPointer = (clientX: number, clientY: number, gesture: 'tap' | 'drag') => {
|
|
177
|
+
const box = geometry()
|
|
178
|
+
if (!box) return undefined
|
|
179
|
+
const dx = clientX - box.cx
|
|
180
|
+
const dy = clientY - box.cy
|
|
181
|
+
const turn = ((Math.atan2(dy, dx) + Math.PI / 2) / (2 * Math.PI) + 1) % 1
|
|
182
|
+
|
|
183
|
+
if (selection === 'hour') {
|
|
184
|
+
const position = Math.round(turn * 12) % 12
|
|
185
|
+
if (hour12) return hourMinutes(position)
|
|
186
|
+
const midpoint = (box.radius(false) + box.radius(true)) / 2
|
|
187
|
+
const inner = Math.hypot(dx, dy) < midpoint
|
|
188
|
+
return (position + (inner ? 12 : 0)) * MINUTES_IN_HOUR + minute
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// A tap lands on a whole five minutes, the way the numbers read; a drag keeps the full step.
|
|
192
|
+
const snap = gesture === 'tap' ? Math.max(step, 5) : step
|
|
193
|
+
const raw = Math.round(turn * MINUTES_IN_HOUR) % MINUTES_IN_HOUR
|
|
194
|
+
return snapToStep(hour * MINUTES_IN_HOUR + raw, snap)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const announce = () => {
|
|
198
|
+
announcement = formatMinutes(value, locale, hour12)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
let pointerId: number | undefined
|
|
202
|
+
let origin: { x: number; y: number } | undefined
|
|
203
|
+
let moved = false
|
|
204
|
+
|
|
205
|
+
// Capturing keeps the drag alive once the finger leaves the dial. It throws when the pointer is
|
|
206
|
+
// already gone, which is nothing to act on, so both directions are guarded.
|
|
207
|
+
const capture = (id: number, hold: boolean) => {
|
|
208
|
+
try {
|
|
209
|
+
if (hold) element?.setPointerCapture(id)
|
|
210
|
+
else element?.releasePointerCapture(id)
|
|
211
|
+
} catch {
|
|
212
|
+
// The pointer ended on its own; there is nothing left to capture or release.
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const handlePointerdown = (event: PointerEvent) => {
|
|
217
|
+
if (disabled || event.button !== 0) return
|
|
218
|
+
event.preventDefault()
|
|
219
|
+
pointerId = event.pointerId
|
|
220
|
+
origin = { x: event.clientX, y: event.clientY }
|
|
221
|
+
moved = false
|
|
222
|
+
capture(event.pointerId, true)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const handlePointermove = (event: PointerEvent) => {
|
|
226
|
+
if (disabled || pointerId !== event.pointerId || !origin) return
|
|
227
|
+
if (!moved && Math.hypot(event.clientX - origin.x, event.clientY - origin.y) < DRAG_THRESHOLD) {
|
|
228
|
+
return
|
|
229
|
+
}
|
|
230
|
+
moved = true
|
|
231
|
+
tracking = true
|
|
232
|
+
const next = fromPointer(event.clientX, event.clientY, 'drag')
|
|
233
|
+
if (next !== undefined) onselect?.(next)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const handlePointerup = (event: PointerEvent) => {
|
|
237
|
+
if (disabled || pointerId !== event.pointerId) return
|
|
238
|
+
capture(event.pointerId, false)
|
|
239
|
+
const next = fromPointer(event.clientX, event.clientY, moved ? 'drag' : 'tap')
|
|
240
|
+
if (next !== undefined) onselect?.(next)
|
|
241
|
+
pointerId = undefined
|
|
242
|
+
origin = undefined
|
|
243
|
+
tracking = false
|
|
244
|
+
moved = false
|
|
245
|
+
announce()
|
|
246
|
+
onselectionend?.('pointer')
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const handlePointercancel = (event: PointerEvent) => {
|
|
250
|
+
if (pointerId !== event.pointerId) return
|
|
251
|
+
capture(event.pointerId, false)
|
|
252
|
+
pointerId = undefined
|
|
253
|
+
origin = undefined
|
|
254
|
+
tracking = false
|
|
255
|
+
moved = false
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const focusOption = async (index: number) => {
|
|
259
|
+
const target = options[index]
|
|
260
|
+
if (!target) return
|
|
261
|
+
onselect?.(target.minutes)
|
|
262
|
+
announcement = formatMinutes(target.minutes, locale, hour12)
|
|
263
|
+
// The buttons are keyed by their minute, so let the list settle before reaching for one.
|
|
264
|
+
await tick()
|
|
265
|
+
element
|
|
266
|
+
?.querySelector<HTMLElement>(`.np-clock-dial-option[data-minutes="${target.minutes}"]`)
|
|
267
|
+
?.focus()
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const stepBy = (delta: number) => {
|
|
271
|
+
const from = tabStopIndex
|
|
272
|
+
const count = options.length
|
|
273
|
+
focusOption((((from + delta) % count) + count) % count)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const handleKeydown = (event: KeyboardEvent) => {
|
|
277
|
+
if (disabled) return
|
|
278
|
+
const keys: Record<string, () => void> = {
|
|
279
|
+
ArrowRight: () => stepBy(1),
|
|
280
|
+
ArrowUp: () => stepBy(1),
|
|
281
|
+
ArrowLeft: () => stepBy(-1),
|
|
282
|
+
ArrowDown: () => stepBy(-1),
|
|
283
|
+
PageUp: () => stepBy(5),
|
|
284
|
+
PageDown: () => stepBy(-5),
|
|
285
|
+
Home: () => focusOption(0),
|
|
286
|
+
End: () => focusOption(options.length - 1),
|
|
287
|
+
}
|
|
288
|
+
const action = keys[event.key]
|
|
289
|
+
if (!action) return
|
|
290
|
+
event.preventDefault()
|
|
291
|
+
action()
|
|
292
|
+
}
|
|
293
|
+
</script>
|
|
294
|
+
|
|
295
|
+
<div
|
|
296
|
+
{...attributes}
|
|
297
|
+
bind:this={element}
|
|
298
|
+
role="listbox"
|
|
299
|
+
aria-label={selection === 'hour' ? selectHourLabel : selectMinuteLabel}
|
|
300
|
+
aria-disabled={disabled || undefined}
|
|
301
|
+
class={['np-clock-dial', tracking && 'np-tracking', disabled && 'np-disabled', attributes.class]}
|
|
302
|
+
onpointerdown={handlePointerdown}
|
|
303
|
+
onpointermove={handlePointermove}
|
|
304
|
+
onpointerup={handlePointerup}
|
|
305
|
+
onpointercancel={handlePointercancel}
|
|
306
|
+
onkeydown={handleKeydown}
|
|
307
|
+
>
|
|
308
|
+
<div class="np-clock-dial-selector" style="rotate: {rotation}deg">
|
|
309
|
+
<div class="np-clock-dial-track" style="--_r: {radiusOf(handleInner)}"></div>
|
|
310
|
+
<div
|
|
311
|
+
bind:this={handleElement}
|
|
312
|
+
class="np-clock-dial-handle"
|
|
313
|
+
style="--_r: {radiusOf(handleInner)}"
|
|
314
|
+
></div>
|
|
315
|
+
</div>
|
|
316
|
+
<div class="np-clock-dial-center"></div>
|
|
317
|
+
|
|
318
|
+
{#each options as option, index (option.minutes)}
|
|
319
|
+
{@const angle = (angleOf(option.position, option.positions) * Math.PI) / 180}
|
|
320
|
+
{@const radius = radiusOf(option.inner)}
|
|
321
|
+
<button
|
|
322
|
+
type="button"
|
|
323
|
+
role="option"
|
|
324
|
+
{disabled}
|
|
325
|
+
data-minutes={option.minutes}
|
|
326
|
+
aria-selected={index === selectedIndex}
|
|
327
|
+
aria-disabled={option.disabled || undefined}
|
|
328
|
+
aria-label={option.accessibleLabel}
|
|
329
|
+
tabindex={tabStopIndex === index ? 0 : -1}
|
|
330
|
+
class={[
|
|
331
|
+
'np-clock-dial-option',
|
|
332
|
+
option.minutes === value && 'selected',
|
|
333
|
+
!option.label && 'np-unlabelled',
|
|
334
|
+
]}
|
|
335
|
+
style="--_x: calc({radius} * {Math.cos(angle).toFixed(5)}); --_y: calc({radius} * {Math.sin(
|
|
336
|
+
angle,
|
|
337
|
+
).toFixed(5)})"
|
|
338
|
+
onclick={() => {
|
|
339
|
+
if (option.disabled) return
|
|
340
|
+
onselect?.(option.minutes)
|
|
341
|
+
announcement = formatMinutes(option.minutes, locale, hour12)
|
|
342
|
+
onselectionend?.('keyboard')
|
|
343
|
+
}}
|
|
344
|
+
>
|
|
345
|
+
<span aria-hidden="true">{option.label ?? ''}</span>
|
|
346
|
+
</button>
|
|
347
|
+
{/each}
|
|
348
|
+
|
|
349
|
+
<span class="np-clock-dial-sr-only" aria-live="polite">{announcement}</span>
|
|
350
|
+
</div>
|
|
351
|
+
|
|
352
|
+
<style>
|
|
353
|
+
.np-clock-dial {
|
|
354
|
+
--_size: var(--np-time-picker-clock-dial-size, 16rem);
|
|
355
|
+
--_handle: 3rem;
|
|
356
|
+
position: relative;
|
|
357
|
+
box-sizing: border-box;
|
|
358
|
+
width: var(--_size);
|
|
359
|
+
height: var(--_size);
|
|
360
|
+
flex: none;
|
|
361
|
+
border-radius: var(--np-time-picker-clock-dial-container-shape, var(--np-shape-corner-full));
|
|
362
|
+
background-color: var(
|
|
363
|
+
--np-time-picker-clock-dial-container-color,
|
|
364
|
+
var(--np-color-surface-container-highest)
|
|
365
|
+
);
|
|
366
|
+
touch-action: none;
|
|
367
|
+
-webkit-tap-highlight-color: transparent;
|
|
368
|
+
cursor: pointer;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.np-clock-dial:focus-visible {
|
|
372
|
+
outline: none;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
.np-clock-dial.np-disabled {
|
|
376
|
+
cursor: default;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
.np-clock-dial-center,
|
|
380
|
+
.np-clock-dial-handle,
|
|
381
|
+
.np-clock-dial-track {
|
|
382
|
+
background-color: var(--np-time-picker-clock-dial-selector-color, var(--np-color-primary));
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.np-clock-dial-center {
|
|
386
|
+
position: absolute;
|
|
387
|
+
left: 50%;
|
|
388
|
+
top: 50%;
|
|
389
|
+
width: 0.5rem;
|
|
390
|
+
height: 0.5rem;
|
|
391
|
+
margin: -0.25rem 0 0 -0.25rem;
|
|
392
|
+
border-radius: var(--np-shape-corner-full);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/*
|
|
396
|
+
* A point at the centre rather than a box over the whole dial: rotating a 256dp box would push its
|
|
397
|
+
* corners some 47px past every edge and make the panel scroll, even though nothing is drawn there.
|
|
398
|
+
*/
|
|
399
|
+
.np-clock-dial-selector {
|
|
400
|
+
position: absolute;
|
|
401
|
+
left: 50%;
|
|
402
|
+
top: 50%;
|
|
403
|
+
width: 0;
|
|
404
|
+
height: 0;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
.np-clock-dial-track {
|
|
408
|
+
position: absolute;
|
|
409
|
+
left: 0;
|
|
410
|
+
top: 0;
|
|
411
|
+
height: 0.125rem;
|
|
412
|
+
width: var(--_r);
|
|
413
|
+
transform-origin: 0 50%;
|
|
414
|
+
translate: 0 -0.0625rem;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
.np-clock-dial-handle {
|
|
418
|
+
position: absolute;
|
|
419
|
+
left: 0;
|
|
420
|
+
top: 0;
|
|
421
|
+
width: var(--_handle);
|
|
422
|
+
height: var(--_handle);
|
|
423
|
+
margin: calc(var(--_handle) / -2) 0 0 calc(var(--_handle) / -2);
|
|
424
|
+
border-radius: var(--np-shape-corner-full);
|
|
425
|
+
translate: var(--_r) 0;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
.np-clock-dial-option {
|
|
429
|
+
font: inherit;
|
|
430
|
+
position: absolute;
|
|
431
|
+
left: 50%;
|
|
432
|
+
top: 50%;
|
|
433
|
+
display: flex;
|
|
434
|
+
align-items: center;
|
|
435
|
+
justify-content: center;
|
|
436
|
+
box-sizing: border-box;
|
|
437
|
+
width: var(--_handle);
|
|
438
|
+
height: var(--_handle);
|
|
439
|
+
margin: calc(var(--_handle) / -2) 0 0 calc(var(--_handle) / -2);
|
|
440
|
+
padding: 0;
|
|
441
|
+
border: 0;
|
|
442
|
+
border-radius: var(--np-shape-corner-full);
|
|
443
|
+
background: none;
|
|
444
|
+
translate: var(--_x) var(--_y);
|
|
445
|
+
font-size: 1rem;
|
|
446
|
+
line-height: 1.5rem;
|
|
447
|
+
font-variant-numeric: tabular-nums;
|
|
448
|
+
color: var(--np-time-picker-clock-dial-label-color, var(--np-color-on-surface));
|
|
449
|
+
/* The dial itself reads every pointer position, so the numbers only exist for the keyboard. */
|
|
450
|
+
pointer-events: none;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
.np-clock-dial-option.np-unlabelled {
|
|
454
|
+
width: 1.5rem;
|
|
455
|
+
height: 1.5rem;
|
|
456
|
+
margin: -0.75rem 0 0 -0.75rem;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
.np-clock-dial-option.selected {
|
|
460
|
+
color: var(--np-time-picker-clock-dial-selected-label-color, var(--np-color-on-primary));
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
.np-clock-dial-option[aria-disabled='true'] {
|
|
464
|
+
color: color-mix(in srgb, var(--np-color-on-surface) 38%, transparent);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
.np-clock-dial-option:focus-visible {
|
|
468
|
+
outline: 3px solid var(--np-color-secondary);
|
|
469
|
+
outline-offset: 2px;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
.np-clock-dial-sr-only {
|
|
473
|
+
position: absolute;
|
|
474
|
+
width: 1px;
|
|
475
|
+
height: 1px;
|
|
476
|
+
margin: -1px;
|
|
477
|
+
padding: 0;
|
|
478
|
+
overflow: hidden;
|
|
479
|
+
clip-path: inset(50%);
|
|
480
|
+
white-space: nowrap;
|
|
481
|
+
border: 0;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
485
|
+
.np-clock-dial-selector {
|
|
486
|
+
transition: rotate var(--np-motion-expressive-default-spatial);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
.np-clock-dial-track {
|
|
490
|
+
transition: width var(--np-motion-expressive-default-spatial);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
.np-clock-dial-handle {
|
|
494
|
+
transition: translate var(--np-motion-expressive-default-spatial);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
.np-clock-dial-option {
|
|
498
|
+
transition: color var(--np-motion-expressive-fast-effects);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/* While a finger is down the handle has to follow it exactly, so nothing may lag behind. */
|
|
503
|
+
.np-clock-dial.np-tracking .np-clock-dial-selector,
|
|
504
|
+
.np-clock-dial.np-tracking .np-clock-dial-track,
|
|
505
|
+
.np-clock-dial.np-tracking .np-clock-dial-handle {
|
|
506
|
+
transition: none;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
@media (forced-colors: active) {
|
|
510
|
+
.np-clock-dial {
|
|
511
|
+
border: 1px solid CanvasText;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
.np-clock-dial-center,
|
|
515
|
+
.np-clock-dial-handle,
|
|
516
|
+
.np-clock-dial-track {
|
|
517
|
+
background-color: Highlight;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
.np-clock-dial-option.selected {
|
|
521
|
+
color: HighlightText;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
.np-clock-dial-option[aria-disabled='true'] {
|
|
525
|
+
color: GrayText;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
</style>
|