@inkra/react 0.9.0 → 0.11.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.
Files changed (44) hide show
  1. package/README.md +75 -5
  2. package/THIRD_PARTY_NOTICES.md +9 -0
  3. package/Xiaolai-OFL.txt +94 -0
  4. package/dist/PencilText.js +4 -1
  5. package/dist/assets/ui-hand-xiaolai-extended.woff2 +0 -0
  6. package/dist/assets/ui-hand-xiaolai.woff2 +0 -0
  7. package/dist/assets/xiaolai.css +17 -0
  8. package/dist/form/EditablePencilText.js +4 -2
  9. package/dist/i18n/catalog.d.ts +1 -1
  10. package/dist/i18n/catalog.js +2 -0
  11. package/dist/internal/surfaceRenderer.d.ts +4 -4
  12. package/dist/internal/surfaceRenderer.js +2 -2
  13. package/dist/internal/textRenderer.d.ts +1 -1
  14. package/dist/internal/textRenderer.js +23 -11
  15. package/dist/log/index.js +7 -6
  16. package/dist/number-ticker/MorphValue.d.ts +7 -0
  17. package/dist/number-ticker/MorphValue.js +10 -0
  18. package/dist/number-ticker/index.d.ts +1 -1
  19. package/dist/number-ticker/index.js +65 -11
  20. package/dist/number-ticker/morph-motion.d.ts +46 -0
  21. package/dist/number-ticker/morph-motion.js +121 -0
  22. package/dist/number-ticker/morph.d.ts +7 -0
  23. package/dist/number-ticker/morph.js +78 -0
  24. package/dist/selection/CircularSwitchVisual.d.ts +8 -0
  25. package/dist/selection/CircularSwitchVisual.js +82 -0
  26. package/dist/selection/Switch.d.ts +7 -0
  27. package/dist/selection/Switch.js +15 -3
  28. package/dist/slider/circular.d.ts +35 -0
  29. package/dist/slider/circular.js +196 -0
  30. package/dist/slider/circularMath.d.ts +10 -0
  31. package/dist/slider/circularMath.js +34 -0
  32. package/dist/slider/index.d.ts +5 -0
  33. package/dist/slider/index.js +5 -3
  34. package/dist/styles/base.css +4 -2
  35. package/dist/styles/breadcrumb.css +1 -1
  36. package/dist/styles/log.css +5 -18
  37. package/dist/styles/markdown.css +1 -1
  38. package/dist/styles/number-ticker.css +42 -1
  39. package/dist/styles/selection.css +50 -0
  40. package/dist/styles/slider.css +65 -0
  41. package/dist/styles/slot.css +1 -1
  42. package/dist/styles.css +169 -24
  43. package/dist/tokens.css +13 -10
  44. package/package.json +3 -2
