@sableui/react 0.11.0 → 0.13.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/AGENTS.md +10 -4
- package/CHANGELOG.md +199 -0
- package/DESIGN.md +3 -3
- package/README.md +15 -1
- package/bin/sable.mjs +254 -10
- package/dist/components/app-header/app-header.meta.js +2 -1
- package/dist/components/button/button.js +3 -0
- package/dist/components/button/button.meta.js +1 -1
- package/dist/components/button/button.variants.js +1 -1
- package/dist/components/button-group/button-group.js +17 -0
- package/dist/components/button-group/button-group.meta.js +72 -0
- package/dist/components/button-group/index.js +7 -0
- package/dist/components/caret-menu/caret-menu.js +221 -0
- package/dist/components/caret-menu/caret-menu.meta.js +119 -0
- package/dist/components/caret-menu/index.js +8 -0
- package/dist/components/chat-composer/chat-composer.js +30 -7
- package/dist/components/chat-composer/chat-composer.meta.js +27 -8
- package/dist/components/combobox/combobox.meta.js +26 -0
- package/dist/components/input-group/index.js +7 -0
- package/dist/components/input-group/input-group.js +66 -0
- package/dist/components/input-group/input-group.meta.js +106 -0
- package/dist/components/menu/index.js +2 -0
- package/dist/components/menu/menu.js +51 -9
- package/dist/components/menu/menu.meta.js +11 -2
- package/dist/components/menubar/index.js +8 -0
- package/dist/components/menubar/menubar.js +42 -0
- package/dist/components/menubar/menubar.meta.js +99 -0
- package/dist/components/message-scroller/message-scroller.meta.js +4 -0
- package/dist/components/navigation-menu/index.js +19 -0
- package/dist/components/navigation-menu/navigation-menu.js +108 -0
- package/dist/components/navigation-menu/navigation-menu.meta.js +141 -0
- package/dist/components/overflow-menu/overflow-menu.meta.js +13 -1
- package/dist/components/pagination/pagination.js +11 -5
- package/dist/components/pagination/pagination.meta.js +21 -3
- package/dist/components/scroller/scroller.js +8 -4
- package/dist/components/scroller/scroller.meta.js +6 -0
- package/dist/components/sheet/sheet.js +82 -9
- package/dist/components/sheet/sheet.meta.js +41 -4
- package/dist/components/sheet/sheet.variants.js +8 -1
- package/dist/components/sidebar/sidebar.js +13 -2
- package/dist/components/sidebar/sidebar.meta.js +10 -0
- package/dist/components/streaming-text/streaming-text.meta.js +5 -1
- package/dist/file-hashes.json +49 -33
- package/dist/icon-sets/reicon.js +2 -0
- package/dist/index.d.ts +660 -164
- package/dist/index.js +12 -1
- package/dist/lib/palette.js +46 -6
- package/dist/lib/taxonomy.js +5 -0
- package/dist/lib/use-caret-trigger.js +187 -0
- package/dist/lib/use-copy.js +6 -1
- package/dist/lib/use-smooth-wheel.js +164 -0
- package/dist/palette.css +243 -165
- package/dist/patterns/chat-panel.js +1 -1
- package/dist/patterns/chat-panel.meta.js +1 -0
- package/dist/styles.css +25 -0
- package/llms/components/app-header.md +1 -1
- package/llms/components/button-group.md +55 -0
- package/llms/components/button.md +1 -1
- package/llms/components/caret-menu.md +100 -0
- package/llms/components/chat-composer.md +24 -7
- package/llms/components/combobox.md +12 -0
- package/llms/components/input-group.md +78 -0
- package/llms/components/menu.md +16 -1
- package/llms/components/menubar.md +110 -0
- package/llms/components/message-scroller.md +1 -0
- package/llms/components/navigation-menu.md +113 -0
- package/llms/components/overflow-menu.md +26 -1
- package/llms/components/pagination.md +15 -3
- package/llms/components/scroller.md +1 -0
- package/llms/components/sheet.md +18 -3
- package/llms/components/sidebar.md +2 -0
- package/llms/components/streaming-text.md +2 -1
- package/llms/patterns/chat-panel.md +4 -1
- package/llms/reference/token-intent.md +1 -1
- package/llms/tasks/building-a-chat.md +158 -0
- package/llms/tasks/building-with-sable.md +4 -1
- package/llms/tasks/converting-an-app.md +7 -0
- package/llms/tasks/extending-a-component.md +170 -0
- package/llms/tasks/getting-started.md +22 -4
- package/llms/tasks/migrating-from-shadcn.md +351 -0
- package/llms-full.txt +1322 -27
- package/llms.txt +13 -4
- package/package.json +3 -2
- package/registry.json +979 -23
- package/scripts/lint-rules.mjs +35 -0
- package/scripts/wiring.mjs +541 -0
- package/skills/entry/SKILL.md +3 -2
- package/skills/sable/SKILL.md +26 -5
- package/targets/ios.json +45 -0
- package/dist/lib/use-wheel-to-horizontal.js +0 -65
package/dist/index.js
CHANGED
|
@@ -42,11 +42,14 @@ export * from "./layout/index.js";
|
|
|
42
42
|
export * from "./icon/index.js";
|
|
43
43
|
export * from "./motion/index.js";
|
|
44
44
|
export * from "./components/button/index.js";
|
|
45
|
+
export * from "./components/button-group/index.js";
|
|
45
46
|
export * from "./components/card/index.js";
|
|
47
|
+
export * from "./components/caret-menu/index.js";
|
|
46
48
|
export * from "./components/badge/index.js";
|
|
47
49
|
export * from "./components/dialog/index.js";
|
|
48
50
|
export * from "./components/separator/index.js";
|
|
49
51
|
export * from "./components/input/index.js";
|
|
52
|
+
export * from "./components/input-group/index.js";
|
|
50
53
|
export * from "./components/textarea/index.js";
|
|
51
54
|
export * from "./components/number-field/index.js";
|
|
52
55
|
export * from "./components/otp-field/index.js";
|
|
@@ -84,6 +87,8 @@ export * from "./components/radio/index.js";
|
|
|
84
87
|
export * from "./components/select/index.js";
|
|
85
88
|
export * from "./components/popover/index.js";
|
|
86
89
|
export * from "./components/menu/index.js";
|
|
90
|
+
export * from "./components/navigation-menu/index.js";
|
|
91
|
+
export * from "./components/menubar/index.js";
|
|
87
92
|
export * from "./components/sheet/index.js";
|
|
88
93
|
export * from "./components/table/index.js";
|
|
89
94
|
export * from "./components/data-table/index.js";
|
|
@@ -161,9 +166,13 @@ import { Density, DensityProvider, useDensity, useAnchoredDensity, useTypeTier }
|
|
|
161
166
|
import { PresentationProvider, usePresentation } from "./lib/presentation.js";
|
|
162
167
|
import { useScrollActivity } from "./lib/use-scroll-activity.js";
|
|
163
168
|
import { useCopy } from "./lib/use-copy.js";
|
|
169
|
+
import { useSmoothWheel } from "./lib/use-smooth-wheel.js";
|
|
170
|
+
import {
|
|
171
|
+
useCaretTrigger
|
|
172
|
+
} from "./lib/use-caret-trigger.js";
|
|
164
173
|
import { STATUS_TONES, STATUS_TONE_ICON } from "./lib/status-tone.js";
|
|
165
174
|
import { PALETTE_COLORS, PALETTE_FAMILIES, PALETTE_SCALES, PALETTE_STEPS, PALETTE_TONES, isPaletteColor } from "./lib/palette.js";
|
|
166
|
-
var SABLE_UI_VERSION = "0.
|
|
175
|
+
var SABLE_UI_VERSION = "0.13.0";
|
|
167
176
|
export {
|
|
168
177
|
ART_TOKENS,
|
|
169
178
|
CHART_SERIES_COUNT,
|
|
@@ -214,10 +223,12 @@ export {
|
|
|
214
223
|
themeManifestById,
|
|
215
224
|
themeManifests,
|
|
216
225
|
useAnchoredDensity,
|
|
226
|
+
useCaretTrigger,
|
|
217
227
|
useCopy,
|
|
218
228
|
useDensity,
|
|
219
229
|
usePresentation,
|
|
220
230
|
useSableTheme,
|
|
221
231
|
useScrollActivity,
|
|
232
|
+
useSmoothWheel,
|
|
222
233
|
useTypeTier
|
|
223
234
|
};
|
package/dist/lib/palette.js
CHANGED
|
@@ -4,7 +4,9 @@ var PALETTE_FAMILIES = [
|
|
|
4
4
|
{ name: "red", hue: 25, light: { l: 0.6, c: 0.21 }, dark: { l: 0.64, c: 0.2 } },
|
|
5
5
|
{ name: "orange", hue: 50, light: { l: 0.69, c: 0.18 }, dark: { l: 0.71, c: 0.17 } },
|
|
6
6
|
// Yellow is only yellow when it is light; at 0.6 it is olive.
|
|
7
|
-
|
|
7
|
+
// In dark mode it sits a little deeper, or 800–950 crowd into the room
|
|
8
|
+
// left between it and the ink.
|
|
9
|
+
{ name: "yellow", hue: 92, light: { l: 0.83, c: 0.16 }, dark: { l: 0.78, c: 0.15 } },
|
|
8
10
|
{ name: "green", hue: 145, light: { l: 0.64, c: 0.17 }, dark: { l: 0.68, c: 0.16 } },
|
|
9
11
|
{ name: "teal", hue: 190, light: { l: 0.7, c: 0.12 }, dark: { l: 0.74, c: 0.12 } },
|
|
10
12
|
{ name: "blue", hue: 262, light: { l: 0.6, c: 0.19 }, dark: { l: 0.64, c: 0.17 } },
|
|
@@ -69,10 +71,15 @@ var NEUTRAL_MIX = {
|
|
|
69
71
|
900: 90,
|
|
70
72
|
950: 95
|
|
71
73
|
};
|
|
72
|
-
var
|
|
73
|
-
light: { 50: 0.
|
|
74
|
-
dark: { 50: 0.
|
|
74
|
+
var TONE_LADDER = {
|
|
75
|
+
light: { 50: 0.975, 100: 0.945, 200: 0.895, 300: 0.82, 400: 0.725, 500: 0.63, 600: 0.55, 700: 0.47, 800: 0.4, 900: 0.33, 950: 0.27 },
|
|
76
|
+
dark: { 50: 0.215, 100: 0.25, 200: 0.31, 300: 0.39, 400: 0.5, 500: 0.6, 600: 0.69, 700: 0.77, 800: 0.84, 900: 0.895, 950: 0.94 }
|
|
75
77
|
};
|
|
78
|
+
var TONE_CHROMA_ENDS = {
|
|
79
|
+
light: { page: 0.12, ink: 0.52 },
|
|
80
|
+
dark: { page: 0.25, ink: 0.3 }
|
|
81
|
+
};
|
|
82
|
+
var TONE_SNAP = 1e4;
|
|
76
83
|
var LEAN = { amount: 0.5, cap: 15, fullAt: 0.06, maxHarmony: 2 };
|
|
77
84
|
var LEAN_REACH = LEAN.cap * LEAN.maxHarmony;
|
|
78
85
|
var clamp = (n, lo, hi) => Math.min(hi, Math.max(lo, n));
|
|
@@ -85,6 +92,34 @@ function stepFrom(anchor, step, mode) {
|
|
|
85
92
|
const curve = STEP_CURVES[mode][step];
|
|
86
93
|
return { l: anchor.l + (curve.target - anchor.l) * curve.pull, c: anchor.c * curve.chroma };
|
|
87
94
|
}
|
|
95
|
+
function ladderDepth(l, mode) {
|
|
96
|
+
const top = TONE_LADDER[mode][50];
|
|
97
|
+
return mode === "light" ? top - l : l - top;
|
|
98
|
+
}
|
|
99
|
+
function toneAnchor(l, mode) {
|
|
100
|
+
const ladder = TONE_LADDER[mode];
|
|
101
|
+
let depth = 0;
|
|
102
|
+
for (let i = 1; i < PALETTE_STEPS.length; i++) {
|
|
103
|
+
const [before, at] = [ladder[PALETTE_STEPS[i - 1]], ladder[PALETTE_STEPS[i]]];
|
|
104
|
+
const mid = (before + at) / 2;
|
|
105
|
+
depth += Math.abs(at - before) * clamp((mode === "light" ? mid - l : l - mid) * TONE_SNAP, 0, 1);
|
|
106
|
+
}
|
|
107
|
+
return depth;
|
|
108
|
+
}
|
|
109
|
+
function toneStep(tone, step, mode) {
|
|
110
|
+
const end = ladderDepth(TONE_LADDER[mode][950], mode);
|
|
111
|
+
const at = ladderDepth(TONE_LADDER[mode][step], mode);
|
|
112
|
+
const anchor = toneAnchor(tone.l, mode);
|
|
113
|
+
const own = ladderDepth(tone.l, mode);
|
|
114
|
+
const pageSide = clamp((anchor - at) * TONE_SNAP, 0, 1);
|
|
115
|
+
const towardPage = at * own / Math.max(anchor, 1e-3);
|
|
116
|
+
const towardInk = own + (at - anchor) * (end - own) / Math.max(end - anchor, 1e-3);
|
|
117
|
+
const depth = pageSide * towardPage + (1 - pageSide) * towardInk;
|
|
118
|
+
const ends = TONE_CHROMA_ENDS[mode];
|
|
119
|
+
const share = pageSide * (1 - (1 - ends.page) * ((anchor - at) / Math.max(anchor, 1e-3))) + (1 - pageSide) * (1 - (1 - ends.ink) * ((at - anchor) / Math.max(end - anchor, 1e-3)));
|
|
120
|
+
const top = TONE_LADDER[mode][50];
|
|
121
|
+
return { l: mode === "light" ? top - depth : top + depth, c: tone.c * share, anchor: Math.abs(at - anchor) * TONE_SNAP < 1 };
|
|
122
|
+
}
|
|
88
123
|
export {
|
|
89
124
|
LEAN,
|
|
90
125
|
LEAN_REACH,
|
|
@@ -96,8 +131,13 @@ export {
|
|
|
96
131
|
PALETTE_STEPS,
|
|
97
132
|
PALETTE_TONES,
|
|
98
133
|
STEP_CURVES,
|
|
99
|
-
|
|
134
|
+
TONE_CHROMA_ENDS,
|
|
135
|
+
TONE_LADDER,
|
|
136
|
+
TONE_SNAP,
|
|
100
137
|
familyHue,
|
|
101
138
|
isPaletteColor,
|
|
102
|
-
|
|
139
|
+
ladderDepth,
|
|
140
|
+
stepFrom,
|
|
141
|
+
toneAnchor,
|
|
142
|
+
toneStep
|
|
103
143
|
};
|
package/dist/lib/taxonomy.js
CHANGED
|
@@ -24,6 +24,8 @@ var COMPONENT_TAXONOMY = {
|
|
|
24
24
|
options" pattern) — that its list opens in a layer is how it works, not
|
|
25
25
|
what it is. Copying is `useCopy` on a Button, not a component. */
|
|
26
26
|
Button: ["Actions", "either"],
|
|
27
|
+
ButtonGroup: ["Actions", "either"],
|
|
28
|
+
Menubar: ["Actions", "either"],
|
|
27
29
|
OverflowMenu: ["Actions", "either"],
|
|
28
30
|
Toolbar: ["Actions", "either"],
|
|
29
31
|
/* ── Forms ────────────────────────────────────────────────────────────────
|
|
@@ -38,6 +40,7 @@ var COMPONENT_TAXONOMY = {
|
|
|
38
40
|
FileDropzone: ["Forms", "page"],
|
|
39
41
|
Form: ["Forms", "either"],
|
|
40
42
|
Input: ["Forms", "either"],
|
|
43
|
+
InputGroup: ["Forms", "either"],
|
|
41
44
|
Label: ["Forms", "either"],
|
|
42
45
|
NumberField: ["Forms", "either"],
|
|
43
46
|
OtpField: ["Forms", "either"],
|
|
@@ -64,6 +67,7 @@ var COMPONENT_TAXONOMY = {
|
|
|
64
67
|
Toggle: ["Selection", "either"],
|
|
65
68
|
/* ── Overlays ─────────────────────────────────────────────────────────────
|
|
66
69
|
Anything that renders above the page in its own layer. */
|
|
70
|
+
CaretMenu: ["Overlays", "either"],
|
|
67
71
|
CommandMenu: ["Overlays", "either"],
|
|
68
72
|
ContextCard: ["Overlays", "page"],
|
|
69
73
|
ContextMenu: ["Overlays", "either"],
|
|
@@ -81,6 +85,7 @@ var COMPONENT_TAXONOMY = {
|
|
|
81
85
|
stay together; `Tree` is the hierarchy browser. */
|
|
82
86
|
Breadcrumbs: ["Navigation", "either"],
|
|
83
87
|
BottomNav: ["Navigation", "page"],
|
|
88
|
+
NavigationMenu: ["Navigation", "page"],
|
|
84
89
|
Pagination: ["Navigation", "either"],
|
|
85
90
|
ShowMore: ["Navigation", "page"],
|
|
86
91
|
SideNav: ["Navigation", "page"],
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/lib/use-caret-trigger.ts
|
|
4
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
5
|
+
function useCaretTrigger({ triggers }) {
|
|
6
|
+
const [field, setField] = useState(null);
|
|
7
|
+
const [match, setMatch] = useState(null);
|
|
8
|
+
const dismissedAt = useRef(null);
|
|
9
|
+
const composing = useRef(false);
|
|
10
|
+
const triggersRef = useRef(triggers);
|
|
11
|
+
triggersRef.current = triggers;
|
|
12
|
+
const evaluate = useCallback(() => {
|
|
13
|
+
if (!field || composing.current) return;
|
|
14
|
+
const next = field.ownerDocument.activeElement === field ? findMatch(field, triggersRef.current) : null;
|
|
15
|
+
if (!next || next.start !== dismissedAt.current) dismissedAt.current = null;
|
|
16
|
+
const visible = next && dismissedAt.current === null ? next : null;
|
|
17
|
+
setMatch((prev) => sameMatch(prev, visible) ? prev : visible);
|
|
18
|
+
}, [field]);
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (!field) return;
|
|
21
|
+
const doc = field.ownerDocument;
|
|
22
|
+
const onSelection = () => {
|
|
23
|
+
if (doc.activeElement === field) evaluate();
|
|
24
|
+
};
|
|
25
|
+
const onCompositionStart = () => {
|
|
26
|
+
composing.current = true;
|
|
27
|
+
};
|
|
28
|
+
const onCompositionEnd = () => {
|
|
29
|
+
composing.current = false;
|
|
30
|
+
setTimeout(evaluate, 0);
|
|
31
|
+
};
|
|
32
|
+
const onBlur = () => {
|
|
33
|
+
dismissedAt.current = null;
|
|
34
|
+
setMatch(null);
|
|
35
|
+
};
|
|
36
|
+
const onInput = (event) => {
|
|
37
|
+
if (event.target === field) evaluate();
|
|
38
|
+
};
|
|
39
|
+
doc.addEventListener("input", onInput);
|
|
40
|
+
field.addEventListener("focus", evaluate);
|
|
41
|
+
field.addEventListener("click", evaluate);
|
|
42
|
+
field.addEventListener("keyup", evaluate);
|
|
43
|
+
field.addEventListener("compositionstart", onCompositionStart);
|
|
44
|
+
field.addEventListener("compositionend", onCompositionEnd);
|
|
45
|
+
field.addEventListener("blur", onBlur);
|
|
46
|
+
doc.addEventListener("selectionchange", onSelection);
|
|
47
|
+
return () => {
|
|
48
|
+
doc.removeEventListener("input", onInput);
|
|
49
|
+
field.removeEventListener("focus", evaluate);
|
|
50
|
+
field.removeEventListener("click", evaluate);
|
|
51
|
+
field.removeEventListener("keyup", evaluate);
|
|
52
|
+
field.removeEventListener("compositionstart", onCompositionStart);
|
|
53
|
+
field.removeEventListener("compositionend", onCompositionEnd);
|
|
54
|
+
field.removeEventListener("blur", onBlur);
|
|
55
|
+
doc.removeEventListener("selectionchange", onSelection);
|
|
56
|
+
};
|
|
57
|
+
}, [field, evaluate]);
|
|
58
|
+
const dismiss = useCallback(() => {
|
|
59
|
+
setMatch((current) => {
|
|
60
|
+
if (current) dismissedAt.current = current.start;
|
|
61
|
+
return null;
|
|
62
|
+
});
|
|
63
|
+
}, []);
|
|
64
|
+
const replace = useCallback(
|
|
65
|
+
(text) => {
|
|
66
|
+
if (!field || !match) return;
|
|
67
|
+
insertText(field, match.start, match.end, text);
|
|
68
|
+
dismissedAt.current = match.start;
|
|
69
|
+
evaluate();
|
|
70
|
+
},
|
|
71
|
+
[field, match, evaluate]
|
|
72
|
+
);
|
|
73
|
+
const getAnchorRect = useCallback(() => {
|
|
74
|
+
if (!field || !match) return null;
|
|
75
|
+
return caretRect(field, match.start);
|
|
76
|
+
}, [field, match]);
|
|
77
|
+
return { fieldRef: setField, field, match, getAnchorRect, replace, dismiss };
|
|
78
|
+
}
|
|
79
|
+
var BOUNDARY = /[\s([{"'“‘]/;
|
|
80
|
+
function findMatch(field, triggers) {
|
|
81
|
+
const { value, selectionStart, selectionEnd } = field;
|
|
82
|
+
if (selectionStart === null || selectionStart !== selectionEnd) return null;
|
|
83
|
+
return matchAt(value, selectionStart, triggers);
|
|
84
|
+
}
|
|
85
|
+
function matchAt(value, caret, triggers) {
|
|
86
|
+
for (let i = caret - 1; i >= 0; i--) {
|
|
87
|
+
const ch = value[i];
|
|
88
|
+
if (ch === "\n") return null;
|
|
89
|
+
const trigger = triggers.find((t) => t.char === ch);
|
|
90
|
+
if (trigger && opens(value, i, caret, trigger)) {
|
|
91
|
+
return { trigger, query: value.slice(i + 1, caret), start: i, end: caret };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
function opens(value, i, caret, trigger) {
|
|
97
|
+
const query = value.slice(i + 1, caret);
|
|
98
|
+
if (!trigger.allowSpaces && /\s/.test(query)) return false;
|
|
99
|
+
if (query.length > (trigger.maxQueryLength ?? 32)) return false;
|
|
100
|
+
if (/^\s/.test(query)) return false;
|
|
101
|
+
const before = i === 0 ? "" : value[i - 1];
|
|
102
|
+
if (before !== "" && !BOUNDARY.test(before)) return false;
|
|
103
|
+
if (trigger.atStart && value.slice(0, i).trim() !== "") return false;
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
function sameMatch(a, b) {
|
|
107
|
+
if (a === b) return true;
|
|
108
|
+
if (!a || !b) return false;
|
|
109
|
+
return a.trigger === b.trigger && a.query === b.query && a.start === b.start && a.end === b.end;
|
|
110
|
+
}
|
|
111
|
+
function insertText(field, start, end, text) {
|
|
112
|
+
field.focus();
|
|
113
|
+
field.setSelectionRange(start, end);
|
|
114
|
+
const doc = field.ownerDocument;
|
|
115
|
+
const inserted = text === "" ? typeof doc.execCommand === "function" && doc.execCommand("delete", false) : typeof doc.execCommand === "function" && doc.execCommand("insertText", false, text);
|
|
116
|
+
if (!inserted || field.value.slice(start, start + text.length) !== text) {
|
|
117
|
+
const next = field.value.slice(0, start) + text + field.value.slice(end);
|
|
118
|
+
const proto = field instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
|
|
119
|
+
Object.getOwnPropertyDescriptor(proto, "value")?.set?.call(field, next);
|
|
120
|
+
field.dispatchEvent(new Event("input", { bubbles: true }));
|
|
121
|
+
}
|
|
122
|
+
const caret = start + text.length;
|
|
123
|
+
field.setSelectionRange(caret, caret);
|
|
124
|
+
}
|
|
125
|
+
var MIRRORED = [
|
|
126
|
+
"boxSizing",
|
|
127
|
+
"width",
|
|
128
|
+
"borderTopWidth",
|
|
129
|
+
"borderRightWidth",
|
|
130
|
+
"borderBottomWidth",
|
|
131
|
+
"borderLeftWidth",
|
|
132
|
+
"paddingTop",
|
|
133
|
+
"paddingRight",
|
|
134
|
+
"paddingBottom",
|
|
135
|
+
"paddingLeft",
|
|
136
|
+
"fontFamily",
|
|
137
|
+
"fontSize",
|
|
138
|
+
"fontStyle",
|
|
139
|
+
"fontVariant",
|
|
140
|
+
"fontWeight",
|
|
141
|
+
"fontStretch",
|
|
142
|
+
"fontFeatureSettings",
|
|
143
|
+
"lineHeight",
|
|
144
|
+
"letterSpacing",
|
|
145
|
+
"wordSpacing",
|
|
146
|
+
"textIndent",
|
|
147
|
+
"textTransform",
|
|
148
|
+
"tabSize",
|
|
149
|
+
"direction"
|
|
150
|
+
];
|
|
151
|
+
function caretRect(field, index) {
|
|
152
|
+
const doc = field.ownerDocument;
|
|
153
|
+
const view = doc.defaultView ?? window;
|
|
154
|
+
const style = view.getComputedStyle(field);
|
|
155
|
+
const mirror = doc.createElement("div");
|
|
156
|
+
const s = mirror.style;
|
|
157
|
+
for (const prop of MIRRORED) s[prop] = style[prop];
|
|
158
|
+
s.position = "absolute";
|
|
159
|
+
s.visibility = "hidden";
|
|
160
|
+
s.top = "0";
|
|
161
|
+
s.left = "-9999px";
|
|
162
|
+
s.overflow = "hidden";
|
|
163
|
+
const multiline = field instanceof HTMLTextAreaElement;
|
|
164
|
+
s.whiteSpace = multiline ? "pre-wrap" : "pre";
|
|
165
|
+
s.overflowWrap = multiline ? "break-word" : "normal";
|
|
166
|
+
mirror.textContent = field.value.slice(0, index);
|
|
167
|
+
const marker = doc.createElement("span");
|
|
168
|
+
marker.textContent = field.value.slice(index) || "\u200B";
|
|
169
|
+
mirror.appendChild(marker);
|
|
170
|
+
doc.body.appendChild(mirror);
|
|
171
|
+
const box = field.getBoundingClientRect();
|
|
172
|
+
const lineHeight = Number.parseFloat(style.lineHeight) || marker.offsetHeight;
|
|
173
|
+
const rect = new DOMRect(
|
|
174
|
+
box.left + marker.offsetLeft - field.scrollLeft,
|
|
175
|
+
box.top + marker.offsetTop - field.scrollTop,
|
|
176
|
+
1,
|
|
177
|
+
lineHeight
|
|
178
|
+
);
|
|
179
|
+
mirror.remove();
|
|
180
|
+
return rect;
|
|
181
|
+
}
|
|
182
|
+
export {
|
|
183
|
+
caretRect,
|
|
184
|
+
findMatch,
|
|
185
|
+
matchAt,
|
|
186
|
+
useCaretTrigger
|
|
187
|
+
};
|
package/dist/lib/use-copy.js
CHANGED
|
@@ -11,7 +11,12 @@ function useCopy(text, { timeout = 1500 } = {}) {
|
|
|
11
11
|
const copy = useCallback(async () => {
|
|
12
12
|
if (typeof navigator === "undefined" || !navigator.clipboard) return false;
|
|
13
13
|
try {
|
|
14
|
-
|
|
14
|
+
const value = typeof text === "function" ? text() : text;
|
|
15
|
+
if (typeof value === "string") await navigator.clipboard.writeText(value);
|
|
16
|
+
else if (typeof ClipboardItem !== "undefined" && navigator.clipboard.write) {
|
|
17
|
+
const blob = value.then((t) => new Blob([t], { type: "text/plain" }));
|
|
18
|
+
await navigator.clipboard.write([new ClipboardItem({ "text/plain": blob })]);
|
|
19
|
+
} else await navigator.clipboard.writeText(await value);
|
|
15
20
|
} catch {
|
|
16
21
|
return false;
|
|
17
22
|
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/lib/use-smooth-wheel.ts
|
|
4
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
5
|
+
import { useMotion } from "../motion/MotionProvider.js";
|
|
6
|
+
var LINE_HEIGHT = 16;
|
|
7
|
+
var PAGE_HEIGHT = 400;
|
|
8
|
+
function toPixels(delta, mode) {
|
|
9
|
+
if (mode === 1) return delta * LINE_HEIGHT;
|
|
10
|
+
if (mode === 2) return delta * PAGE_HEIGHT;
|
|
11
|
+
return delta;
|
|
12
|
+
}
|
|
13
|
+
function isNotch(event) {
|
|
14
|
+
if (event.deltaMode !== 0) return true;
|
|
15
|
+
const legacy = event.wheelDeltaY;
|
|
16
|
+
if (legacy) return event.deltaX === 0 && legacy % 120 === 0;
|
|
17
|
+
return event.deltaX === 0 && Number.isInteger(event.deltaY) && Math.abs(event.deltaY) >= 50;
|
|
18
|
+
}
|
|
19
|
+
function innerScrolls(target, region, deltaY) {
|
|
20
|
+
for (let el = target instanceof Element ? target : null; el && el !== region; el = el.parentElement) {
|
|
21
|
+
if (!(el instanceof HTMLElement) || el.scrollHeight <= el.clientHeight) continue;
|
|
22
|
+
const overflow = getComputedStyle(el).overflowY;
|
|
23
|
+
if (overflow !== "auto" && overflow !== "scroll") continue;
|
|
24
|
+
const max = el.scrollHeight - el.clientHeight;
|
|
25
|
+
if (deltaY < 0 ? Math.round(el.scrollTop) > 0 : Math.round(el.scrollTop) < max) return true;
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
function cssOwnsMotion(el) {
|
|
30
|
+
const style = getComputedStyle(el);
|
|
31
|
+
return style.scrollBehavior === "smooth" || style.scrollSnapType !== "" && style.scrollSnapType !== "none";
|
|
32
|
+
}
|
|
33
|
+
var EASE_PER_FRAME = 0.14;
|
|
34
|
+
var FRAME_MS = 1e3 / 60;
|
|
35
|
+
var SETTLE_PX = 0.5;
|
|
36
|
+
function useSmoothWheel({
|
|
37
|
+
axis = "vertical",
|
|
38
|
+
mapVertical = true,
|
|
39
|
+
smooth = true
|
|
40
|
+
} = {}) {
|
|
41
|
+
const { enabled: motion } = useMotion();
|
|
42
|
+
const settings = useRef({ axis, mapVertical, smooth: smooth && motion });
|
|
43
|
+
settings.current = { axis, mapVertical, smooth: smooth && motion };
|
|
44
|
+
const horizontal = axis === "horizontal";
|
|
45
|
+
const active = smooth && motion || horizontal && mapVertical;
|
|
46
|
+
const nodeRef = useRef(null);
|
|
47
|
+
const anim = useRef(null);
|
|
48
|
+
const stop = useCallback(() => {
|
|
49
|
+
if (anim.current) cancelAnimationFrame(anim.current.frame);
|
|
50
|
+
anim.current = null;
|
|
51
|
+
}, []);
|
|
52
|
+
const step = useCallback((now) => {
|
|
53
|
+
const el = nodeRef.current;
|
|
54
|
+
const a = anim.current;
|
|
55
|
+
if (!el || !a) return;
|
|
56
|
+
const key = settings.current.axis === "horizontal" ? "scrollLeft" : "scrollTop";
|
|
57
|
+
const dt = Math.min(64, now - a.last);
|
|
58
|
+
a.last = now;
|
|
59
|
+
a.pos += (a.target - a.pos) * (1 - (1 - EASE_PER_FRAME) ** (dt / FRAME_MS));
|
|
60
|
+
if (Math.abs(a.target - a.pos) < SETTLE_PX) a.pos = a.target;
|
|
61
|
+
el[key] = a.pos;
|
|
62
|
+
a.written = el[key];
|
|
63
|
+
if (a.pos === a.target) anim.current = null;
|
|
64
|
+
else a.frame = requestAnimationFrame(step);
|
|
65
|
+
}, []);
|
|
66
|
+
const onWheel = useCallback(
|
|
67
|
+
(event) => {
|
|
68
|
+
const el = nodeRef.current;
|
|
69
|
+
if (!el || event.ctrlKey) return;
|
|
70
|
+
const { axis: current, mapVertical: map, smooth: ease } = settings.current;
|
|
71
|
+
const sideways = current === "horizontal";
|
|
72
|
+
if (event.shiftKey) return;
|
|
73
|
+
if (Math.abs(event.deltaX) > Math.abs(event.deltaY) || event.deltaY === 0) return;
|
|
74
|
+
if (sideways && !map) return;
|
|
75
|
+
if (innerScrolls(event.target, el, event.deltaY)) return;
|
|
76
|
+
const notch = ease && isNotch(event) && !cssOwnsMotion(el);
|
|
77
|
+
if (!sideways && !notch) return;
|
|
78
|
+
const key = sideways ? "scrollLeft" : "scrollTop";
|
|
79
|
+
const max = sideways ? el.scrollWidth - el.clientWidth : el.scrollHeight - el.clientHeight;
|
|
80
|
+
if (max <= 0) return;
|
|
81
|
+
const delta = toPixels(event.deltaY, event.deltaMode);
|
|
82
|
+
const from = notch && anim.current ? anim.current.target : el[key];
|
|
83
|
+
if (delta < 0 && Math.round(from) <= 0 || delta > 0 && Math.round(from) >= max) return;
|
|
84
|
+
event.preventDefault();
|
|
85
|
+
const target = Math.max(0, Math.min(max, from + delta));
|
|
86
|
+
if (!notch) {
|
|
87
|
+
stop();
|
|
88
|
+
el[key] = target;
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (anim.current) {
|
|
92
|
+
anim.current.target = target;
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
anim.current = {
|
|
96
|
+
target,
|
|
97
|
+
pos: el[key],
|
|
98
|
+
written: el[key],
|
|
99
|
+
last: performance.now(),
|
|
100
|
+
frame: requestAnimationFrame(step)
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
[step, stop]
|
|
104
|
+
);
|
|
105
|
+
const onScroll = useCallback(() => {
|
|
106
|
+
const el = nodeRef.current;
|
|
107
|
+
const a = anim.current;
|
|
108
|
+
if (!el || !a) return;
|
|
109
|
+
const key = settings.current.axis === "horizontal" ? "scrollLeft" : "scrollTop";
|
|
110
|
+
if (Math.abs(el[key] - a.written) > 1) stop();
|
|
111
|
+
}, [stop]);
|
|
112
|
+
const activeRef = useRef(active);
|
|
113
|
+
activeRef.current = active;
|
|
114
|
+
const attached = useRef(null);
|
|
115
|
+
const sync = useCallback(() => {
|
|
116
|
+
const want = activeRef.current ? nodeRef.current : null;
|
|
117
|
+
if (attached.current === want) return;
|
|
118
|
+
const was = attached.current;
|
|
119
|
+
if (was) {
|
|
120
|
+
was.removeEventListener("wheel", onWheel);
|
|
121
|
+
was.removeEventListener("scroll", onScroll);
|
|
122
|
+
was.removeEventListener("pointerdown", stop);
|
|
123
|
+
stop();
|
|
124
|
+
}
|
|
125
|
+
attached.current = want;
|
|
126
|
+
if (want) {
|
|
127
|
+
want.addEventListener("wheel", onWheel, { passive: false });
|
|
128
|
+
want.addEventListener("scroll", onScroll, { passive: true });
|
|
129
|
+
want.addEventListener("pointerdown", stop, { passive: true });
|
|
130
|
+
}
|
|
131
|
+
}, [onWheel, onScroll, stop]);
|
|
132
|
+
useEffect(sync, [active, sync]);
|
|
133
|
+
return useCallback(
|
|
134
|
+
(node) => {
|
|
135
|
+
nodeRef.current = node;
|
|
136
|
+
sync();
|
|
137
|
+
},
|
|
138
|
+
[sync]
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
function scrollRegionKeyDown(event, orientation) {
|
|
142
|
+
const el = event.currentTarget;
|
|
143
|
+
const horizontal = orientation === "horizontal";
|
|
144
|
+
const page = horizontal ? el.clientWidth : el.clientHeight;
|
|
145
|
+
const step = Math.max(40, page * 0.2);
|
|
146
|
+
const back = horizontal ? "ArrowLeft" : "ArrowUp";
|
|
147
|
+
const forward = horizontal ? "ArrowRight" : "ArrowDown";
|
|
148
|
+
const axis = horizontal ? "scrollLeft" : "scrollTop";
|
|
149
|
+
const max = horizontal ? el.scrollWidth - el.clientWidth : el.scrollHeight - el.clientHeight;
|
|
150
|
+
let next = null;
|
|
151
|
+
if (event.key === back) next = el[axis] - step;
|
|
152
|
+
else if (event.key === forward) next = el[axis] + step;
|
|
153
|
+
else if (event.key === "Home") next = 0;
|
|
154
|
+
else if (event.key === "End") next = max;
|
|
155
|
+
else if (event.key === "PageUp") next = el[axis] - page;
|
|
156
|
+
else if (event.key === "PageDown") next = el[axis] + page;
|
|
157
|
+
if (next === null) return;
|
|
158
|
+
event.preventDefault();
|
|
159
|
+
el[axis] = Math.max(0, Math.min(max, next));
|
|
160
|
+
}
|
|
161
|
+
export {
|
|
162
|
+
scrollRegionKeyDown,
|
|
163
|
+
useSmoothWheel
|
|
164
|
+
};
|