@braedonsaunders/appkit-ui 0.2.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 +661 -0
- package/README.md +11 -0
- package/account-menu.d.ts +49 -0
- package/account-menu.d.ts.map +1 -0
- package/account-menu.js +66 -0
- package/account-menu.js.map +1 -0
- package/alert.d.ts +13 -0
- package/alert.d.ts.map +1 -0
- package/alert.js +22 -0
- package/alert.js.map +1 -0
- package/animated-number.d.ts +13 -0
- package/animated-number.d.ts.map +1 -0
- package/animated-number.js +53 -0
- package/animated-number.js.map +1 -0
- package/api-reference.d.ts +44 -0
- package/api-reference.d.ts.map +1 -0
- package/api-reference.js +82 -0
- package/api-reference.js.map +1 -0
- package/app-shell.d.ts +39 -0
- package/app-shell.d.ts.map +1 -0
- package/app-shell.js +21 -0
- package/app-shell.js.map +1 -0
- package/app-sidebar.d.ts +22 -0
- package/app-sidebar.d.ts.map +1 -0
- package/app-sidebar.js +30 -0
- package/app-sidebar.js.map +1 -0
- package/avatar.d.ts +9 -0
- package/avatar.d.ts.map +1 -0
- package/avatar.js +18 -0
- package/avatar.js.map +1 -0
- package/badge.d.ts +9 -0
- package/badge.d.ts.map +1 -0
- package/badge.js +23 -0
- package/badge.js.map +1 -0
- package/button.d.ts +24 -0
- package/button.d.ts.map +1 -0
- package/button.js +50 -0
- package/button.js.map +1 -0
- package/card.d.ts +21 -0
- package/card.d.ts.map +1 -0
- package/card.js +36 -0
- package/card.js.map +1 -0
- package/checkbox.d.ts +4 -0
- package/checkbox.d.ts.map +1 -0
- package/checkbox.js +6 -0
- package/checkbox.js.map +1 -0
- package/confirm-dialog.d.ts +23 -0
- package/confirm-dialog.d.ts.map +1 -0
- package/confirm-dialog.js +67 -0
- package/confirm-dialog.js.map +1 -0
- package/context-menu.d.ts +47 -0
- package/context-menu.d.ts.map +1 -0
- package/context-menu.js +90 -0
- package/context-menu.js.map +1 -0
- package/dialog.d.ts +25 -0
- package/dialog.d.ts.map +1 -0
- package/dialog.js +89 -0
- package/dialog.js.map +1 -0
- package/document-title.d.ts +11 -0
- package/document-title.d.ts.map +1 -0
- package/document-title.js +17 -0
- package/document-title.js.map +1 -0
- package/drawer.d.ts +63 -0
- package/drawer.d.ts.map +1 -0
- package/drawer.js +188 -0
- package/drawer.js.map +1 -0
- package/empty-state.d.ts +10 -0
- package/empty-state.d.ts.map +1 -0
- package/empty-state.js +7 -0
- package/empty-state.js.map +1 -0
- package/file-uploader.d.ts +67 -0
- package/file-uploader.d.ts.map +1 -0
- package/file-uploader.js +156 -0
- package/file-uploader.js.map +1 -0
- package/filter-chips.d.ts +47 -0
- package/filter-chips.d.ts.map +1 -0
- package/filter-chips.js +62 -0
- package/filter-chips.js.map +1 -0
- package/global-search.d.ts +41 -0
- package/global-search.d.ts.map +1 -0
- package/global-search.js +116 -0
- package/global-search.js.map +1 -0
- package/index.d.ts +70 -0
- package/index.d.ts.map +1 -0
- package/index.js +70 -0
- package/index.js.map +1 -0
- package/input.d.ts +9 -0
- package/input.d.ts.map +1 -0
- package/input.js +11 -0
- package/input.js.map +1 -0
- package/label.d.ts +3 -0
- package/label.d.ts.map +1 -0
- package/label.js +6 -0
- package/label.js.map +1 -0
- package/line-grid.d.ts +48 -0
- package/line-grid.d.ts.map +1 -0
- package/line-grid.js +155 -0
- package/line-grid.js.map +1 -0
- package/link-context.d.ts +44 -0
- package/link-context.d.ts.map +1 -0
- package/link-context.js +32 -0
- package/link-context.js.map +1 -0
- package/list-nav.d.ts +28 -0
- package/list-nav.d.ts.map +1 -0
- package/list-nav.js +41 -0
- package/list-nav.js.map +1 -0
- package/list-pagination.d.ts +27 -0
- package/list-pagination.d.ts.map +1 -0
- package/list-pagination.js +43 -0
- package/list-pagination.js.map +1 -0
- package/list-params.d.ts +41 -0
- package/list-params.d.ts.map +1 -0
- package/list-params.js +84 -0
- package/list-params.js.map +1 -0
- package/list-search-input-state.d.ts +13 -0
- package/list-search-input-state.d.ts.map +1 -0
- package/list-search-input-state.js +27 -0
- package/list-search-input-state.js.map +1 -0
- package/list-search-input.d.ts +16 -0
- package/list-search-input.d.ts.map +1 -0
- package/list-search-input.js +51 -0
- package/list-search-input.js.map +1 -0
- package/mobile-tab-bar.d.ts +14 -0
- package/mobile-tab-bar.d.ts.map +1 -0
- package/mobile-tab-bar.js +25 -0
- package/mobile-tab-bar.js.map +1 -0
- package/multi-select.d.ts +32 -0
- package/multi-select.d.ts.map +1 -0
- package/multi-select.js +52 -0
- package/multi-select.js.map +1 -0
- package/nav-overflow.d.ts +12 -0
- package/nav-overflow.d.ts.map +1 -0
- package/nav-overflow.js +30 -0
- package/nav-overflow.js.map +1 -0
- package/navigation-config-editor.d.ts +26 -0
- package/navigation-config-editor.d.ts.map +1 -0
- package/navigation-config-editor.js +57 -0
- package/navigation-config-editor.js.map +1 -0
- package/navigation-config.d.ts +48 -0
- package/navigation-config.d.ts.map +1 -0
- package/navigation-config.js +75 -0
- package/navigation-config.js.map +1 -0
- package/navigation-mode.d.ts +16 -0
- package/navigation-mode.d.ts.map +1 -0
- package/navigation-mode.js +30 -0
- package/navigation-mode.js.map +1 -0
- package/notifications-bell.d.ts +22 -0
- package/notifications-bell.d.ts.map +1 -0
- package/notifications-bell.js +30 -0
- package/notifications-bell.js.map +1 -0
- package/org-chart.d.ts +85 -0
- package/org-chart.d.ts.map +1 -0
- package/org-chart.js +193 -0
- package/org-chart.js.map +1 -0
- package/package.json +61 -0
- package/page-header.d.ts +32 -0
- package/page-header.d.ts.map +1 -0
- package/page-header.js +14 -0
- package/page-header.js.map +1 -0
- package/page-layout.d.ts +29 -0
- package/page-layout.d.ts.map +1 -0
- package/page-layout.js +23 -0
- package/page-layout.js.map +1 -0
- package/page-transition.d.ts +13 -0
- package/page-transition.d.ts.map +1 -0
- package/page-transition.js +14 -0
- package/page-transition.js.map +1 -0
- package/paged-table.d.ts +54 -0
- package/paged-table.d.ts.map +1 -0
- package/paged-table.js +74 -0
- package/paged-table.js.map +1 -0
- package/popover.d.ts +16 -0
- package/popover.d.ts.map +1 -0
- package/popover.js +99 -0
- package/popover.js.map +1 -0
- package/progress.d.ts +7 -0
- package/progress.d.ts.map +1 -0
- package/progress.js +8 -0
- package/progress.js.map +1 -0
- package/prompt-dialog.d.ts +21 -0
- package/prompt-dialog.d.ts.map +1 -0
- package/prompt-dialog.js +109 -0
- package/prompt-dialog.js.map +1 -0
- package/record-list.d.ts +76 -0
- package/record-list.d.ts.map +1 -0
- package/record-list.js +51 -0
- package/record-list.js.map +1 -0
- package/search-select.d.ts +42 -0
- package/search-select.d.ts.map +1 -0
- package/search-select.js +194 -0
- package/search-select.js.map +1 -0
- package/select-options.d.ts +16 -0
- package/select-options.d.ts.map +1 -0
- package/select-options.js +119 -0
- package/select-options.js.map +1 -0
- package/select.d.ts +24 -0
- package/select.d.ts.map +1 -0
- package/select.js +65 -0
- package/select.js.map +1 -0
- package/separator.d.ts +6 -0
- package/separator.d.ts.map +1 -0
- package/separator.js +7 -0
- package/separator.js.map +1 -0
- package/settings-layout.d.ts +79 -0
- package/settings-layout.d.ts.map +1 -0
- package/settings-layout.js +74 -0
- package/settings-layout.js.map +1 -0
- package/sidebar-nav.d.ts +67 -0
- package/sidebar-nav.d.ts.map +1 -0
- package/sidebar-nav.js +291 -0
- package/sidebar-nav.js.map +1 -0
- package/signature-pad.d.ts +16 -0
- package/signature-pad.d.ts.map +1 -0
- package/signature-pad.js +355 -0
- package/signature-pad.js.map +1 -0
- package/skeleton.d.ts +9 -0
- package/skeleton.d.ts.map +1 -0
- package/skeleton.js +12 -0
- package/skeleton.js.map +1 -0
- package/slider.d.ts +13 -0
- package/slider.d.ts.map +1 -0
- package/slider.js +15 -0
- package/slider.js.map +1 -0
- package/sortable-th.d.ts +28 -0
- package/sortable-th.d.ts.map +1 -0
- package/sortable-th.js +35 -0
- package/sortable-th.js.map +1 -0
- package/sparkline.d.ts +16 -0
- package/sparkline.d.ts.map +1 -0
- package/sparkline.js +46 -0
- package/sparkline.js.map +1 -0
- package/spinner.d.ts +11 -0
- package/spinner.d.ts.map +1 -0
- package/spinner.js +11 -0
- package/spinner.js.map +1 -0
- package/styles.css +166 -0
- package/subtab-nav.d.ts +17 -0
- package/subtab-nav.d.ts.map +1 -0
- package/subtab-nav.js +11 -0
- package/subtab-nav.js.map +1 -0
- package/switch.d.ts +9 -0
- package/switch.d.ts.map +1 -0
- package/switch.js +11 -0
- package/switch.js.map +1 -0
- package/tab-content.d.ts +24 -0
- package/tab-content.d.ts.map +1 -0
- package/tab-content.js +26 -0
- package/tab-content.js.map +1 -0
- package/table.d.ts +28 -0
- package/table.d.ts.map +1 -0
- package/table.js +21 -0
- package/table.js.map +1 -0
- package/tabs.d.ts +18 -0
- package/tabs.d.ts.map +1 -0
- package/tabs.js +18 -0
- package/tabs.js.map +1 -0
- package/text-context.d.ts +9 -0
- package/text-context.d.ts.map +1 -0
- package/text-context.js +12 -0
- package/text-context.js.map +1 -0
- package/textarea.d.ts +4 -0
- package/textarea.d.ts.map +1 -0
- package/textarea.js +6 -0
- package/textarea.js.map +1 -0
- package/theme-script.d.ts +3 -0
- package/theme-script.d.ts.map +1 -0
- package/theme-script.js +6 -0
- package/theme-script.js.map +1 -0
- package/theme.d.ts +28 -0
- package/theme.d.ts.map +1 -0
- package/theme.js +92 -0
- package/theme.js.map +1 -0
- package/toast.d.ts +52 -0
- package/toast.d.ts.map +1 -0
- package/toast.js +178 -0
- package/toast.js.map +1 -0
- package/tooltip.d.ts +12 -0
- package/tooltip.d.ts.map +1 -0
- package/tooltip.js +14 -0
- package/tooltip.js.map +1 -0
- package/top-nav.d.ts +18 -0
- package/top-nav.d.ts.map +1 -0
- package/top-nav.js +160 -0
- package/top-nav.js.map +1 -0
- package/upload-limits.d.ts +4 -0
- package/upload-limits.d.ts.map +1 -0
- package/upload-limits.js +21 -0
- package/upload-limits.js.map +1 -0
- package/use-element-size.d.ts +7 -0
- package/use-element-size.d.ts.map +1 -0
- package/use-element-size.js +21 -0
- package/use-element-size.js.map +1 -0
- package/utils.d.ts +4 -0
- package/utils.d.ts.map +1 -0
- package/utils.js +7 -0
- package/utils.js.map +1 -0
- package/workspace-launcher.d.ts +50 -0
- package/workspace-launcher.d.ts.map +1 -0
- package/workspace-launcher.js +127 -0
- package/workspace-launcher.js.map +1 -0
package/signature-pad.js
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
// SignaturePad — a canvas-based "sign here with your finger / stylus / mouse"
|
|
4
|
+
// primitive. Outputs a PNG data-url via `onChange(value)`; pass `null` to
|
|
5
|
+
// represent an empty canvas. Standalone — does not depend on the upload
|
|
6
|
+
// helpers; the consumer is responsible for uploading the data-url if needed.
|
|
7
|
+
//
|
|
8
|
+
// Example:
|
|
9
|
+
//
|
|
10
|
+
// function MySignatureField() {
|
|
11
|
+
// const [value, setValue] = useState<string | null>(null)
|
|
12
|
+
// return (
|
|
13
|
+
// <div className="space-y-2">
|
|
14
|
+
// <SignaturePad value={value} onChange={setValue} />
|
|
15
|
+
// {value ? (
|
|
16
|
+
// <button onClick={() => uploadDataUrl(value)}>Save</button>
|
|
17
|
+
// ) : null}
|
|
18
|
+
// </div>
|
|
19
|
+
// )
|
|
20
|
+
// }
|
|
21
|
+
//
|
|
22
|
+
// Pointer events are used directly so a single handler covers mouse, touch,
|
|
23
|
+
// and stylus input. Strokes are rendered with quadratic-curve interpolation
|
|
24
|
+
// for smoothness, and stroke width is modulated lightly by pointer velocity
|
|
25
|
+
// (slower = thicker) to feel like ink.
|
|
26
|
+
import { useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from 'react';
|
|
27
|
+
import { Button } from './button.js';
|
|
28
|
+
import { useUiText } from './text-context.js';
|
|
29
|
+
import { cn } from './utils.js';
|
|
30
|
+
const SIGNATURE_INK_TOKEN = '--ch-signature-ink';
|
|
31
|
+
const STROKE_BASE_WIDTH = 1.8;
|
|
32
|
+
const STROKE_MAX_WIDTH = 3.2;
|
|
33
|
+
const STROKE_MIN_WIDTH = 1.1;
|
|
34
|
+
const VELOCITY_FILTER = 0.65; // smoothing factor on velocity (0..1)
|
|
35
|
+
const DEBOUNCE_MS = 100;
|
|
36
|
+
export function SignaturePad({ value, onChange, disabled = false, height = 160, className, ariaLabel, }) {
|
|
37
|
+
const t = useUiText();
|
|
38
|
+
const localizedAriaLabel = t(ariaLabel ?? 'Signature');
|
|
39
|
+
const wrapRef = useRef(null);
|
|
40
|
+
const canvasRef = useRef(null);
|
|
41
|
+
const labelId = useId();
|
|
42
|
+
// Latest props as refs so stable event handlers can read them.
|
|
43
|
+
const onChangeRef = useRef(onChange);
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
onChangeRef.current = onChange;
|
|
46
|
+
}, [onChange]);
|
|
47
|
+
const valueRef = useRef(value);
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
valueRef.current = value;
|
|
50
|
+
}, [value]);
|
|
51
|
+
// Drawing state — kept in refs to avoid re-renders during a stroke.
|
|
52
|
+
const drawingRef = useRef(false);
|
|
53
|
+
// True once the user has actually drawn on this canvas. Until then, resize
|
|
54
|
+
// must restore the incoming `value` prop rather than a (possibly blank)
|
|
55
|
+
// snapshot taken from a default-sized 300×150 canvas on first mount.
|
|
56
|
+
const hasDrawnRef = useRef(false);
|
|
57
|
+
const activePointerRef = useRef(null);
|
|
58
|
+
const lastPointRef = useRef(null);
|
|
59
|
+
const lastMidRef = useRef(null);
|
|
60
|
+
const lastWidthRef = useRef(STROKE_BASE_WIDTH);
|
|
61
|
+
const lastVelocityRef = useRef(0);
|
|
62
|
+
const dprRef = useRef(1);
|
|
63
|
+
const debounceRef = useRef(null);
|
|
64
|
+
const [hasInk, setHasInk] = useState(!!value);
|
|
65
|
+
const getCtx = useCallback(() => canvasRef.current?.getContext('2d') ?? null, []);
|
|
66
|
+
// Load a data-url onto the canvas (used for initial value + resize preservation).
|
|
67
|
+
const loadDataUrl = useCallback((dataUrl) => new Promise((resolve) => {
|
|
68
|
+
const canvas = canvasRef.current;
|
|
69
|
+
const ctx = getCtx();
|
|
70
|
+
if (!canvas || !ctx) {
|
|
71
|
+
resolve();
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const img = new Image();
|
|
75
|
+
img.onload = () => {
|
|
76
|
+
// The image was captured at CSS-pixel dimensions, so paint it back
|
|
77
|
+
// sized to the current CSS box; the context transform handles DPR.
|
|
78
|
+
ctx.drawImage(img, 0, 0, canvas.width / dprRef.current, canvas.height / dprRef.current);
|
|
79
|
+
resolve();
|
|
80
|
+
};
|
|
81
|
+
img.onerror = () => resolve();
|
|
82
|
+
img.src = dataUrl;
|
|
83
|
+
}), [getCtx]);
|
|
84
|
+
// Resize the backing store to match the container width and DPR, preserving
|
|
85
|
+
// existing strokes by snapshotting before and restoring after.
|
|
86
|
+
const resizeCanvas = useCallback(async () => {
|
|
87
|
+
const canvas = canvasRef.current;
|
|
88
|
+
const wrap = wrapRef.current;
|
|
89
|
+
if (!canvas || !wrap)
|
|
90
|
+
return;
|
|
91
|
+
const dpr = Math.max(1, window.devicePixelRatio || 1);
|
|
92
|
+
const cssWidth = Math.max(1, Math.floor(wrap.clientWidth));
|
|
93
|
+
const cssHeight = Math.max(1, Math.floor(height));
|
|
94
|
+
// Already correctly sized — bail (avoids the resize loop on first mount).
|
|
95
|
+
if (canvas.width === Math.round(cssWidth * dpr) &&
|
|
96
|
+
canvas.height === Math.round(cssHeight * dpr) &&
|
|
97
|
+
dprRef.current === dpr) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
// Snapshot current pixels so we can repaint after resize — but only once
|
|
101
|
+
// the user has drawn. Before that, an initial-value pad must restore the
|
|
102
|
+
// `value` prop, not a blank snapshot from the default-sized canvas.
|
|
103
|
+
let snapshot = null;
|
|
104
|
+
if (hasDrawnRef.current && canvas.width > 0 && canvas.height > 0) {
|
|
105
|
+
try {
|
|
106
|
+
snapshot = canvas.toDataURL('image/png');
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
snapshot = null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
dprRef.current = dpr;
|
|
113
|
+
canvas.width = Math.round(cssWidth * dpr);
|
|
114
|
+
canvas.height = Math.round(cssHeight * dpr);
|
|
115
|
+
canvas.style.width = `${cssWidth}px`;
|
|
116
|
+
canvas.style.height = `${cssHeight}px`;
|
|
117
|
+
const ctx = canvas.getContext('2d');
|
|
118
|
+
if (!ctx)
|
|
119
|
+
return;
|
|
120
|
+
// Scale once so all drawing math can be in CSS pixels.
|
|
121
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
122
|
+
ctx.lineCap = 'round';
|
|
123
|
+
ctx.lineJoin = 'round';
|
|
124
|
+
const ink = getComputedStyle(document.documentElement).getPropertyValue(SIGNATURE_INK_TOKEN);
|
|
125
|
+
const inkColor = ink.trim() ? `rgb(${ink.trim().replace(/\s+/g, ' ')})` : 'currentColor';
|
|
126
|
+
ctx.strokeStyle = inkColor;
|
|
127
|
+
ctx.fillStyle = inkColor;
|
|
128
|
+
ctx.clearRect(0, 0, cssWidth, cssHeight);
|
|
129
|
+
// Prefer in-memory snapshot over the prop value to avoid losing strokes
|
|
130
|
+
// that haven't yet been debounced into onChange.
|
|
131
|
+
const toRestore = snapshot ?? valueRef.current;
|
|
132
|
+
if (toRestore) {
|
|
133
|
+
await loadDataUrl(toRestore);
|
|
134
|
+
}
|
|
135
|
+
}, [height, loadDataUrl]);
|
|
136
|
+
// Initial sizing + window resize listener.
|
|
137
|
+
useLayoutEffect(() => {
|
|
138
|
+
void resizeCanvas();
|
|
139
|
+
const onResize = () => void resizeCanvas();
|
|
140
|
+
window.addEventListener('resize', onResize);
|
|
141
|
+
return () => window.removeEventListener('resize', onResize);
|
|
142
|
+
}, [resizeCanvas]);
|
|
143
|
+
// If `value` changes externally (e.g. parent reset), repaint.
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
const canvas = canvasRef.current;
|
|
146
|
+
const ctx = getCtx();
|
|
147
|
+
if (!canvas || !ctx)
|
|
148
|
+
return;
|
|
149
|
+
const cssWidth = canvas.width / dprRef.current;
|
|
150
|
+
const cssHeight = canvas.height / dprRef.current;
|
|
151
|
+
if (value === null) {
|
|
152
|
+
ctx.clearRect(0, 0, cssWidth, cssHeight);
|
|
153
|
+
setHasInk(false);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
// Only repaint if this value didn't originate from our own onChange — we
|
|
157
|
+
// detect that by comparing to the most recent emitted value.
|
|
158
|
+
if (value === lastEmittedRef.current)
|
|
159
|
+
return;
|
|
160
|
+
ctx.clearRect(0, 0, cssWidth, cssHeight);
|
|
161
|
+
void loadDataUrl(value).then(() => setHasInk(true));
|
|
162
|
+
}, [getCtx, loadDataUrl, value]);
|
|
163
|
+
// Initialized to null (not `value`) so the external-value repaint effect
|
|
164
|
+
// runs once on mount and actually paints an initial signature.
|
|
165
|
+
const lastEmittedRef = useRef(null);
|
|
166
|
+
const emit = useCallback((next) => {
|
|
167
|
+
lastEmittedRef.current = next;
|
|
168
|
+
onChangeRef.current(next);
|
|
169
|
+
}, []);
|
|
170
|
+
const scheduleEmit = useCallback(() => {
|
|
171
|
+
if (debounceRef.current)
|
|
172
|
+
clearTimeout(debounceRef.current);
|
|
173
|
+
debounceRef.current = setTimeout(() => {
|
|
174
|
+
const canvas = canvasRef.current;
|
|
175
|
+
if (!canvas)
|
|
176
|
+
return;
|
|
177
|
+
try {
|
|
178
|
+
emit(canvas.toDataURL('image/png'));
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
// Tainted canvas etc — silently skip.
|
|
182
|
+
}
|
|
183
|
+
}, DEBOUNCE_MS);
|
|
184
|
+
}, [emit]);
|
|
185
|
+
// --- Drawing ---------------------------------------------------------------
|
|
186
|
+
const pointFromEvent = useCallback((e) => {
|
|
187
|
+
const canvas = canvasRef.current;
|
|
188
|
+
const rect = canvas.getBoundingClientRect();
|
|
189
|
+
return {
|
|
190
|
+
x: e.clientX - rect.left,
|
|
191
|
+
y: e.clientY - rect.top,
|
|
192
|
+
t: e.timeStamp,
|
|
193
|
+
};
|
|
194
|
+
}, []);
|
|
195
|
+
// Variable stroke width: slower pointer → thicker line. Velocity is smoothed
|
|
196
|
+
// with a simple low-pass filter so quick jitter doesn't make the line pulse.
|
|
197
|
+
const computeWidth = useCallback((velocity) => {
|
|
198
|
+
const smoothed = VELOCITY_FILTER * velocity + (1 - VELOCITY_FILTER) * lastVelocityRef.current;
|
|
199
|
+
lastVelocityRef.current = smoothed;
|
|
200
|
+
// Map velocity to width: 0 px/ms → max, 1.5+ px/ms → min.
|
|
201
|
+
const t = Math.min(1, smoothed / 1.5);
|
|
202
|
+
return STROKE_MAX_WIDTH - (STROKE_MAX_WIDTH - STROKE_MIN_WIDTH) * t;
|
|
203
|
+
}, []);
|
|
204
|
+
const onPointerDown = useCallback((e) => {
|
|
205
|
+
if (disabled)
|
|
206
|
+
return;
|
|
207
|
+
const canvas = canvasRef.current;
|
|
208
|
+
const ctx = getCtx();
|
|
209
|
+
if (!canvas || !ctx)
|
|
210
|
+
return;
|
|
211
|
+
// Ignore secondary pointers while one is already drawing.
|
|
212
|
+
if (activePointerRef.current !== null)
|
|
213
|
+
return;
|
|
214
|
+
activePointerRef.current = e.pointerId;
|
|
215
|
+
drawingRef.current = true;
|
|
216
|
+
hasDrawnRef.current = true;
|
|
217
|
+
try {
|
|
218
|
+
canvas.setPointerCapture(e.pointerId);
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
// Some browsers throw if the pointer can't be captured — non-fatal.
|
|
222
|
+
}
|
|
223
|
+
const p = pointFromEvent(e);
|
|
224
|
+
lastPointRef.current = p;
|
|
225
|
+
lastMidRef.current = { x: p.x, y: p.y };
|
|
226
|
+
lastVelocityRef.current = 0;
|
|
227
|
+
lastWidthRef.current = STROKE_BASE_WIDTH;
|
|
228
|
+
// Paint a dot at the starting point so taps produce visible ink.
|
|
229
|
+
ctx.beginPath();
|
|
230
|
+
ctx.arc(p.x, p.y, STROKE_BASE_WIDTH / 2, 0, Math.PI * 2);
|
|
231
|
+
ctx.fill();
|
|
232
|
+
setHasInk(true);
|
|
233
|
+
}, [disabled, getCtx, pointFromEvent]);
|
|
234
|
+
const onPointerMove = useCallback((e) => {
|
|
235
|
+
if (!drawingRef.current)
|
|
236
|
+
return;
|
|
237
|
+
if (e.pointerId !== activePointerRef.current)
|
|
238
|
+
return;
|
|
239
|
+
const ctx = getCtx();
|
|
240
|
+
const prev = lastPointRef.current;
|
|
241
|
+
const prevMid = lastMidRef.current;
|
|
242
|
+
if (!ctx || !prev || !prevMid)
|
|
243
|
+
return;
|
|
244
|
+
const p = pointFromEvent(e);
|
|
245
|
+
const dx = p.x - prev.x;
|
|
246
|
+
const dy = p.y - prev.y;
|
|
247
|
+
const dist = Math.hypot(dx, dy);
|
|
248
|
+
const dt = Math.max(1, p.t - prev.t);
|
|
249
|
+
const velocity = dist / dt; // px per ms
|
|
250
|
+
const targetWidth = computeWidth(velocity);
|
|
251
|
+
// Smooth width changes between consecutive segments.
|
|
252
|
+
const width = (lastWidthRef.current + targetWidth) / 2;
|
|
253
|
+
// Quadratic curve through the previous mid-point, using `prev` as the
|
|
254
|
+
// control point and the new mid-point as the destination.
|
|
255
|
+
const mid = { x: (prev.x + p.x) / 2, y: (prev.y + p.y) / 2 };
|
|
256
|
+
ctx.beginPath();
|
|
257
|
+
ctx.lineWidth = width;
|
|
258
|
+
ctx.moveTo(prevMid.x, prevMid.y);
|
|
259
|
+
ctx.quadraticCurveTo(prev.x, prev.y, mid.x, mid.y);
|
|
260
|
+
ctx.stroke();
|
|
261
|
+
lastPointRef.current = p;
|
|
262
|
+
lastMidRef.current = mid;
|
|
263
|
+
lastWidthRef.current = width;
|
|
264
|
+
}, [computeWidth, getCtx, pointFromEvent]);
|
|
265
|
+
const finishStroke = useCallback((e) => {
|
|
266
|
+
if (e.pointerId !== activePointerRef.current)
|
|
267
|
+
return;
|
|
268
|
+
const canvas = canvasRef.current;
|
|
269
|
+
const ctx = getCtx();
|
|
270
|
+
const prev = lastPointRef.current;
|
|
271
|
+
const prevMid = lastMidRef.current;
|
|
272
|
+
if (drawingRef.current && ctx && prev && prevMid) {
|
|
273
|
+
// Close out the curve to the final point so the last segment isn't lost.
|
|
274
|
+
ctx.beginPath();
|
|
275
|
+
ctx.lineWidth = lastWidthRef.current;
|
|
276
|
+
ctx.moveTo(prevMid.x, prevMid.y);
|
|
277
|
+
ctx.lineTo(prev.x, prev.y);
|
|
278
|
+
ctx.stroke();
|
|
279
|
+
}
|
|
280
|
+
drawingRef.current = false;
|
|
281
|
+
activePointerRef.current = null;
|
|
282
|
+
lastPointRef.current = null;
|
|
283
|
+
lastMidRef.current = null;
|
|
284
|
+
if (canvas) {
|
|
285
|
+
try {
|
|
286
|
+
canvas.releasePointerCapture(e.pointerId);
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
// ignore
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
scheduleEmit();
|
|
293
|
+
}, [getCtx, scheduleEmit]);
|
|
294
|
+
// Attach pointer listeners imperatively so we get raw PointerEvent objects
|
|
295
|
+
// and can use `{ passive: false }` where useful.
|
|
296
|
+
useEffect(() => {
|
|
297
|
+
const canvas = canvasRef.current;
|
|
298
|
+
if (!canvas)
|
|
299
|
+
return;
|
|
300
|
+
if (disabled)
|
|
301
|
+
return;
|
|
302
|
+
const down = (e) => onPointerDown(e);
|
|
303
|
+
const move = (e) => onPointerMove(e);
|
|
304
|
+
const up = (e) => finishStroke(e);
|
|
305
|
+
canvas.addEventListener('pointerdown', down);
|
|
306
|
+
canvas.addEventListener('pointermove', move);
|
|
307
|
+
canvas.addEventListener('pointerup', up);
|
|
308
|
+
canvas.addEventListener('pointercancel', up);
|
|
309
|
+
canvas.addEventListener('pointerleave', up);
|
|
310
|
+
return () => {
|
|
311
|
+
canvas.removeEventListener('pointerdown', down);
|
|
312
|
+
canvas.removeEventListener('pointermove', move);
|
|
313
|
+
canvas.removeEventListener('pointerup', up);
|
|
314
|
+
canvas.removeEventListener('pointercancel', up);
|
|
315
|
+
canvas.removeEventListener('pointerleave', up);
|
|
316
|
+
};
|
|
317
|
+
}, [disabled, finishStroke, onPointerDown, onPointerMove]);
|
|
318
|
+
// Clean up any pending debounce on unmount.
|
|
319
|
+
useEffect(() => {
|
|
320
|
+
return () => {
|
|
321
|
+
if (debounceRef.current)
|
|
322
|
+
clearTimeout(debounceRef.current);
|
|
323
|
+
};
|
|
324
|
+
}, []);
|
|
325
|
+
const handleClear = useCallback(() => {
|
|
326
|
+
const canvas = canvasRef.current;
|
|
327
|
+
const ctx = getCtx();
|
|
328
|
+
if (!canvas || !ctx)
|
|
329
|
+
return;
|
|
330
|
+
const cssWidth = canvas.width / dprRef.current;
|
|
331
|
+
const cssHeight = canvas.height / dprRef.current;
|
|
332
|
+
ctx.clearRect(0, 0, cssWidth, cssHeight);
|
|
333
|
+
setHasInk(false);
|
|
334
|
+
if (debounceRef.current) {
|
|
335
|
+
clearTimeout(debounceRef.current);
|
|
336
|
+
debounceRef.current = null;
|
|
337
|
+
}
|
|
338
|
+
emit(null);
|
|
339
|
+
}, [emit, getCtx]);
|
|
340
|
+
return (_jsxs("div", { ref: wrapRef, className: cn('w-full', className), children: [_jsxs("div", { className: cn(
|
|
341
|
+
// The signing surface stays white in both themes: the ink is dark
|
|
342
|
+
// uses the stable document-ink token and the exported PNG is
|
|
343
|
+
// dark-on-transparent for PDFs,
|
|
344
|
+
// so a dark backing would hide the stroke while signing and viewing.
|
|
345
|
+
'relative w-full overflow-hidden rounded-md border bg-[rgb(var(--ch-signature-surface))]', disabled
|
|
346
|
+
? 'border-border'
|
|
347
|
+
: 'border-border-strong'), style: { height }, children: [disabled && value ? (
|
|
348
|
+
// Static image render in disabled mode so the underlying canvas
|
|
349
|
+
// (and its pointer handlers) doesn't have to participate. This is a
|
|
350
|
+
// generated data URL, which a server-side image optimizer cannot load.
|
|
351
|
+
_jsx("img", { src: value, alt: localizedAriaLabel, className: "h-full w-full object-contain", draggable: false })) : (_jsxs(_Fragment, { children: [_jsx("canvas", { ref: canvasRef, role: "img", "aria-label": localizedAriaLabel, "aria-describedby": labelId, className: cn('block h-full w-full', disabled ? 'cursor-not-allowed opacity-60' : 'cursor-crosshair'), style: { touchAction: 'none' } }), !hasInk && !disabled ? (_jsxs("div", { className: "pointer-events-none absolute inset-0", children: [_jsx("div", { className: "absolute right-4 left-4 border-t border-dashed border-border", style: { top: `${(2 / 3) * 100}%` } }), _jsx("div", { className: "absolute inset-0 flex items-center justify-center", children: _jsx("span", { className: "text-xs text-fg-subtle", children: t('Sign here') }) })] })) : null] })), _jsx("span", { id: labelId, className: "sr-only", children: hasInk
|
|
352
|
+
? t('{value0} provided.', { value0: localizedAriaLabel })
|
|
353
|
+
: t('{value0} empty.', { value0: localizedAriaLabel }) })] }), !disabled ? (_jsxs("div", { className: "mt-2 flex items-center justify-between", children: [_jsx("span", { className: "text-xs text-fg-muted", children: hasInk ? ' ' : t('Sign above with your finger, stylus, or mouse.') }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: handleClear, disabled: !hasInk, "aria-label": t('Clear signature'), children: t('Clear') })] })) : null] }));
|
|
354
|
+
}
|
|
355
|
+
//# sourceMappingURL=signature-pad.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signature-pad.js","sourceRoot":"","sources":["../src/signature-pad.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client'\n\n// SignaturePad — a canvas-based \"sign here with your finger / stylus / mouse\"\n// primitive. Outputs a PNG data-url via `onChange(value)`; pass `null` to\n// represent an empty canvas. Standalone — does not depend on the upload\n// helpers; the consumer is responsible for uploading the data-url if needed.\n//\n// Example:\n//\n// function MySignatureField() {\n// const [value, setValue] = useState<string | null>(null)\n// return (\n// <div className=\"space-y-2\">\n// <SignaturePad value={value} onChange={setValue} />\n// {value ? (\n// <button onClick={() => uploadDataUrl(value)}>Save</button>\n// ) : null}\n// </div>\n// )\n// }\n//\n// Pointer events are used directly so a single handler covers mouse, touch,\n// and stylus input. Strokes are rendered with quadratic-curve interpolation\n// for smoothness, and stroke width is modulated lightly by pointer velocity\n// (slower = thicker) to feel like ink.\n\nimport { useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from 'react'\nimport { Button } from './button'\nimport { useUiText } from './text-context'\nimport { cn } from './utils'\n\nexport type SignaturePadProps = {\n /** Current value as a PNG data-url, or `null` if empty. */\n value: string | null\n /** Called with the new data-url after each completed stroke, or `null` on clear. */\n onChange: (value: string | null) => void\n /** Disable drawing. Renders `value` (if any) as a static image. */\n disabled?: boolean\n /** Canvas height in CSS pixels. Default 160. */\n height?: number\n /** Extra classes on the outer wrapper. */\n className?: string\n /** Accessibility label for the canvas. Default \"Signature\". */\n ariaLabel?: string\n}\n\nconst SIGNATURE_INK_TOKEN = '--ch-signature-ink'\nconst STROKE_BASE_WIDTH = 1.8\nconst STROKE_MAX_WIDTH = 3.2\nconst STROKE_MIN_WIDTH = 1.1\nconst VELOCITY_FILTER = 0.65 // smoothing factor on velocity (0..1)\nconst DEBOUNCE_MS = 100\n\ntype Point = { x: number; y: number; t: number }\n\nexport function SignaturePad({\n value,\n onChange,\n disabled = false,\n height = 160,\n className,\n ariaLabel,\n}: SignaturePadProps) {\n const t = useUiText()\n const localizedAriaLabel = t(ariaLabel ?? 'Signature')\n const wrapRef = useRef<HTMLDivElement>(null)\n const canvasRef = useRef<HTMLCanvasElement>(null)\n const labelId = useId()\n\n // Latest props as refs so stable event handlers can read them.\n const onChangeRef = useRef(onChange)\n useEffect(() => {\n onChangeRef.current = onChange\n }, [onChange])\n\n const valueRef = useRef<string | null>(value)\n useEffect(() => {\n valueRef.current = value\n }, [value])\n\n // Drawing state — kept in refs to avoid re-renders during a stroke.\n const drawingRef = useRef(false)\n // True once the user has actually drawn on this canvas. Until then, resize\n // must restore the incoming `value` prop rather than a (possibly blank)\n // snapshot taken from a default-sized 300×150 canvas on first mount.\n const hasDrawnRef = useRef(false)\n const activePointerRef = useRef<number | null>(null)\n const lastPointRef = useRef<Point | null>(null)\n const lastMidRef = useRef<{ x: number; y: number } | null>(null)\n const lastWidthRef = useRef(STROKE_BASE_WIDTH)\n const lastVelocityRef = useRef(0)\n const dprRef = useRef(1)\n const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)\n\n const [hasInk, setHasInk] = useState<boolean>(!!value)\n\n const getCtx = useCallback(() => canvasRef.current?.getContext('2d') ?? null, [])\n\n // Load a data-url onto the canvas (used for initial value + resize preservation).\n const loadDataUrl = useCallback(\n (dataUrl: string) =>\n new Promise<void>((resolve) => {\n const canvas = canvasRef.current\n const ctx = getCtx()\n if (!canvas || !ctx) {\n resolve()\n return\n }\n const img = new Image()\n img.onload = () => {\n // The image was captured at CSS-pixel dimensions, so paint it back\n // sized to the current CSS box; the context transform handles DPR.\n ctx.drawImage(img, 0, 0, canvas.width / dprRef.current, canvas.height / dprRef.current)\n resolve()\n }\n img.onerror = () => resolve()\n img.src = dataUrl\n }),\n [getCtx],\n )\n\n // Resize the backing store to match the container width and DPR, preserving\n // existing strokes by snapshotting before and restoring after.\n const resizeCanvas = useCallback(async () => {\n const canvas = canvasRef.current\n const wrap = wrapRef.current\n if (!canvas || !wrap) return\n const dpr = Math.max(1, window.devicePixelRatio || 1)\n const cssWidth = Math.max(1, Math.floor(wrap.clientWidth))\n const cssHeight = Math.max(1, Math.floor(height))\n\n // Already correctly sized — bail (avoids the resize loop on first mount).\n if (\n canvas.width === Math.round(cssWidth * dpr) &&\n canvas.height === Math.round(cssHeight * dpr) &&\n dprRef.current === dpr\n ) {\n return\n }\n\n // Snapshot current pixels so we can repaint after resize — but only once\n // the user has drawn. Before that, an initial-value pad must restore the\n // `value` prop, not a blank snapshot from the default-sized canvas.\n let snapshot: string | null = null\n if (hasDrawnRef.current && canvas.width > 0 && canvas.height > 0) {\n try {\n snapshot = canvas.toDataURL('image/png')\n } catch {\n snapshot = null\n }\n }\n\n dprRef.current = dpr\n canvas.width = Math.round(cssWidth * dpr)\n canvas.height = Math.round(cssHeight * dpr)\n canvas.style.width = `${cssWidth}px`\n canvas.style.height = `${cssHeight}px`\n\n const ctx = canvas.getContext('2d')\n if (!ctx) return\n // Scale once so all drawing math can be in CSS pixels.\n ctx.setTransform(dpr, 0, 0, dpr, 0, 0)\n ctx.lineCap = 'round'\n ctx.lineJoin = 'round'\n const ink = getComputedStyle(document.documentElement).getPropertyValue(SIGNATURE_INK_TOKEN)\n const inkColor = ink.trim() ? `rgb(${ink.trim().replace(/\\s+/g, ' ')})` : 'currentColor'\n ctx.strokeStyle = inkColor\n ctx.fillStyle = inkColor\n ctx.clearRect(0, 0, cssWidth, cssHeight)\n\n // Prefer in-memory snapshot over the prop value to avoid losing strokes\n // that haven't yet been debounced into onChange.\n const toRestore = snapshot ?? valueRef.current\n if (toRestore) {\n await loadDataUrl(toRestore)\n }\n }, [height, loadDataUrl])\n\n // Initial sizing + window resize listener.\n useLayoutEffect(() => {\n void resizeCanvas()\n const onResize = () => void resizeCanvas()\n window.addEventListener('resize', onResize)\n return () => window.removeEventListener('resize', onResize)\n }, [resizeCanvas])\n\n // If `value` changes externally (e.g. parent reset), repaint.\n useEffect(() => {\n const canvas = canvasRef.current\n const ctx = getCtx()\n if (!canvas || !ctx) return\n const cssWidth = canvas.width / dprRef.current\n const cssHeight = canvas.height / dprRef.current\n\n if (value === null) {\n ctx.clearRect(0, 0, cssWidth, cssHeight)\n setHasInk(false)\n return\n }\n // Only repaint if this value didn't originate from our own onChange — we\n // detect that by comparing to the most recent emitted value.\n if (value === lastEmittedRef.current) return\n ctx.clearRect(0, 0, cssWidth, cssHeight)\n void loadDataUrl(value).then(() => setHasInk(true))\n }, [getCtx, loadDataUrl, value])\n\n // Initialized to null (not `value`) so the external-value repaint effect\n // runs once on mount and actually paints an initial signature.\n const lastEmittedRef = useRef<string | null>(null)\n\n const emit = useCallback((next: string | null) => {\n lastEmittedRef.current = next\n onChangeRef.current(next)\n }, [])\n\n const scheduleEmit = useCallback(() => {\n if (debounceRef.current) clearTimeout(debounceRef.current)\n debounceRef.current = setTimeout(() => {\n const canvas = canvasRef.current\n if (!canvas) return\n try {\n emit(canvas.toDataURL('image/png'))\n } catch {\n // Tainted canvas etc — silently skip.\n }\n }, DEBOUNCE_MS)\n }, [emit])\n\n // --- Drawing ---------------------------------------------------------------\n\n const pointFromEvent = useCallback((e: PointerEvent): Point => {\n const canvas = canvasRef.current!\n const rect = canvas.getBoundingClientRect()\n return {\n x: e.clientX - rect.left,\n y: e.clientY - rect.top,\n t: e.timeStamp,\n }\n }, [])\n\n // Variable stroke width: slower pointer → thicker line. Velocity is smoothed\n // with a simple low-pass filter so quick jitter doesn't make the line pulse.\n const computeWidth = useCallback((velocity: number) => {\n const smoothed = VELOCITY_FILTER * velocity + (1 - VELOCITY_FILTER) * lastVelocityRef.current\n lastVelocityRef.current = smoothed\n // Map velocity to width: 0 px/ms → max, 1.5+ px/ms → min.\n const t = Math.min(1, smoothed / 1.5)\n return STROKE_MAX_WIDTH - (STROKE_MAX_WIDTH - STROKE_MIN_WIDTH) * t\n }, [])\n\n const onPointerDown = useCallback(\n (e: PointerEvent) => {\n if (disabled) return\n const canvas = canvasRef.current\n const ctx = getCtx()\n if (!canvas || !ctx) return\n // Ignore secondary pointers while one is already drawing.\n if (activePointerRef.current !== null) return\n\n activePointerRef.current = e.pointerId\n drawingRef.current = true\n hasDrawnRef.current = true\n try {\n canvas.setPointerCapture(e.pointerId)\n } catch {\n // Some browsers throw if the pointer can't be captured — non-fatal.\n }\n\n const p = pointFromEvent(e)\n lastPointRef.current = p\n lastMidRef.current = { x: p.x, y: p.y }\n lastVelocityRef.current = 0\n lastWidthRef.current = STROKE_BASE_WIDTH\n\n // Paint a dot at the starting point so taps produce visible ink.\n ctx.beginPath()\n ctx.arc(p.x, p.y, STROKE_BASE_WIDTH / 2, 0, Math.PI * 2)\n ctx.fill()\n\n setHasInk(true)\n },\n [disabled, getCtx, pointFromEvent],\n )\n\n const onPointerMove = useCallback(\n (e: PointerEvent) => {\n if (!drawingRef.current) return\n if (e.pointerId !== activePointerRef.current) return\n const ctx = getCtx()\n const prev = lastPointRef.current\n const prevMid = lastMidRef.current\n if (!ctx || !prev || !prevMid) return\n\n const p = pointFromEvent(e)\n const dx = p.x - prev.x\n const dy = p.y - prev.y\n const dist = Math.hypot(dx, dy)\n const dt = Math.max(1, p.t - prev.t)\n const velocity = dist / dt // px per ms\n const targetWidth = computeWidth(velocity)\n // Smooth width changes between consecutive segments.\n const width = (lastWidthRef.current + targetWidth) / 2\n\n // Quadratic curve through the previous mid-point, using `prev` as the\n // control point and the new mid-point as the destination.\n const mid = { x: (prev.x + p.x) / 2, y: (prev.y + p.y) / 2 }\n ctx.beginPath()\n ctx.lineWidth = width\n ctx.moveTo(prevMid.x, prevMid.y)\n ctx.quadraticCurveTo(prev.x, prev.y, mid.x, mid.y)\n ctx.stroke()\n\n lastPointRef.current = p\n lastMidRef.current = mid\n lastWidthRef.current = width\n },\n [computeWidth, getCtx, pointFromEvent],\n )\n\n const finishStroke = useCallback(\n (e: PointerEvent) => {\n if (e.pointerId !== activePointerRef.current) return\n const canvas = canvasRef.current\n const ctx = getCtx()\n const prev = lastPointRef.current\n const prevMid = lastMidRef.current\n\n if (drawingRef.current && ctx && prev && prevMid) {\n // Close out the curve to the final point so the last segment isn't lost.\n ctx.beginPath()\n ctx.lineWidth = lastWidthRef.current\n ctx.moveTo(prevMid.x, prevMid.y)\n ctx.lineTo(prev.x, prev.y)\n ctx.stroke()\n }\n\n drawingRef.current = false\n activePointerRef.current = null\n lastPointRef.current = null\n lastMidRef.current = null\n\n if (canvas) {\n try {\n canvas.releasePointerCapture(e.pointerId)\n } catch {\n // ignore\n }\n }\n\n scheduleEmit()\n },\n [getCtx, scheduleEmit],\n )\n\n // Attach pointer listeners imperatively so we get raw PointerEvent objects\n // and can use `{ passive: false }` where useful.\n useEffect(() => {\n const canvas = canvasRef.current\n if (!canvas) return\n if (disabled) return\n\n const down = (e: PointerEvent) => onPointerDown(e)\n const move = (e: PointerEvent) => onPointerMove(e)\n const up = (e: PointerEvent) => finishStroke(e)\n\n canvas.addEventListener('pointerdown', down)\n canvas.addEventListener('pointermove', move)\n canvas.addEventListener('pointerup', up)\n canvas.addEventListener('pointercancel', up)\n canvas.addEventListener('pointerleave', up)\n return () => {\n canvas.removeEventListener('pointerdown', down)\n canvas.removeEventListener('pointermove', move)\n canvas.removeEventListener('pointerup', up)\n canvas.removeEventListener('pointercancel', up)\n canvas.removeEventListener('pointerleave', up)\n }\n }, [disabled, finishStroke, onPointerDown, onPointerMove])\n\n // Clean up any pending debounce on unmount.\n useEffect(() => {\n return () => {\n if (debounceRef.current) clearTimeout(debounceRef.current)\n }\n }, [])\n\n const handleClear = useCallback(() => {\n const canvas = canvasRef.current\n const ctx = getCtx()\n if (!canvas || !ctx) return\n const cssWidth = canvas.width / dprRef.current\n const cssHeight = canvas.height / dprRef.current\n ctx.clearRect(0, 0, cssWidth, cssHeight)\n setHasInk(false)\n if (debounceRef.current) {\n clearTimeout(debounceRef.current)\n debounceRef.current = null\n }\n emit(null)\n }, [emit, getCtx])\n\n return (\n <div ref={wrapRef} className={cn('w-full', className)}>\n <div\n className={cn(\n // The signing surface stays white in both themes: the ink is dark\n // uses the stable document-ink token and the exported PNG is\n // dark-on-transparent for PDFs,\n // so a dark backing would hide the stroke while signing and viewing.\n 'relative w-full overflow-hidden rounded-md border bg-[rgb(var(--ch-signature-surface))]',\n disabled\n ? 'border-border'\n : 'border-border-strong',\n )}\n style={{ height }}\n >\n {disabled && value ? (\n // Static image render in disabled mode so the underlying canvas\n // (and its pointer handlers) doesn't have to participate. This is a\n // generated data URL, which a server-side image optimizer cannot load.\n <img\n src={value}\n alt={localizedAriaLabel}\n className=\"h-full w-full object-contain\"\n draggable={false}\n />\n ) : (\n <>\n <canvas\n ref={canvasRef}\n role=\"img\"\n aria-label={localizedAriaLabel}\n aria-describedby={labelId}\n className={cn(\n 'block h-full w-full',\n disabled ? 'cursor-not-allowed opacity-60' : 'cursor-crosshair',\n )}\n style={{ touchAction: 'none' }}\n />\n {/* Empty-state baseline + hint, hidden once the user has signed. */}\n {!hasInk && !disabled ? (\n <div className=\"pointer-events-none absolute inset-0\">\n <div\n className=\"absolute right-4 left-4 border-t border-dashed border-border\"\n style={{ top: `${(2 / 3) * 100}%` }}\n />\n <div className=\"absolute inset-0 flex items-center justify-center\">\n <span className=\"text-xs text-fg-subtle\">{t('Sign here')}</span>\n </div>\n </div>\n ) : null}\n </>\n )}\n {/* Visually-hidden status for screen readers. */}\n <span id={labelId} className=\"sr-only\">\n {hasInk\n ? t('{value0} provided.', { value0: localizedAriaLabel })\n : t('{value0} empty.', { value0: localizedAriaLabel })}\n </span>\n </div>\n\n {!disabled ? (\n <div className=\"mt-2 flex items-center justify-between\">\n <span className=\"text-xs text-fg-muted\">\n {hasInk ? ' ' : t('Sign above with your finger, stylus, or mouse.')}\n </span>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={handleClear}\n disabled={!hasInk}\n aria-label={t('Clear signature')}\n >\n {t('Clear')}\n </Button>\n </div>\n ) : null}\n </div>\n )\n}\n"]}
|
package/skeleton.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export type SkeletonProps = React.HTMLAttributes<HTMLDivElement>;
|
|
3
|
+
/**
|
|
4
|
+
* Skeleton placeholder with a left→right shimmer sweep (`.appkit-shimmer` in
|
|
5
|
+
* tokens.css — pure CSS, so it works server-rendered). Reduced-motion users get
|
|
6
|
+
* a static placeholder.
|
|
7
|
+
*/
|
|
8
|
+
export declare function Skeleton({ className, ...props }: SkeletonProps): React.JSX.Element;
|
|
9
|
+
//# sourceMappingURL=skeleton.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skeleton.d.ts","sourceRoot":"","sources":["../src/skeleton.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAG9B,MAAM,MAAM,aAAa,GAAG,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC,CAAA;AAEhE;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,aAAa,qBAQ9D"}
|
package/skeleton.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { cn } from './utils.js';
|
|
4
|
+
/**
|
|
5
|
+
* Skeleton placeholder with a left→right shimmer sweep (`.appkit-shimmer` in
|
|
6
|
+
* tokens.css — pure CSS, so it works server-rendered). Reduced-motion users get
|
|
7
|
+
* a static placeholder.
|
|
8
|
+
*/
|
|
9
|
+
export function Skeleton({ className, ...props }) {
|
|
10
|
+
return (_jsx("div", { "aria-hidden": "true", className: cn('appkit-shimmer relative overflow-hidden rounded-md bg-border-subtle', className), ...props }));
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=skeleton.js.map
|
package/skeleton.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skeleton.js","sourceRoot":"","sources":["../src/skeleton.tsx"],"names":[],"mappings":";AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAA;AAI5B;;;;GAIG;AACH,MAAM,UAAU,QAAQ,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAiB;IAC7D,OAAO,CACL,6BACc,MAAM,EAClB,SAAS,EAAE,EAAE,CAAC,qEAAqE,EAAE,SAAS,CAAC,KAC3F,KAAK,GACT,CACH,CAAA;AACH,CAAC","sourcesContent":["import * as React from 'react'\nimport { cn } from './utils'\n\nexport type SkeletonProps = React.HTMLAttributes<HTMLDivElement>\n\n/**\n * Skeleton placeholder with a left→right shimmer sweep (`.appkit-shimmer` in\n * tokens.css — pure CSS, so it works server-rendered). Reduced-motion users get\n * a static placeholder.\n */\nexport function Skeleton({ className, ...props }: SkeletonProps) {\n return (\n <div\n aria-hidden=\"true\"\n className={cn('appkit-shimmer relative overflow-hidden rounded-md bg-border-subtle', className)}\n {...props}\n />\n )\n}\n"]}
|
package/slider.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export type SliderProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type'>;
|
|
3
|
+
/**
|
|
4
|
+
* Slider — a tokenized range input for bounded, imprecise values (percent
|
|
5
|
+
* complete, opacity, weighting). It stays a native `<input type="range">` so
|
|
6
|
+
* keyboard, touch, and assistive-technology behaviour come for free; only the
|
|
7
|
+
* track and thumb are restyled, through `accent-color`.
|
|
8
|
+
*
|
|
9
|
+
* For an exact number, use `Input type="number"` — a slider says "roughly
|
|
10
|
+
* this much", and dressing up a precise field as one loses that meaning.
|
|
11
|
+
*/
|
|
12
|
+
export declare const Slider: React.ForwardRefExoticComponent<SliderProps & React.RefAttributes<HTMLInputElement>>;
|
|
13
|
+
//# sourceMappingURL=slider.d.ts.map
|
package/slider.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slider.d.ts","sourceRoot":"","sources":["../src/slider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAG9B,MAAM,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,mBAAmB,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC,CAAA;AAEnF;;;;;;;;GAQG;AACH,eAAO,MAAM,MAAM,sFAclB,CAAA"}
|
package/slider.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { cn } from './utils.js';
|
|
4
|
+
/**
|
|
5
|
+
* Slider — a tokenized range input for bounded, imprecise values (percent
|
|
6
|
+
* complete, opacity, weighting). It stays a native `<input type="range">` so
|
|
7
|
+
* keyboard, touch, and assistive-technology behaviour come for free; only the
|
|
8
|
+
* track and thumb are restyled, through `accent-color`.
|
|
9
|
+
*
|
|
10
|
+
* For an exact number, use `Input type="number"` — a slider says "roughly
|
|
11
|
+
* this much", and dressing up a precise field as one loses that meaning.
|
|
12
|
+
*/
|
|
13
|
+
export const Slider = React.forwardRef(({ className, ...props }, ref) => (_jsx("input", { ref: ref, type: "range", className: cn('h-1.5 w-full cursor-pointer appearance-none rounded-full bg-border accent-primary', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-offset-2 focus-visible:ring-offset-bg', 'disabled:cursor-not-allowed disabled:opacity-50', className), ...props })));
|
|
14
|
+
Slider.displayName = 'Slider';
|
|
15
|
+
//# sourceMappingURL=slider.js.map
|
package/slider.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slider.js","sourceRoot":"","sources":["../src/slider.tsx"],"names":[],"mappings":";AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAA;AAI5B;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG,KAAK,CAAC,UAAU,CACpC,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,GAAG,EAAE,EAAE,CAAC,CAChC,gBACE,GAAG,EAAE,GAAG,EACR,IAAI,EAAC,OAAO,EACZ,SAAS,EAAE,EAAE,CACX,mFAAmF,EACnF,qIAAqI,EACrI,iDAAiD,EACjD,SAAS,CACV,KACG,KAAK,GACT,CACH,CACF,CAAA;AACD,MAAM,CAAC,WAAW,GAAG,QAAQ,CAAA","sourcesContent":["import * as React from 'react'\nimport { cn } from './utils'\n\nexport type SliderProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type'>\n\n/**\n * Slider — a tokenized range input for bounded, imprecise values (percent\n * complete, opacity, weighting). It stays a native `<input type=\"range\">` so\n * keyboard, touch, and assistive-technology behaviour come for free; only the\n * track and thumb are restyled, through `accent-color`.\n *\n * For an exact number, use `Input type=\"number\"` — a slider says \"roughly\n * this much\", and dressing up a precise field as one loses that meaning.\n */\nexport const Slider = React.forwardRef<HTMLInputElement, SliderProps>(\n ({ className, ...props }, ref) => (\n <input\n ref={ref}\n type=\"range\"\n className={cn(\n 'h-1.5 w-full cursor-pointer appearance-none rounded-full bg-border accent-primary',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-offset-2 focus-visible:ring-offset-bg',\n 'disabled:cursor-not-allowed disabled:opacity-50',\n className,\n )}\n {...props}\n />\n ),\n)\nSlider.displayName = 'Slider'\n"]}
|
package/sortable-th.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { type ListSearchParams } from './list-params.js';
|
|
3
|
+
type SortLinkProps = {
|
|
4
|
+
basePath: string;
|
|
5
|
+
currentParams: ListSearchParams;
|
|
6
|
+
column: string;
|
|
7
|
+
active: boolean;
|
|
8
|
+
dir: 'asc' | 'desc';
|
|
9
|
+
align?: 'left' | 'right';
|
|
10
|
+
sortParamKey?: string;
|
|
11
|
+
dirParamKey?: string;
|
|
12
|
+
pageParamKey?: string;
|
|
13
|
+
children: React.ReactNode;
|
|
14
|
+
};
|
|
15
|
+
/** Sortable header for tables built from the <Table> primitives. */
|
|
16
|
+
export declare function SortableTh({ className, ...props }: SortLinkProps & {
|
|
17
|
+
className?: string;
|
|
18
|
+
}): React.JSX.Element;
|
|
19
|
+
/**
|
|
20
|
+
* Sortable header for raw `<table>` lists — renders a plain
|
|
21
|
+
* `<th className="px-3 py-2">` and derives `active` from the current `sort`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function SortTh({ sort, className, ...props }: Omit<SortLinkProps, 'active'> & {
|
|
24
|
+
sort: string;
|
|
25
|
+
className?: string;
|
|
26
|
+
}): React.JSX.Element;
|
|
27
|
+
export {};
|
|
28
|
+
//# sourceMappingURL=sortable-th.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sortable-th.d.ts","sourceRoot":"","sources":["../src/sortable-th.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAG9B,OAAO,EAAa,KAAK,gBAAgB,EAAE,MAAM,eAAe,CAAA;AAIhE,KAAK,aAAa,GAAG;IACnB,QAAQ,EAAE,MAAM,CAAA;IAChB,aAAa,EAAE,gBAAgB,CAAA;IAC/B,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,OAAO,CAAA;IACf,GAAG,EAAE,KAAK,GAAG,MAAM,CAAA;IACnB,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IACxB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;CAC1B,CAAA;AA8CD,oEAAoE;AACpE,wBAAgB,UAAU,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,aAAa,GAAG;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,qBAMzF;AAED;;;GAGG;AACH,wBAAgB,MAAM,CAAC,EACrB,IAAI,EACJ,SAAS,EACT,GAAG,KAAK,EACT,EAAE,IAAI,CAAC,aAAa,EAAE,QAAQ,CAAC,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,qBAMtE"}
|
package/sortable-th.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { ArrowDown, ArrowUp, ArrowUpDown } from 'lucide-react';
|
|
5
|
+
import { TableHead } from './table.js';
|
|
6
|
+
import { mergeHref } from './list-params.js';
|
|
7
|
+
import { useListNavClick } from './list-nav.js';
|
|
8
|
+
import { cn } from './utils.js';
|
|
9
|
+
/**
|
|
10
|
+
* The clickable header label: column name + sort caret, linking to the toggled
|
|
11
|
+
* sort URL. Clicking an inactive column sorts ascending; clicking the active
|
|
12
|
+
* column flips direction. Page resets to 1.
|
|
13
|
+
*/
|
|
14
|
+
function SortLink({ basePath, currentParams, column, active, dir, align = 'left', sortParamKey = 'sort', dirParamKey = 'dir', pageParamKey = 'page', children, }) {
|
|
15
|
+
const nextDir = active && dir === 'asc' ? 'desc' : 'asc';
|
|
16
|
+
const href = mergeHref(basePath, currentParams, {
|
|
17
|
+
[sortParamKey]: column,
|
|
18
|
+
[dirParamKey]: nextDir,
|
|
19
|
+
[pageParamKey]: 1,
|
|
20
|
+
});
|
|
21
|
+
const navClick = useListNavClick(href);
|
|
22
|
+
return (_jsxs("a", { href: href, onClick: navClick, className: cn('inline-flex items-center gap-1.5 hover:text-fg', align === 'right' && 'flex-row-reverse'), children: [children, active ? (dir === 'asc' ? (_jsx(ArrowUp, { size: 12, className: "text-fg" })) : (_jsx(ArrowDown, { size: 12, className: "text-fg" }))) : (_jsx(ArrowUpDown, { size: 12, className: "text-fg-subtle/60" }))] }));
|
|
23
|
+
}
|
|
24
|
+
/** Sortable header for tables built from the <Table> primitives. */
|
|
25
|
+
export function SortableTh({ className, ...props }) {
|
|
26
|
+
return (_jsx(TableHead, { className: className, children: _jsx(SortLink, { ...props }) }));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Sortable header for raw `<table>` lists — renders a plain
|
|
30
|
+
* `<th className="px-3 py-2">` and derives `active` from the current `sort`.
|
|
31
|
+
*/
|
|
32
|
+
export function SortTh({ sort, className, ...props }) {
|
|
33
|
+
return (_jsx("th", { className: cn('px-3 py-2', className), children: _jsx(SortLink, { ...props, active: sort === props.column }) }));
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=sortable-th.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sortable-th.js","sourceRoot":"","sources":["../src/sortable-th.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC9D,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AACnC,OAAO,EAAE,SAAS,EAAyB,MAAM,eAAe,CAAA;AAChE,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAC5C,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAA;AAe5B;;;;GAIG;AACH,SAAS,QAAQ,CAAC,EAChB,QAAQ,EACR,aAAa,EACb,MAAM,EACN,MAAM,EACN,GAAG,EACH,KAAK,GAAG,MAAM,EACd,YAAY,GAAG,MAAM,EACrB,WAAW,GAAG,KAAK,EACnB,YAAY,GAAG,MAAM,EACrB,QAAQ,GACM;IACd,MAAM,OAAO,GAAmB,MAAM,IAAI,GAAG,KAAK,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAA;IACxE,MAAM,IAAI,GAAG,SAAS,CAAC,QAAQ,EAAE,aAAa,EAAE;QAC9C,CAAC,YAAY,CAAC,EAAE,MAAM;QACtB,CAAC,WAAW,CAAC,EAAE,OAAO;QACtB,CAAC,YAAY,CAAC,EAAE,CAAC;KAClB,CAAC,CAAA;IACF,MAAM,QAAQ,GAAG,eAAe,CAAC,IAAI,CAAC,CAAA;IACtC,OAAO,CACL,aACE,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,QAAQ,EACjB,SAAS,EAAE,EAAE,CAAC,gDAAgD,EAAE,KAAK,KAAK,OAAO,IAAI,kBAAkB,CAAC,aAEvG,QAAQ,EACR,MAAM,CAAC,CAAC,CAAC,CACR,GAAG,KAAK,KAAK,CAAC,CAAC,CAAC,CACd,KAAC,OAAO,IAAC,IAAI,EAAE,EAAE,EAAE,SAAS,EAAC,SAAS,GAAG,CAC1C,CAAC,CAAC,CAAC,CACF,KAAC,SAAS,IAAC,IAAI,EAAE,EAAE,EAAE,SAAS,EAAC,SAAS,GAAG,CAC5C,CACF,CAAC,CAAC,CAAC,CACF,KAAC,WAAW,IAAC,IAAI,EAAE,EAAE,EAAE,SAAS,EAAC,mBAAmB,GAAG,CACxD,IACC,CACL,CAAA;AACH,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,UAAU,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAA0C;IACxF,OAAO,CACL,KAAC,SAAS,IAAC,SAAS,EAAE,SAAS,YAC7B,KAAC,QAAQ,OAAK,KAAK,GAAI,GACb,CACb,CAAA;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,MAAM,CAAC,EACrB,IAAI,EACJ,SAAS,EACT,GAAG,KAAK,EAC6D;IACrE,OAAO,CACL,aAAI,SAAS,EAAE,EAAE,CAAC,WAAW,EAAE,SAAS,CAAC,YACvC,KAAC,QAAQ,OAAK,KAAK,EAAE,MAAM,EAAE,IAAI,KAAK,KAAK,CAAC,MAAM,GAAI,GACnD,CACN,CAAA;AACH,CAAC","sourcesContent":["'use client'\n\nimport * as React from 'react'\nimport { ArrowDown, ArrowUp, ArrowUpDown } from 'lucide-react'\nimport { TableHead } from './table'\nimport { mergeHref, type ListSearchParams } from './list-params'\nimport { useListNavClick } from './list-nav'\nimport { cn } from './utils'\n\ntype SortLinkProps = {\n basePath: string\n currentParams: ListSearchParams\n column: string\n active: boolean\n dir: 'asc' | 'desc'\n align?: 'left' | 'right'\n sortParamKey?: string\n dirParamKey?: string\n pageParamKey?: string\n children: React.ReactNode\n}\n\n/**\n * The clickable header label: column name + sort caret, linking to the toggled\n * sort URL. Clicking an inactive column sorts ascending; clicking the active\n * column flips direction. Page resets to 1.\n */\nfunction SortLink({\n basePath,\n currentParams,\n column,\n active,\n dir,\n align = 'left',\n sortParamKey = 'sort',\n dirParamKey = 'dir',\n pageParamKey = 'page',\n children,\n}: SortLinkProps) {\n const nextDir: 'asc' | 'desc' = active && dir === 'asc' ? 'desc' : 'asc'\n const href = mergeHref(basePath, currentParams, {\n [sortParamKey]: column,\n [dirParamKey]: nextDir,\n [pageParamKey]: 1,\n })\n const navClick = useListNavClick(href)\n return (\n <a\n href={href}\n onClick={navClick}\n className={cn('inline-flex items-center gap-1.5 hover:text-fg', align === 'right' && 'flex-row-reverse')}\n >\n {children}\n {active ? (\n dir === 'asc' ? (\n <ArrowUp size={12} className=\"text-fg\" />\n ) : (\n <ArrowDown size={12} className=\"text-fg\" />\n )\n ) : (\n <ArrowUpDown size={12} className=\"text-fg-subtle/60\" />\n )}\n </a>\n )\n}\n\n/** Sortable header for tables built from the <Table> primitives. */\nexport function SortableTh({ className, ...props }: SortLinkProps & { className?: string }) {\n return (\n <TableHead className={className}>\n <SortLink {...props} />\n </TableHead>\n )\n}\n\n/**\n * Sortable header for raw `<table>` lists — renders a plain\n * `<th className=\"px-3 py-2\">` and derives `active` from the current `sort`.\n */\nexport function SortTh({\n sort,\n className,\n ...props\n}: Omit<SortLinkProps, 'active'> & { sort: string; className?: string }) {\n return (\n <th className={cn('px-3 py-2', className)}>\n <SortLink {...props} active={sort === props.column} />\n </th>\n )\n}\n"]}
|
package/sparkline.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type SparklineTone = 'up' | 'down' | 'good' | 'bad' | 'neutral';
|
|
2
|
+
export type SparklineProps = {
|
|
3
|
+
points: number[];
|
|
4
|
+
tone?: SparklineTone;
|
|
5
|
+
auto?: boolean;
|
|
6
|
+
goodWhenRising?: boolean;
|
|
7
|
+
dots?: boolean;
|
|
8
|
+
stroke?: string;
|
|
9
|
+
area?: boolean;
|
|
10
|
+
strokeWidth?: number;
|
|
11
|
+
className?: string;
|
|
12
|
+
ariaLabel?: string;
|
|
13
|
+
noTrendLabel?: string;
|
|
14
|
+
};
|
|
15
|
+
export declare function Sparkline({ points, tone, auto, goodWhenRising, dots, stroke, area, strokeWidth, className, ariaLabel, noTrendLabel, }: SparklineProps): import("react").JSX.Element;
|
|
16
|
+
//# sourceMappingURL=sparkline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sparkline.d.ts","sourceRoot":"","sources":["../src/sparkline.tsx"],"names":[],"mappings":"AAEA,MAAM,MAAM,aAAa,GAAG,IAAI,GAAG,MAAM,GAAG,MAAM,GAAG,KAAK,GAAG,SAAS,CAAA;AAEtE,MAAM,MAAM,cAAc,GAAG;IAC3B,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,IAAI,CAAC,EAAE,aAAa,CAAA;IACpB,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB,CAAA;AAUD,wBAAgB,SAAS,CAAC,EACxB,MAAM,EACN,IAAgB,EAChB,IAAI,EACJ,cAAqB,EACrB,IAAY,EACZ,MAAM,EACN,IAAY,EACZ,WAAiB,EACjB,SAAS,EACT,SAAS,EACT,YAA8B,GAC/B,EAAE,cAAc,+BA6ChB"}
|