@@ -0,0 +1,196 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { forwardRef, useEffect, useId, useImperativeHandle, useRef, useState } from 'react';
3
+ import { PencilSurface } from '../PencilSurface.js';
4
+ import { PencilText } from '../PencilText.js';
5
+ import { ProgressRing } from '../display/ProgressRing.js';
6
+ import { advanceArcPosition, initialArcPosition, normalizeAngle, snapCircularValue, stepCircularValue } from './circularMath.js';
7
+ const diameters = { sm: 128, md: 160, lg: 192 };
8
+ const thicknesses = { sm: 5, md: 6, lg: 8 };
9
+ const thumbSizes = { sm: 20, md: 24, lg: 28 };
10
+ export const CircularSlider = forwardRef(function CircularSlider({ value, defaultValue, onValueChange, onValueCommit, min: suppliedMin = 0, max: suppliedMax = 100, step: suppliedStep = 1, disabled = false, readOnly = false, name, form, size = 'md', diameter, startAngle = 225, sweepAngle = 270, clockwise = true, showValue = true, centerIcon, centerContent, getValueLabel, seed, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, 'aria-valuetext': ariaValueText, className, style, tabIndex, onPointerDown, onPointerMove, onPointerUp, onPointerCancel, onLostPointerCapture, onKeyDown, ...props }, forwardedRef) {
11
+ const id = useId();
12
+ const rootRef = useRef(null);
13
+ const inputRef = useRef(null);
14
+ const gesture = useRef(null);
15
+ useImperativeHandle(forwardedRef, () => rootRef.current);
16
+ const min = Number.isFinite(suppliedMin) ? suppliedMin : 0;
17
+ const max = Number.isFinite(suppliedMax) && Number.isFinite(suppliedMax - min) ? Math.max(min, suppliedMax) : min;
18
+ const step = Number.isFinite(suppliedStep) && suppliedStep > 0 ? suppliedStep : 1;
19
+ const initialValue = useRef(defaultValue ?? min);
20
+ const [uncontrolledValue, setUncontrolledValue] = useState(initialValue.current);
21
+ const controlled = value !== undefined;
22
+ const current = snapCircularValue(value ?? uncontrolledValue, min, max, step);
23
+ const latest = useRef({ current, controlled, min, max, step });
24
+ latest.current = { current, controlled, min, max, step };
25
+ const blocked = disabled || readOnly || min === max;
26
+ const resolvedDiameter = Math.min(512, Math.max(96, Number.isFinite(diameter) ? diameter : diameters[size]));
27
+ const start = normalizeAngle(Number.isFinite(startAngle) ? startAngle : 225);
28
+ const sweep = Math.min(360, Math.max(1, Number.isFinite(sweepAngle) ? sweepAngle : 270));
29
+ const ringDiameter = resolvedDiameter - 32;
30
+ // Explicitly tiny diameters scale visual weight rather than consuming the center slot.
31
+ const thickness = Math.min(thicknesses[size], 5 + (resolvedDiameter - 96) / 4);
32
+ const thumbSize = Math.min(thumbSizes[size], 16 + (resolvedDiameter - 96) / 2);
33
+ const radius = ringDiameter / 2 - thickness - 4;
34
+ const progress = max > min ? (current - min) / (max - min) : 0;
35
+ const angle = (start + progress * sweep * (clockwise ? 1 : -1)) * Math.PI / 180;
36
+ const label = getValueLabel?.(current) ?? String(current);
37
+ const content = centerContent === undefined ? (showValue ? label : null) : typeof centerContent === 'function' ? centerContent(current) : centerContent;
38
+ const hasCenterIcon = centerIcon != null && typeof centerIcon !== 'boolean';
39
+ const hasCenterContent = content != null && typeof content !== 'boolean';
40
+ // Compact icon/value stacks need more vertical room than an inscribed square.
41
+ const centerWidth = Math.max(16, (radius - thickness - 4) * 2) + 10;
42
+ const centerHeight = Math.max(20, (radius - thickness - 2) * 2);
43
+ const combinedCenter = hasCenterIcon && hasCenterContent;
44
+ const requestedIconSize = Math.min(24, Math.max(12, 16 + (resolvedDiameter - 104) / 4));
45
+ // Account for gap + padding, reserving a complete value line at every supported size.
46
+ const centerIconSize = combinedCenter ? Math.min(requestedIconSize, centerHeight - 20) : 24;
47
+ const centerLineHeight = Math.min(resolvedDiameter <= 112 ? 18 : 24, centerHeight - centerIconSize - 6);
48
+ const centerFontSize = centerLineHeight < 18 ? '12px' : centerLineHeight < 22 ? '14px' : 'var(--inkra-text-lg)';
49
+ const resolvedSeed = seed ?? `inkra-circular-slider-${id}`;
50
+ function releaseCapture(pointerId) {
51
+ const root = rootRef.current;
52
+ if (root?.hasPointerCapture?.(pointerId))
53
+ root.releasePointerCapture(pointerId);
54
+ }
55
+ useEffect(() => {
56
+ if (!blocked || !gesture.current)
57
+ return;
58
+ const pointerId = gesture.current.id;
59
+ gesture.current = null;
60
+ releaseCapture(pointerId);
61
+ }, [blocked]);
62
+ useEffect(() => () => {
63
+ const active = gesture.current;
64
+ gesture.current = null;
65
+ if (active)
66
+ releaseCapture(active.id);
67
+ }, []);
68
+ useEffect(() => {
69
+ const document = inputRef.current?.ownerDocument;
70
+ if (!document)
71
+ return;
72
+ let timer;
73
+ const reset = (event) => {
74
+ // Resolve ownership at dispatch time, including forms moved or replaced after mount.
75
+ if (event.target !== inputRef.current?.form)
76
+ return;
77
+ clearTimeout(timer);
78
+ timer = setTimeout(() => {
79
+ if (event.defaultPrevented)
80
+ return;
81
+ const active = gesture.current;
82
+ gesture.current = null;
83
+ if (active)
84
+ releaseCapture(active.id);
85
+ const state = latest.current;
86
+ const next = state.controlled ? state.current : snapCircularValue(initialValue.current, state.min, state.max, state.step);
87
+ if (!state.controlled)
88
+ setUncontrolledValue(next);
89
+ if (inputRef.current)
90
+ inputRef.current.value = String(next);
91
+ }, 0);
92
+ };
93
+ document.addEventListener('reset', reset, true);
94
+ return () => { clearTimeout(timer); document.removeEventListener('reset', reset, true); };
95
+ }, []);
96
+ function propose(next) {
97
+ if (!controlled)
98
+ setUncontrolledValue(next);
99
+ onValueChange?.(next);
100
+ }
101
+ function pointerPosition(clientX, clientY) {
102
+ const rect = rootRef.current?.getBoundingClientRect();
103
+ if (!rect || rect.width === 0 || rect.height === 0)
104
+ return null;
105
+ const x = (clientX - rect.left) / rect.width - 0.5;
106
+ const y = (clientY - rect.top) / rect.height - 0.5;
107
+ return { angle: normalizeAngle(Math.atan2(x, -y) * 180 / Math.PI), distance: Math.hypot(x, y) * resolvedDiameter };
108
+ }
109
+ function movePointer(clientX, clientY) {
110
+ const active = gesture.current;
111
+ const point = pointerPosition(clientX, clientY);
112
+ if (!active || !point || point.distance < Math.max(8, radius * 0.25))
113
+ return;
114
+ active.position = advanceArcPosition(active.position, active.angle, point.angle, sweep, clockwise);
115
+ active.angle = point.angle;
116
+ const next = snapCircularValue(min + active.position / sweep * (max - min), min, max, step);
117
+ if (next === active.value)
118
+ return;
119
+ active.value = next;
120
+ active.changed = true;
121
+ propose(next);
122
+ }
123
+ return _jsxs("div", { ...props, ref: rootRef, role: "slider", className: ['inkra-circular-slider', className].filter(Boolean).join(' '), "data-size": size, "data-disabled": disabled ? '' : undefined, "data-readonly": readOnly ? '' : undefined, "aria-label": ariaLabel ?? (ariaLabelledBy ? undefined : 'Value'), "aria-labelledby": ariaLabelledBy, "aria-valuemin": min, "aria-valuemax": max, "aria-valuenow": current, "aria-valuetext": getValueLabel ? label : ariaValueText, "aria-disabled": disabled || undefined, "aria-readonly": readOnly || undefined, tabIndex: disabled ? -1 : tabIndex ?? 0, style: { '--inkra-circular-slider-diameter': `${resolvedDiameter}px`, '--inkra-circular-slider-thumb-size': `${thumbSize}px`, '--inkra-circular-slider-center-width': `${centerWidth}px`, '--inkra-circular-slider-center-height': `${centerHeight + 6}px`, '--inkra-circular-slider-center-icon-size': `${centerIconSize}px`, '--inkra-circular-slider-center-font-size': combinedCenter ? centerFontSize : 'var(--inkra-text-lg)', '--inkra-circular-slider-center-line-height': combinedCenter ? `${centerLineHeight}px` : 'var(--inkra-line-ui)', ...style }, onPointerDown: event => {
124
+ onPointerDown?.(event);
125
+ if (event.defaultPrevented || blocked || gesture.current || event.button !== 0 || event.isPrimary === false)
126
+ return;
127
+ const point = pointerPosition(event.clientX, event.clientY);
128
+ if (!point || point.distance < Math.max(8, radius - 22) || point.distance > radius + 22)
129
+ return;
130
+ event.preventDefault();
131
+ event.currentTarget.focus({ preventScroll: true });
132
+ const thumbAngle = start + progress * sweep * (clockwise ? 1 : -1);
133
+ const delta = (point.angle - thumbAngle) * Math.PI / 180;
134
+ const thumbDistance = Math.sqrt(Math.max(0, point.distance ** 2 + radius ** 2 - 2 * point.distance * radius * Math.cos(delta)));
135
+ // The whole 44px thumb target grabs without changing the current value.
136
+ const position = thumbDistance <= 22 ? progress * sweep : initialArcPosition(point.angle, start, sweep, clockwise, progress * sweep);
137
+ const next = snapCircularValue(min + position / sweep * (max - min), min, max, step);
138
+ gesture.current = { id: event.pointerId, angle: point.angle, position, value: next, changed: next !== current };
139
+ try {
140
+ event.currentTarget.setPointerCapture(event.pointerId);
141
+ }
142
+ catch { /* Pointer capture is unavailable in some synthetic event environments. */ }
143
+ if (next !== current)
144
+ propose(next);
145
+ }, onPointerMove: event => {
146
+ onPointerMove?.(event);
147
+ if (event.defaultPrevented || blocked || event.pointerId !== gesture.current?.id)
148
+ return;
149
+ event.preventDefault();
150
+ movePointer(event.clientX, event.clientY);
151
+ }, onPointerUp: event => {
152
+ onPointerUp?.(event);
153
+ const active = gesture.current;
154
+ if (!active || active.id !== event.pointerId)
155
+ return;
156
+ if (!event.defaultPrevented && !blocked)
157
+ movePointer(event.clientX, event.clientY);
158
+ gesture.current = null;
159
+ releaseCapture(event.pointerId);
160
+ if (!event.defaultPrevented && !blocked && active.changed)
161
+ onValueCommit?.(active.value);
162
+ }, onPointerCancel: event => {
163
+ onPointerCancel?.(event);
164
+ if (event.pointerId !== gesture.current?.id)
165
+ return;
166
+ gesture.current = null;
167
+ releaseCapture(event.pointerId);
168
+ }, onLostPointerCapture: event => {
169
+ onLostPointerCapture?.(event);
170
+ if (event.pointerId === gesture.current?.id)
171
+ gesture.current = null;
172
+ }, onKeyDown: event => {
173
+ onKeyDown?.(event);
174
+ if (event.defaultPrevented || blocked || event.altKey || event.ctrlKey || event.metaKey)
175
+ return;
176
+ let next;
177
+ if (event.key === 'Home')
178
+ next = min;
179
+ else if (event.key === 'End')
180
+ next = max;
181
+ else if (['ArrowRight', 'ArrowUp', 'ArrowLeft', 'ArrowDown', 'PageUp', 'PageDown'].includes(event.key)) {
182
+ const sign = ['ArrowRight', 'ArrowUp', 'PageUp'].includes(event.key) ? 1 : -1;
183
+ const count = event.key.startsWith('Page') || event.shiftKey ? 10 : 1;
184
+ next = stepCircularValue(current, sign * count, min, max, step);
185
+ }
186
+ else
187
+ return;
188
+ event.preventDefault();
189
+ if (next !== current) {
190
+ propose(next);
191
+ onValueCommit?.(next);
192
+ }
193
+ }, children: [_jsxs("span", { className: "inkra-circular-slider__arc", "aria-hidden": "true", style: { transform: `rotate(${start}deg) scaleX(${clockwise ? 1 : -1})` }, children: [_jsx("span", { className: "inkra-circular-slider__track", children: _jsx(ProgressRing, { diameter: ringDiameter, thickness: thickness, percent: sweep / 3.6, seed: `${resolvedSeed}-track` }) }), _jsx("span", { className: "inkra-circular-slider__range", children: _jsx(ProgressRing, { diameter: ringDiameter, thickness: thickness, percent: progress * sweep / 3.6, seed: `${resolvedSeed}-range` }) })] }), (hasCenterIcon || hasCenterContent) && _jsxs("div", { className: "inkra-circular-slider__center", "aria-hidden": "true", inert: true, children: [hasCenterIcon && _jsx("div", { className: "inkra-circular-slider__center-icon", children: centerIcon }), hasCenterContent && _jsx("div", { className: "inkra-circular-slider__center-content", children: typeof content === 'string' || typeof content === 'number'
194
+ ? _jsx(PencilText, { className: "inkra-circular-slider__value", ellipsis: centerContent !== undefined, children: String(content) })
195
+ : content })] }), _jsx("span", { "aria-hidden": "true", className: "inkra-circular-slider__thumb", style: { left: `${50 + Math.sin(angle) * radius / resolvedDiameter * 100}%`, top: `${50 - Math.cos(angle) * radius / resolvedDiameter * 100}%` }, children: _jsx(PencilSurface, { as: "span", className: "inkra-circular-slider__thumb-surface", variant: "paper", radius: thumbSize / 2, seed: `${resolvedSeed}-thumb`, children: _jsx(PencilSurface, { as: "span", className: "inkra-circular-slider__focus-ring", variant: "outline", radius: thumbSize / 2 + 4, seed: `${resolvedSeed}-focus` }) }) }), _jsx("input", { ref: inputRef, type: "hidden", name: name, form: form, value: current, disabled: disabled })] });
196
+ });
@@ -0,0 +1,10 @@
1
+ /** Clock angles: 0 points up and positive angles turn clockwise. */
2
+ export declare const normalizeAngle: (angle: number) => number;
3
+ export declare const clampCircularValue: (value: number, min: number, max: number) => number;
4
+ /** Snap to the grid anchored at min, retaining max as an attainable final stop. */
5
+ export declare function snapCircularValue(value: number, min: number, max: number, step: number): number;
6
+ export declare function stepCircularValue(value: number, steps: number, min: number, max: number, step: number): number;
7
+ /** Map an initial track press to an arc, selecting the closest endpoint in its gap. */
8
+ export declare function initialArcPosition(angle: number, start: number, sweep: number, clockwise: boolean, current: number): number;
9
+ /** Unwrap small consecutive movements; crossing the seam never resets to zero. */
10
+ export declare function advanceArcPosition(position: number, previousAngle: number, angle: number, sweep: number, clockwise: boolean): number;
@@ -0,0 +1,34 @@
1
+ /** Clock angles: 0 points up and positive angles turn clockwise. */
2
+ export const normalizeAngle = (angle) => ((angle % 360) + 360) % 360;
3
+ export const clampCircularValue = (value, min, max) => Math.min(max, Math.max(min, value));
4
+ function cleanNumber(value) {
5
+ return Number(value.toPrecision(14));
6
+ }
7
+ /** Snap to the grid anchored at min, retaining max as an attainable final stop. */
8
+ export function snapCircularValue(value, min, max, step) {
9
+ if (!Number.isFinite(value))
10
+ return min;
11
+ const bounded = clampCircularValue(value, min, max);
12
+ const snapped = clampCircularValue(cleanNumber(min + Math.round((bounded - min) / step) * step), min, max);
13
+ return max - bounded < Math.abs(snapped - bounded) ? max : snapped;
14
+ }
15
+ export function stepCircularValue(value, steps, min, max, step) {
16
+ const position = (value - min) / step;
17
+ const index = steps > 0 ? Math.floor(position + 1e-9) : Math.ceil(position - 1e-9);
18
+ return clampCircularValue(cleanNumber(min + (index + steps) * step), min, max);
19
+ }
20
+ /** Map an initial track press to an arc, selecting the closest endpoint in its gap. */
21
+ export function initialArcPosition(angle, start, sweep, clockwise, current) {
22
+ const position = normalizeAngle((angle - start) * (clockwise ? 1 : -1));
23
+ if (sweep < 360)
24
+ return position <= sweep ? position : position - sweep < 360 - position ? sweep : 0;
25
+ // Only the exact seam represents two values; other track presses are absolute.
26
+ if (position < 1e-7 || 360 - position < 1e-7)
27
+ return current > 180 ? 360 : 0;
28
+ return position;
29
+ }
30
+ /** Unwrap small consecutive movements; crossing the seam never resets to zero. */
31
+ export function advanceArcPosition(position, previousAngle, angle, sweep, clockwise) {
32
+ const delta = normalizeAngle(angle - previousAngle + 180) - 180;
33
+ return clampCircularValue(position + delta * (clockwise ? 1 : -1), 0, sweep);
34
+ }
@@ -1,11 +1,16 @@
1
1
  import * as SliderPrimitive from '@radix-ui/react-slider';
2
2
  export type SliderSize = 'sm' | 'md' | 'lg';
3
3
  export type SliderVariant = 'standard' | 'inset';
4
+ export type SliderThumbAlignment = 'edge' | 'contain';
4
5
  export interface SliderProps extends Omit<SliderPrimitive.SliderProps, 'children' | 'asChild'> {
5
6
  size?: SliderSize;
6
7
  variant?: SliderVariant;
8
+ /** Align thumb centers with track endpoints, or retain the inset hit-target bounds. */
9
+ thumbAlignment?: SliderThumbAlignment;
7
10
  seed?: string | number;
8
11
  thumbLabels?: readonly string[];
9
12
  getValueLabel?: (value: number, index: number) => string;
10
13
  }
11
14
  export declare const Slider: import("react").ForwardRefExoticComponent<SliderProps & import("react").RefAttributes<HTMLSpanElement>>;
15
+ export { CircularSlider } from './circular.js';
16
+ export type { CircularSliderProps } from './circular.js';
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import * as SliderPrimitive from '@radix-ui/react-slider';
3
3
  import { forwardRef, useEffect, useId, useImperativeHandle, useRef, useState } from 'react';
4
4
  import { PencilSurface } from '../PencilSurface.js';
5
- export const Slider = forwardRef(function Slider({ 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, 'aria-describedby': ariaDescribedBy, 'aria-valuetext': ariaValueText, className, defaultValue, dir, disabled = false, getValueLabel, inverted = false, min = 0, max = 100, name, form, onValueChange, onValueCommit, orientation = 'horizontal', seed, size = 'md', thumbLabels, value, variant = 'standard', ...props }, forwardedRef) {
5
+ export const Slider = forwardRef(function Slider({ 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, 'aria-describedby': ariaDescribedBy, 'aria-valuetext': ariaValueText, className, defaultValue, dir, disabled = false, getValueLabel, inverted = false, min = 0, max = 100, name, form, onValueChange, onValueCommit, orientation = 'horizontal', seed, size = 'md', thumbAlignment = 'edge', thumbLabels, value, variant = 'standard', ...props }, forwardedRef) {
6
6
  const generatedId = useId();
7
7
  const rootRef = useRef(null);
8
8
  useImperativeHandle(forwardedRef, () => rootRef.current);
@@ -70,16 +70,17 @@ export const Slider = forwardRef(function Slider({ 'aria-label': ariaLabel, 'ari
70
70
  const clip = {
71
71
  clipPath: orientation === 'horizontal' ? `inset(0 ${end}% 0 ${start}%)` : `inset(${start}% 0 ${end}% 0)`,
72
72
  };
73
- return _jsxs(SliderPrimitive.Root, { ...props, className: ['inkra-slider', className].filter(Boolean).join(' '), "data-size": size, "data-variant": variant, dir: resolvedDir, disabled: disabled, form: radixFormId, inverted: inverted, min: min, max: max, onValueChange: next => {
73
+ return _jsxs(SliderPrimitive.Root, { ...props, className: ['inkra-slider', className].filter(Boolean).join(' '), "data-size": size, "data-variant": variant, "data-thumb-alignment": thumbAlignment, dir: resolvedDir, disabled: disabled, form: radixFormId, inverted: inverted, min: min, max: max, onValueChange: next => {
74
74
  if (!controlled)
75
75
  setUncontrolledValue(next);
76
76
  onValueChange?.(next);
77
77
  }, onValueCommit: onValueCommit, orientation: orientation, ref: rootRef, value: values, children: [_jsxs(SliderPrimitive.Track, { className: "inkra-slider__track", children: [_jsx(PencilSurface, { as: "span", "aria-hidden": "true", className: "inkra-slider__track-surface", variant: "outline", radius: variant === 'inset' ? 8 : 3, seed: `${resolvedSeed}-track` }), _jsx("span", { "aria-hidden": "true", className: "inkra-slider__range", style: clip, children: _jsx(PencilSurface, { as: "span", className: "inkra-slider__range-surface", variant: "selection", seed: `${resolvedSeed}-range` }) })] }), values.map((current, index) => {
78
+ const position = reversed ? 100 - percentage(current) : percentage(current);
78
79
  const thumbLabel = thumbLabels?.[index];
79
80
  const labelledBy = thumbLabel ? undefined : ariaLabelledBy;
80
81
  const label = thumbLabel ?? (ariaLabel ? `${ariaLabel}${values.length > 1 ? ` ${index + 1}` : ''}` : labelledBy ? undefined : `Value${values.length > 1 ? ` ${index + 1}` : ''}`);
81
82
  const inputName = name ? values.length > 1 ? `${name}[]` : name : undefined;
82
- return _jsxs(SliderPrimitive.unstable_ThumbProvider, { name: inputName, children: [_jsx(SliderPrimitive.unstable_ThumbTrigger, { "aria-describedby": ariaDescribedBy, "aria-label": label, "aria-labelledby": labelledBy, "aria-valuenow": current, "aria-valuetext": getValueLabel?.(current, index) ?? ariaValueText, className: "inkra-slider__thumb", children: _jsx(PencilSurface, { as: "span", "aria-hidden": "true", className: "inkra-slider__thumb-surface", variant: "paper", radius: 8, seed: `${resolvedSeed}-thumb-${index}`, children: _jsx(PencilSurface, { as: "span", className: "inkra-slider__focus-ring", variant: "outline", radius: 8, seed: `${resolvedSeed}-focus-${index}` }) }) }), _jsx(SliderPrimitive.unstable_BubbleInput, { disabled: disabled, form: form, name: inputName, ref: input => {
83
+ return _jsxs(SliderPrimitive.unstable_ThumbProvider, { name: inputName, children: [_jsx(SliderPrimitive.unstable_ThumbTrigger, { "aria-describedby": ariaDescribedBy, "aria-label": label, "aria-labelledby": labelledBy, "aria-valuenow": current, "aria-valuetext": getValueLabel?.(current, index) ?? ariaValueText, className: "inkra-slider__thumb", style: { '--inkra-slider-thumb-offset': `${position - 50}%` }, children: _jsx(PencilSurface, { as: "span", "aria-hidden": "true", className: "inkra-slider__thumb-surface", variant: "paper", radius: 8, seed: `${resolvedSeed}-thumb-${index}`, children: _jsx(PencilSurface, { as: "span", className: "inkra-slider__focus-ring", variant: "outline", radius: 8, seed: `${resolvedSeed}-focus-${index}` }) }) }), _jsx(SliderPrimitive.unstable_BubbleInput, { disabled: disabled, form: form, name: inputName, ref: input => {
83
84
  if (input)
84
85
  formInputRefs.current.set(index, input);
85
86
  else
@@ -87,3 +88,4 @@ export const Slider = forwardRef(function Slider({ 'aria-label': ariaLabel, 'ari
87
88
  } })] }, index);
88
89
  })] });
89
90
  });
91
+ export { CircularSlider } from './circular.js';
@@ -1,5 +1,7 @@
1
1
  @import "../tokens.css";
2
2
 
3
+ @import "../assets/xiaolai.css";
4
+
3
5
  @font-face {
4
6
  font-family: "Trip Map Hand";
5
7
  font-style: normal;
@@ -129,13 +131,13 @@
129
131
  .inkra-pencil-text__source {
130
132
  color: inherit;
131
133
  font: inherit;
132
- font-family: "Trip Map Hand", "HanziPen SC", "Hannotate SC", "Kaiti SC", "STKaiti", cursive;
133
134
  font-kerning: none;
134
135
  font-variant-ligatures: none;
135
136
  letter-spacing: 0;
136
137
  }
137
138
 
138
139
  .inkra-pencil-text {
140
+ font-family: var(--inkra-font-hand);
139
141
  display: inline-block;
140
142
  max-width: 100%;
141
143
  min-width: 0;
@@ -216,7 +218,7 @@
216
218
  color: var(--inkra-on-accent, var(--inkra-accent-strong));
217
219
  cursor: pointer;
218
220
  display: inline-flex;
219
- font-family: "Trip Map Hand", "HanziPen SC", "Hannotate SC", "Kaiti SC", "STKaiti", cursive;
221
+ font-family: var(--inkra-font-hand);
220
222
  font-weight: var(--inkra-weight-regular);
221
223
  justify-content: center;
222
224
  letter-spacing: var(--inkra-letter-spacing);
@@ -55,7 +55,7 @@
55
55
  align-items: center;
56
56
  box-sizing: border-box;
57
57
  display: inline-flex;
58
- font-family: "Trip Map Hand", "HanziPen SC", "Hannotate SC", "Kaiti SC", "STKaiti", cursive;
58
+ font-family: var(--inkra-font-hand);
59
59
  font-size: var(--inkra-breadcrumb-item-font-size);
60
60
  gap: var(--inkra-breadcrumb-item-gap);
61
61
  justify-content: center;
@@ -13,8 +13,6 @@
13
13
  }
14
14
 
15
15
  .inkra-log[data-toolbar="true"] { grid-template-rows: auto minmax(var(--inkra-touch-target), 1fr); }
16
- .inkra-log:has(> .inkra-log__latest) { grid-template-rows: minmax(var(--inkra-touch-target), 1fr) auto; }
17
- .inkra-log[data-toolbar="true"]:has(> .inkra-log__latest) { grid-template-rows: auto minmax(var(--inkra-touch-target), 1fr) auto; }
18
16
 
19
17
  .inkra-log__toolbar {
20
18
  align-items: center;
@@ -158,21 +156,15 @@
158
156
  }
159
157
 
160
158
  .inkra-log__latest {
161
- align-items: center;
162
159
  display: flex;
163
- justify-content: flex-end;
164
- max-inline-size: 100%;
165
- min-inline-size: 0;
166
- padding: var(--inkra-space-2) var(--inkra-space-3);
167
- position: relative;
160
+ inset-block-end: clamp(0px, calc(100% - var(--inkra-touch-target)), var(--inkra-space-2));
161
+ inset-inline-end: var(--inkra-space-3);
162
+ position: absolute;
168
163
  z-index: 2;
169
164
  }
170
165
 
171
- .inkra-log__latest-divider {
172
- color: var(--inkra-border);
173
- inset-block-start: -4px;
174
- inset-inline: 0;
175
- position: absolute;
166
+ .inkra-log__latest .inkra-button__surface {
167
+ background: var(--inkra-paper);
176
168
  }
177
169
 
178
170
  @container (max-width: 36rem) {
@@ -188,11 +180,6 @@
188
180
  .inkra-log__follow-control .inkra-button__content,
189
181
  .inkra-log__follow-control .inkra-button__label,
190
182
  .inkra-log__follow-control .inkra-pencil-text__source { min-inline-size: 0; white-space: normal; overflow-wrap: anywhere; }
191
- .inkra-log__latest .inkra-button { inline-size: 100%; }
192
- .inkra-log__latest .inkra-button__surface { height: auto; inline-size: 100%; max-inline-size: 100%; min-height: var(--inkra-button-visual-height); padding-block: var(--inkra-space-2); }
193
- .inkra-log__latest .inkra-button__content,
194
- .inkra-log__latest .inkra-button__label,
195
- .inkra-log__latest .inkra-pencil-text__source { min-inline-size: 0; white-space: normal; overflow-wrap: anywhere; }
196
183
  }
197
184
 
198
185
  @media (forced-colors: active) {
@@ -232,7 +232,7 @@
232
232
  .inkra-markdown__code-surface {
233
233
  background: transparent;
234
234
  color: var(--inkra-ink);
235
- font-family: "Trip Map Hand", var(--inkra-font-hand);
235
+ font-family: var(--inkra-font-hand);
236
236
  font-size: var(--inkra-text-md);
237
237
  font-weight: var(--inkra-weight-regular);
238
238
  line-height: 1.8;
@@ -25,7 +25,7 @@
25
25
 
26
26
  .inkra-number-ticker__native-value {
27
27
  font: inherit;
28
- font-family: "Trip Map Hand", "HanziPen SC", "Hannotate SC", "Kaiti SC", "STKaiti", cursive;
28
+ font-family: var(--inkra-font-hand);
29
29
  }
30
30
 
31
31
  .inkra-number-ticker__reels {
@@ -66,7 +66,48 @@
66
66
  flex: none;
67
67
  }
68
68
 
69
+ .inkra-number-ticker[data-animation="morph"] { min-inline-size: 0; }
70
+
71
+ .inkra-number-ticker[data-animation="morph"] .inkra-number-ticker__value {
72
+ display: block;
73
+ max-inline-size: 100%;
74
+ overflow: hidden;
75
+ }
76
+
77
+ .inkra-number-ticker__morph {
78
+ position: relative;
79
+ display: inline-flex;
80
+ flex: none;
81
+ align-items: baseline;
82
+ justify-content: flex-end;
83
+ max-inline-size: 100%;
84
+ height: 1lh;
85
+ line-height: var(--inkra-line-ui, 1.45);
86
+ overflow: hidden;
87
+ contain: paint;
88
+ unicode-bidi: isolate;
89
+ }
90
+
91
+ .inkra-number-ticker__morph-glyph {
92
+ display: inline-flex;
93
+ flex: none;
94
+ justify-content: center;
95
+ align-items: center;
96
+ height: 1lh;
97
+ white-space: pre;
98
+ }
99
+
100
+ .inkra-number-ticker__morph-glyph > .inkra-pencil-text { flex: none; }
101
+
69
102
  @media (prefers-reduced-motion: reduce), print {
103
+ .inkra-number-ticker__morph { width: auto !important; }
104
+ .inkra-number-ticker__morph-glyph[data-face="from"] { display: none; }
105
+ .inkra-number-ticker__morph-glyph[data-face="to"] {
106
+ position: relative !important;
107
+ left: auto !important;
108
+ transform: none !important;
109
+ opacity: 1 !important;
110
+ }
70
111
  .inkra-number-ticker__column { grid-template-columns: 1fr !important; }
71
112
  .inkra-number-ticker__column[data-digit] { width: 1ch !important; }
72
113
  .inkra-number-ticker__column[data-departing] { display: none; }
@@ -262,3 +262,53 @@
262
262
  outline-offset: var(--inkra-focus-offset);
263
263
  }
264
264
  }
265
+
266
+ /* Circular switches retain the native checkbox while sharing the dial's pencil geometry. */
267
+ .inkra-switch--circular {
268
+ grid-template-columns: minmax(0, 1fr);
269
+ justify-items: center;
270
+ align-content: start;
271
+ width: var(--inkra-circular-switch-diameter);
272
+ gap: 2px;
273
+ }
274
+ .inkra-switch--circular > .inkra-selection-control__input {
275
+ width: var(--inkra-circular-switch-diameter);
276
+ height: var(--inkra-circular-switch-diameter);
277
+ inset: 0 auto auto 0;
278
+ touch-action: manipulation;
279
+ }
280
+ .inkra-switch--circular > .inkra-selection-control__copy { width: 100%; text-align: center; }
281
+ .inkra-switch[data-readonly="true"] { cursor: default; }
282
+ .inkra-switch__circular { display: flex; align-items: center; justify-content: center; position: relative; width: var(--inkra-circular-switch-diameter); height: var(--inkra-circular-switch-diameter); pointer-events: none; user-select: none; }
283
+ .inkra-switch__circular-arc { position: absolute; inset: 16px; transform: rotate(225deg); }
284
+ .inkra-switch__circular-track, .inkra-switch__circular-range { position: absolute; inset: 0; }
285
+ .inkra-switch__circular-track { color: var(--inkra-ink-muted); opacity: .3; }
286
+ .inkra-switch__circular-range { color: var(--inkra-accent-strong); }
287
+ .inkra-switch__circular .inkra-progress__ring { color: inherit; }
288
+ .inkra-switch__circular .inkra-progress__ring,
289
+ .inkra-switch__circular .inkra-progress__ring-rotation,
290
+ .inkra-switch__circular .inkra-progress__ring-direction,
291
+ .inkra-switch__circular .inkra-progress__ring-ink,
292
+ .inkra-switch__circular .inkra-progress__ring-fallback { display: block; position: absolute; inset: 0; width: 100%; height: 100%; }
293
+ .inkra-switch__circular .inkra-progress__ring-ink { visibility: hidden; }
294
+ .inkra-switch__circular .inkra-progress__ring-ink[data-ready="true"] { visibility: visible; }
295
+ .inkra-switch__circular .inkra-progress__ring-ink[data-ready="true"] + .inkra-progress__ring-fallback { visibility: hidden; }
296
+ .inkra-switch__circular-center { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px; padding: 5px; box-sizing: border-box; width: var(--inkra-circular-switch-center-width); max-height: var(--inkra-circular-switch-center-height); min-width: 0; overflow: hidden; pointer-events: none; text-align: center; }
297
+ .inkra-switch__circular-icon { display: flex; align-items: center; justify-content: center; flex: none; width: min(var(--inkra-circular-switch-icon-size), 100%); height: var(--inkra-circular-switch-icon-size); }
298
+ .inkra-switch__circular-icon > * { max-width: 100%; max-height: 100%; }
299
+ .inkra-switch__circular-content { width: 100%; min-width: 0; max-width: 100%; overflow-wrap: anywhere; }
300
+ .inkra-switch__circular-content > * { max-width: 100%; }
301
+ .inkra-switch__circular-value { display: block; white-space: nowrap; max-width: 100%; text-align: center; font-size: var(--inkra-circular-switch-value-size); line-height: var(--inkra-circular-switch-value-line); }
302
+ .inkra-switch__circular-focus { --inkra-ink: var(--inkra-focus); --inkra-accent-strong: var(--inkra-focus); position: absolute !important; inset: 10px; opacity: 0; pointer-events: none; border-radius: 50%; }
303
+ .inkra-switch--circular:has(.inkra-selection-control__input:focus-visible) .inkra-switch__circular-focus { opacity: 1; }
304
+ @media (forced-colors: active) {
305
+ .inkra-switch--circular > .inkra-selection-control__input { position: absolute; opacity: 0; z-index: 2; }
306
+ .inkra-switch__circular-track { color: GrayText; opacity: 1; }
307
+ .inkra-switch__circular-range { color: Highlight; }
308
+ .inkra-switch--circular:has(.inkra-selection-control__input:focus-visible) .inkra-switch__circular { outline: var(--inkra-focus-width) solid Highlight; outline-offset: -10px; border-radius: 50%; }
309
+ .inkra-switch__circular-focus { display: none !important; }
310
+ }
311
+ @media (forced-colors: active), print {
312
+ .inkra-switch__circular .inkra-progress__ring-ink[data-ready="true"] { visibility: hidden; }
313
+ .inkra-switch__circular .inkra-progress__ring-ink[data-ready="true"] + .inkra-progress__ring-fallback { visibility: visible; }
314
+ }
@@ -98,6 +98,17 @@
98
98
  width: var(--inkra-slider-thumb-size);
99
99
  }
100
100
 
101
+ /* Radix insets its measured hit target by half a thumb at each endpoint.
102
+ Translate the whole target back so its visual center follows the track;
103
+ percentages use the actual thumb box and preserve its 44px hit area. */
104
+ .inkra-slider[data-thumb-alignment="edge"] .inkra-slider__thumb {
105
+ translate: var(--inkra-slider-thumb-offset) 0;
106
+ }
107
+
108
+ .inkra-slider[data-thumb-alignment="edge"][data-orientation="vertical"] .inkra-slider__thumb {
109
+ translate: 0 var(--inkra-slider-thumb-offset);
110
+ }
111
+
101
112
  .inkra-slider__thumb:focus-visible {
102
113
  outline: none;
103
114
  }
@@ -143,3 +154,57 @@
143
154
  display: none !important;
144
155
  }
145
156
  }
157
+
158
+ .inkra-circular-slider {
159
+ box-sizing: border-box;
160
+ color: var(--inkra-ink);
161
+ cursor: pointer;
162
+ display: inline-flex;
163
+ flex: none;
164
+ align-items: center;
165
+ justify-content: center;
166
+ width: var(--inkra-circular-slider-diameter);
167
+ height: var(--inkra-circular-slider-diameter);
168
+ position: relative;
169
+ touch-action: none;
170
+ user-select: none;
171
+ vertical-align: middle;
172
+ outline: none;
173
+ }
174
+ .inkra-circular-slider[data-disabled] { cursor: not-allowed; opacity: .55; }
175
+ .inkra-circular-slider[data-readonly] { cursor: default; }
176
+ .inkra-circular-slider__arc { position: absolute; inset: 16px; pointer-events: none; }
177
+ .inkra-circular-slider__track, .inkra-circular-slider__range { position: absolute; inset: 0; }
178
+ .inkra-circular-slider__track { color: var(--inkra-ink-muted); opacity: .3; }
179
+ .inkra-circular-slider__range { color: var(--inkra-accent-strong); }
180
+ /* ProgressRing shares the stable pencil texture; these scoped rules also support slider-only styles. */
181
+ .inkra-circular-slider .inkra-progress__ring { color: inherit; }
182
+ .inkra-circular-slider .inkra-progress__ring,
183
+ .inkra-circular-slider .inkra-progress__ring-rotation,
184
+ .inkra-circular-slider .inkra-progress__ring-direction,
185
+ .inkra-circular-slider .inkra-progress__ring-ink,
186
+ .inkra-circular-slider .inkra-progress__ring-fallback { display: block; position: absolute; inset: 0; width: 100%; height: 100%; }
187
+ .inkra-circular-slider .inkra-progress__ring-ink { visibility: hidden; }
188
+ .inkra-circular-slider .inkra-progress__ring-ink[data-ready="true"] { visibility: visible; }
189
+ .inkra-circular-slider .inkra-progress__ring-ink[data-ready="true"] + .inkra-progress__ring-fallback { visibility: hidden; }
190
+ .inkra-circular-slider__center { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px; padding: 5px; box-sizing: border-box; width: var(--inkra-circular-slider-center-width); max-height: var(--inkra-circular-slider-center-height); min-width: 0; overflow: hidden; pointer-events: none; text-align: center; }
191
+ .inkra-circular-slider__center-icon { display: flex; align-items: center; justify-content: center; flex: none; width: min(var(--inkra-circular-slider-center-icon-size), 100%); height: var(--inkra-circular-slider-center-icon-size); }
192
+ .inkra-circular-slider__center-icon > * { max-width: 100%; max-height: 100%; }
193
+ .inkra-circular-slider__center-content { width: 100%; min-width: 0; max-width: 100%; max-height: var(--inkra-circular-slider-center-height); overflow-wrap: anywhere; }
194
+ .inkra-circular-slider__center-content > * { max-width: 100%; }
195
+ .inkra-circular-slider__value { display: block; white-space: nowrap; max-width: 100%; pointer-events: none; text-align: center; font-size: var(--inkra-circular-slider-center-font-size); line-height: var(--inkra-circular-slider-center-line-height); }
196
+ .inkra-circular-slider__thumb { position: absolute; transform: translate(-50%, -50%); width: var(--inkra-touch-target); height: var(--inkra-touch-target); display: flex; align-items: center; justify-content: center; pointer-events: none; }
197
+ .inkra-circular-slider__thumb-surface { display: block !important; width: var(--inkra-circular-slider-thumb-size); height: var(--inkra-circular-slider-thumb-size); border-radius: 50%; }
198
+ .inkra-circular-slider__focus-ring { --inkra-ink: var(--inkra-focus); position: absolute !important; inset: -4px; opacity: 0; pointer-events: none; }
199
+ .inkra-circular-slider:focus-visible .inkra-circular-slider__focus-ring { opacity: 1; }
200
+ @media (forced-colors: active) {
201
+ .inkra-circular-slider__track { color: GrayText; opacity: 1; }
202
+ .inkra-circular-slider__range { color: Highlight; }
203
+ .inkra-circular-slider__thumb-surface { border: 2px solid ButtonText; background: Canvas; }
204
+ .inkra-circular-slider:focus-visible .inkra-circular-slider__thumb-surface { outline: var(--inkra-focus-width) solid Highlight; outline-offset: var(--inkra-focus-offset); }
205
+ .inkra-circular-slider__focus-ring { display: none !important; }
206
+ }
207
+ @media (forced-colors: active), print {
208
+ .inkra-circular-slider .inkra-progress__ring-ink[data-ready="true"] { visibility: hidden; }
209
+ .inkra-circular-slider .inkra-progress__ring-ink[data-ready="true"] + .inkra-progress__ring-fallback { visibility: visible; }
210
+ }
@@ -64,7 +64,7 @@
64
64
 
65
65
  .inkra-slot__empty-mark {
66
66
  color: var(--inkra-ink-muted);
67
- font-family: "Trip Map Hand", "HanziPen SC", "Hannotate SC", "Kaiti SC", "STKaiti", cursive;
67
+ font-family: var(--inkra-font-hand);
68
68
  font-size: calc(var(--inkra-slot-size) * 0.4);
69
69
  line-height: 1;
70
70
  }