@tangle-network/ui 11.35.0 → 11.37.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/CHANGELOG.md +20 -0
- package/README.md +1 -1
- package/dist/code-presentation.d.ts +2 -0
- package/dist/code-presentation.js +2 -0
- package/dist/index.d.ts +8 -5
- package/dist/index.js +6 -3
- package/dist/lib/code-presentation.d.ts +92 -0
- package/dist/lib/code-presentation.js +437 -0
- package/dist/lib/control-presentation.d.ts +34 -1
- package/dist/lib/control-presentation.js +1 -1
- package/dist/lib/focus.d.ts +2 -2
- package/dist/lib/focus.js +3 -2
- package/dist/lib/motion.d.ts +4 -0
- package/dist/lib/motion.js +7 -0
- package/dist/lib/surface-motion.js +105 -0
- package/dist/markdown/api-schema.d.ts +9 -0
- package/dist/markdown/api-schema.js +20 -0
- package/dist/markdown/code-block.d.ts +15 -7
- package/dist/markdown/code-block.js +120 -268
- package/dist/markdown.d.ts +4 -2
- package/dist/markdown.js +4 -2
- package/dist/presentation.d.ts +88 -0
- package/dist/presentation.js +183 -0
- package/dist/primitives/alert-dialog.js +1 -1
- package/dist/primitives/alert.d.ts +1 -2
- package/dist/primitives/button.d.ts +2 -5
- package/dist/primitives/button.js +5 -31
- package/dist/primitives/card.js +3 -7
- package/dist/primitives/code-block.d.ts +1 -1
- package/dist/primitives/date-picker.d.ts +1 -1
- package/dist/primitives/dialog.js +29 -16
- package/dist/primitives/empty-state.js +6 -5
- package/dist/primitives/heading.d.ts +1 -0
- package/dist/primitives/heading.js +3 -2
- package/dist/primitives/help-text.js +2 -1
- package/dist/primitives/input.d.ts +1 -4
- package/dist/primitives/input.js +3 -24
- package/dist/primitives/label.js +1 -2
- package/dist/primitives/metric-strip.js +7 -12
- package/dist/primitives/page-header.d.ts +1 -0
- package/dist/primitives/page-header.js +7 -6
- package/dist/primitives/page-shell.d.ts +8 -2
- package/dist/primitives/page-shell.js +21 -2
- package/dist/primitives/pagination.js +1 -1
- package/dist/primitives/popover.js +11 -3
- package/dist/primitives/sheet.js +29 -19
- package/dist/primitives/skeleton.js +1 -1
- package/dist/primitives/status-pill.d.ts +3 -1
- package/dist/primitives/status-pill.js +12 -3
- package/dist/primitives/table.js +10 -9
- package/dist/primitives/tabs.js +3 -12
- package/dist/primitives/tooltip.js +11 -3
- package/dist/primitives.d.ts +3 -2
- package/dist/primitives.js +2 -1
- package/dist/utils.d.ts +1 -1
- package/package.json +20 -5
- package/src/code-presentation.ts +2 -0
- package/src/lib/code-presentation.ts +257 -0
- package/src/lib/control-presentation.ts +2 -4
- package/src/lib/focus.ts +3 -2
- package/src/lib/motion.ts +7 -0
- package/src/lib/surface-motion.tsx +85 -0
- package/src/markdown/api-presentation.test.tsx +63 -0
- package/src/markdown/api-schema.tsx +9 -0
- package/src/markdown/code-block.tsx +54 -185
- package/src/markdown/index.ts +2 -1
- package/src/presentation.ts +205 -0
- package/src/primitives/alert.tsx +1 -2
- package/src/primitives/button.tsx +6 -41
- package/src/primitives/card.tsx +5 -10
- package/src/primitives/checkbox-separator-sheet-tooltip.test.tsx +2 -1
- package/src/primitives/code-block.tsx +1 -1
- package/src/primitives/control-presentation.test.tsx +2 -7
- package/src/primitives/dialog.tsx +19 -18
- package/src/primitives/empty-state.tsx +6 -5
- package/src/primitives/heading.tsx +4 -2
- package/src/primitives/help-text.tsx +3 -2
- package/src/primitives/input.tsx +3 -31
- package/src/primitives/label.tsx +2 -4
- package/src/primitives/metric-strip.tsx +8 -17
- package/src/primitives/page-header.tsx +8 -5
- package/src/primitives/page-shell.tsx +13 -2
- package/src/primitives/popover.tsx +6 -8
- package/src/primitives/sheet.tsx +16 -11
- package/src/primitives/skeleton.tsx +1 -1
- package/src/primitives/status-pill.test.tsx +1 -0
- package/src/primitives/status-pill.tsx +10 -4
- package/src/primitives/table.tsx +10 -9
- package/src/primitives/tabs.tsx +5 -17
- package/src/primitives/tooltip.tsx +6 -5
- package/src/styles/compat.css +12 -0
- package/src/styles/presentation.css +38 -0
- package/src/primitives/page-shell.test.tsx +0 -24
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
import { tabsPresentation } from "../presentation.js";
|
|
2
|
+
//#region src/lib/code-presentation.ts
|
|
3
|
+
const SYNTAX = {
|
|
4
|
+
comment: "var(--syntax-comment, currentColor)",
|
|
5
|
+
keyword: "var(--syntax-keyword, currentColor)",
|
|
6
|
+
string: "var(--syntax-string, currentColor)",
|
|
7
|
+
function: "var(--syntax-function, currentColor)",
|
|
8
|
+
number: "var(--syntax-number, currentColor)",
|
|
9
|
+
variable: "var(--syntax-variable, currentColor)",
|
|
10
|
+
foreground: "var(--syntax-foreground, currentColor)"
|
|
11
|
+
};
|
|
12
|
+
const SHIKI_THEME = "tangle-syntax";
|
|
13
|
+
const BRAND_COLOR_FOR_SHIKI = {
|
|
14
|
+
"var(--shiki-foreground)": SYNTAX.foreground,
|
|
15
|
+
"var(--shiki-token-comment)": SYNTAX.comment,
|
|
16
|
+
"var(--shiki-token-keyword)": SYNTAX.keyword,
|
|
17
|
+
"var(--shiki-token-string)": SYNTAX.string,
|
|
18
|
+
"var(--shiki-token-string-expression)": SYNTAX.string,
|
|
19
|
+
"var(--shiki-token-function)": SYNTAX.function,
|
|
20
|
+
"var(--shiki-token-constant)": SYNTAX.number,
|
|
21
|
+
"var(--shiki-token-link)": SYNTAX.number,
|
|
22
|
+
"var(--shiki-token-parameter)": SYNTAX.variable,
|
|
23
|
+
"var(--shiki-token-punctuation)": SYNTAX.foreground
|
|
24
|
+
};
|
|
25
|
+
const GRAMMARS = {
|
|
26
|
+
typescript: () => import("shiki/langs/typescript.mjs"),
|
|
27
|
+
tsx: () => import("shiki/langs/tsx.mjs"),
|
|
28
|
+
javascript: () => import("shiki/langs/javascript.mjs"),
|
|
29
|
+
jsx: () => import("shiki/langs/jsx.mjs"),
|
|
30
|
+
json: () => import("shiki/langs/json.mjs"),
|
|
31
|
+
jsonc: () => import("shiki/langs/jsonc.mjs"),
|
|
32
|
+
bash: () => import("shiki/langs/bash.mjs"),
|
|
33
|
+
python: () => import("shiki/langs/python.mjs"),
|
|
34
|
+
html: () => import("shiki/langs/html.mjs"),
|
|
35
|
+
css: () => import("shiki/langs/css.mjs"),
|
|
36
|
+
scss: () => import("shiki/langs/scss.mjs"),
|
|
37
|
+
markdown: () => import("shiki/langs/markdown.mjs"),
|
|
38
|
+
mdx: () => import("shiki/langs/mdx.mjs"),
|
|
39
|
+
yaml: () => import("shiki/langs/yaml.mjs"),
|
|
40
|
+
toml: () => import("shiki/langs/toml.mjs"),
|
|
41
|
+
sql: () => import("shiki/langs/sql.mjs"),
|
|
42
|
+
diff: () => import("shiki/langs/diff.mjs"),
|
|
43
|
+
go: () => import("shiki/langs/go.mjs"),
|
|
44
|
+
rust: () => import("shiki/langs/rust.mjs"),
|
|
45
|
+
solidity: () => import("shiki/langs/solidity.mjs"),
|
|
46
|
+
ruby: () => import("shiki/langs/ruby.mjs"),
|
|
47
|
+
java: () => import("shiki/langs/java.mjs"),
|
|
48
|
+
kotlin: () => import("shiki/langs/kotlin.mjs"),
|
|
49
|
+
swift: () => import("shiki/langs/swift.mjs"),
|
|
50
|
+
php: () => import("shiki/langs/php.mjs"),
|
|
51
|
+
c: () => import("shiki/langs/c.mjs"),
|
|
52
|
+
cpp: () => import("shiki/langs/cpp.mjs"),
|
|
53
|
+
csharp: () => import("shiki/langs/csharp.mjs"),
|
|
54
|
+
graphql: () => import("shiki/langs/graphql.mjs"),
|
|
55
|
+
xml: () => import("shiki/langs/xml.mjs"),
|
|
56
|
+
dockerfile: () => import("shiki/langs/dockerfile.mjs")
|
|
57
|
+
};
|
|
58
|
+
const LANGUAGE_ALIASES = {
|
|
59
|
+
ts: "typescript",
|
|
60
|
+
mts: "typescript",
|
|
61
|
+
cts: "typescript",
|
|
62
|
+
js: "javascript",
|
|
63
|
+
mjs: "javascript",
|
|
64
|
+
cjs: "javascript",
|
|
65
|
+
node: "javascript",
|
|
66
|
+
sh: "bash",
|
|
67
|
+
shell: "bash",
|
|
68
|
+
zsh: "bash",
|
|
69
|
+
console: "bash",
|
|
70
|
+
shellscript: "bash",
|
|
71
|
+
py: "python",
|
|
72
|
+
python3: "python",
|
|
73
|
+
yml: "yaml",
|
|
74
|
+
md: "markdown",
|
|
75
|
+
rs: "rust",
|
|
76
|
+
sol: "solidity",
|
|
77
|
+
rb: "ruby",
|
|
78
|
+
kt: "kotlin",
|
|
79
|
+
cs: "csharp",
|
|
80
|
+
"c#": "csharp",
|
|
81
|
+
"c++": "cpp",
|
|
82
|
+
gql: "graphql",
|
|
83
|
+
svg: "xml",
|
|
84
|
+
docker: "dockerfile",
|
|
85
|
+
patch: "diff",
|
|
86
|
+
htm: "html"
|
|
87
|
+
};
|
|
88
|
+
const LANGUAGE_NAMES = {
|
|
89
|
+
typescript: "TypeScript",
|
|
90
|
+
tsx: "TSX",
|
|
91
|
+
javascript: "JavaScript",
|
|
92
|
+
jsx: "JSX",
|
|
93
|
+
json: "JSON",
|
|
94
|
+
jsonc: "JSONC",
|
|
95
|
+
bash: "Bash",
|
|
96
|
+
python: "Python",
|
|
97
|
+
html: "HTML",
|
|
98
|
+
css: "CSS",
|
|
99
|
+
scss: "SCSS",
|
|
100
|
+
markdown: "Markdown",
|
|
101
|
+
mdx: "MDX",
|
|
102
|
+
yaml: "YAML",
|
|
103
|
+
toml: "TOML",
|
|
104
|
+
sql: "SQL",
|
|
105
|
+
diff: "Diff",
|
|
106
|
+
go: "Go",
|
|
107
|
+
rust: "Rust",
|
|
108
|
+
solidity: "Solidity",
|
|
109
|
+
ruby: "Ruby",
|
|
110
|
+
java: "Java",
|
|
111
|
+
kotlin: "Kotlin",
|
|
112
|
+
swift: "Swift",
|
|
113
|
+
php: "PHP",
|
|
114
|
+
c: "C",
|
|
115
|
+
cpp: "C++",
|
|
116
|
+
csharp: "C#",
|
|
117
|
+
graphql: "GraphQL",
|
|
118
|
+
xml: "XML",
|
|
119
|
+
dockerfile: "Dockerfile"
|
|
120
|
+
};
|
|
121
|
+
/** The Shiki grammar for a fence language, or undefined for plain text. */
|
|
122
|
+
const PLAIN_TEXT = /* @__PURE__ */ new Set([
|
|
123
|
+
"text",
|
|
124
|
+
"txt",
|
|
125
|
+
"plain",
|
|
126
|
+
"plaintext"
|
|
127
|
+
]);
|
|
128
|
+
function codeBlockGrammar(language) {
|
|
129
|
+
if (!language) return void 0;
|
|
130
|
+
const id = language.toLowerCase();
|
|
131
|
+
const resolved = LANGUAGE_ALIASES[id] ?? id;
|
|
132
|
+
return GRAMMARS[resolved] ? resolved : void 0;
|
|
133
|
+
}
|
|
134
|
+
let highlighterPromise;
|
|
135
|
+
const grammarLoads = /* @__PURE__ */ new Map();
|
|
136
|
+
function loadHighlighter() {
|
|
137
|
+
return highlighterPromise ??= Promise.all([import("shiki/core"), import("shiki/engine/javascript")]).then(([core, engine]) => core.createHighlighterCore({
|
|
138
|
+
themes: [core.createCssVariablesTheme({
|
|
139
|
+
name: SHIKI_THEME,
|
|
140
|
+
variablePrefix: "--shiki-",
|
|
141
|
+
fontStyle: true
|
|
142
|
+
})],
|
|
143
|
+
langs: [],
|
|
144
|
+
engine: engine.createJavaScriptRegexEngine()
|
|
145
|
+
})).catch((error) => {
|
|
146
|
+
highlighterPromise = void 0;
|
|
147
|
+
throw error;
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
async function tokenize(code, grammar) {
|
|
151
|
+
const highlighter = await loadHighlighter();
|
|
152
|
+
let load = grammarLoads.get(grammar);
|
|
153
|
+
if (!load) {
|
|
154
|
+
load = GRAMMARS[grammar]().then((module) => highlighter.loadLanguage(module.default));
|
|
155
|
+
load.catch(() => grammarLoads.delete(grammar));
|
|
156
|
+
grammarLoads.set(grammar, load);
|
|
157
|
+
}
|
|
158
|
+
await load;
|
|
159
|
+
return highlighter.codeToTokensBase(code, {
|
|
160
|
+
lang: grammar,
|
|
161
|
+
theme: SHIKI_THEME
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
function tokenStyle(token) {
|
|
165
|
+
const color = token.color ? BRAND_COLOR_FOR_SHIKI[token.color] ?? SYNTAX.foreground : void 0;
|
|
166
|
+
const style = token.fontStyle ?? 0;
|
|
167
|
+
if (!color && !style) return void 0;
|
|
168
|
+
return {
|
|
169
|
+
...color ? { color } : {},
|
|
170
|
+
...style & 1 ? { fontStyle: "italic" } : {},
|
|
171
|
+
...style & 2 ? { fontWeight: "bold" } : {},
|
|
172
|
+
...style & 4 ? { textDecoration: "underline" } : {}
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
const codePreStyle = {
|
|
176
|
+
margin: 0,
|
|
177
|
+
padding: "var(--code-padding-y, 0.625rem) var(--code-padding-x, 0.75rem)",
|
|
178
|
+
background: "transparent",
|
|
179
|
+
color: SYNTAX.foreground,
|
|
180
|
+
fontSize: "var(--code-font-size, 0.8125rem)",
|
|
181
|
+
lineHeight: "var(--code-line-height, 1.5)",
|
|
182
|
+
overflowX: "auto"
|
|
183
|
+
};
|
|
184
|
+
const codeTextStyle = {
|
|
185
|
+
fontFamily: "var(--font-mono, 'JetBrains Mono', ui-monospace, monospace)",
|
|
186
|
+
whiteSpace: "pre"
|
|
187
|
+
};
|
|
188
|
+
const codePresentation = {
|
|
189
|
+
tabs: `tangle-code-tabs ${tabsPresentation.list.underline}`,
|
|
190
|
+
tab: tabsPresentation.trigger.underline,
|
|
191
|
+
root: "group relative min-w-0 overflow-hidden rounded-lg border font-mono bg-card border-border",
|
|
192
|
+
header: "flex flex-wrap items-center justify-between gap-3 border-b border-border px-3 py-1.5 bg-[var(--code-header-bg,hsl(var(--background)))]",
|
|
193
|
+
copy: "flex items-center justify-center min-w-8 min-h-8 pointer-coarse:min-w-11 pointer-coarse:min-h-11 rounded-md bg-muted border border-border hover:border-[var(--border-strong)] transition-colors data-[copy-loading=true]:after:content-['…'] data-[copy-loading=true]:gap-1",
|
|
194
|
+
copyStatus: "sr-only data-[copy-long-work=true]:not-sr-only data-[copy-long-work=true]:text-xs data-[copy-long-work=true]:text-muted-foreground"
|
|
195
|
+
};
|
|
196
|
+
const apiMetadataText = (meta) => [
|
|
197
|
+
meta.kind,
|
|
198
|
+
meta.method,
|
|
199
|
+
meta.status,
|
|
200
|
+
meta.contentType
|
|
201
|
+
].filter((v) => v !== void 0).join(" · ");
|
|
202
|
+
/** Same lazy grammar/token model for React, Hono SSR and static renderers. */
|
|
203
|
+
async function highlightCode(code, language) {
|
|
204
|
+
const grammar = codeBlockGrammar(language);
|
|
205
|
+
if (!grammar) return code.split("\n").map((content) => [{
|
|
206
|
+
content,
|
|
207
|
+
style: void 0
|
|
208
|
+
}]);
|
|
209
|
+
return (await tokenize(code, grammar)).map((line) => line.map((token) => ({
|
|
210
|
+
content: token.content,
|
|
211
|
+
style: tokenStyle(token)
|
|
212
|
+
})));
|
|
213
|
+
}
|
|
214
|
+
/** Legacy fallback is used only when the modern clipboard API is absent. */
|
|
215
|
+
async function writeClipboard(text) {
|
|
216
|
+
if (navigator.clipboard?.writeText) {
|
|
217
|
+
await navigator.clipboard.writeText(text);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const active = document.activeElement, selection = document.getSelection();
|
|
221
|
+
const ranges = selection ? Array.from({ length: selection.rangeCount }, (_, i) => selection.getRangeAt(i).cloneRange()) : [];
|
|
222
|
+
const field = document.createElement("textarea");
|
|
223
|
+
field.value = text;
|
|
224
|
+
field.readOnly = true;
|
|
225
|
+
field.tabIndex = -1;
|
|
226
|
+
field.setAttribute("aria-hidden", "true");
|
|
227
|
+
field.style.position = "fixed";
|
|
228
|
+
field.style.left = "-9999px";
|
|
229
|
+
document.body.append(field);
|
|
230
|
+
try {
|
|
231
|
+
field.select();
|
|
232
|
+
if (!document.execCommand?.("copy")) throw Error("Clipboard unavailable");
|
|
233
|
+
} finally {
|
|
234
|
+
field.remove();
|
|
235
|
+
active?.focus({ preventScroll: true });
|
|
236
|
+
if (selection) {
|
|
237
|
+
selection.removeAllRanges();
|
|
238
|
+
for (const range of ranges) selection.addRange(range);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const COPY_FEEDBACK_MS = 2e3;
|
|
243
|
+
/** Resolve scoped Brand times, including calc(), without duplicating token values. */
|
|
244
|
+
function interactionBudgets(scope) {
|
|
245
|
+
const view = scope.ownerDocument.defaultView;
|
|
246
|
+
if (!view) return {};
|
|
247
|
+
const probe = scope.ownerDocument.createElement("i");
|
|
248
|
+
probe.hidden = true;
|
|
249
|
+
scope.append(probe);
|
|
250
|
+
const read = (role) => {
|
|
251
|
+
const name = `--interaction-${role}`;
|
|
252
|
+
if (!view.getComputedStyle(scope).getPropertyValue(name).trim()) return void 0;
|
|
253
|
+
probe.style.animationDuration = `var(${name})`;
|
|
254
|
+
const time = /^(\d+(?:\.\d+)?)(ms|s)$/.exec(view.getComputedStyle(probe).animationDuration);
|
|
255
|
+
return time ? Number(time[1]) * (time[2] === "s" ? 1e3 : 1) : void 0;
|
|
256
|
+
};
|
|
257
|
+
try {
|
|
258
|
+
return {
|
|
259
|
+
feedback: read("feedback-budget"),
|
|
260
|
+
pending: read("pending-delay"),
|
|
261
|
+
longWork: read("long-work-delay"),
|
|
262
|
+
mutation: read("mutation-budget")
|
|
263
|
+
};
|
|
264
|
+
} finally {
|
|
265
|
+
probe.remove();
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/** Owns one accepted write, scoped delays and cleanup for both renderers. */
|
|
269
|
+
function createCopyFeedback(change, write = writeClipboard, scope) {
|
|
270
|
+
let disposed = false, pending = false, timer;
|
|
271
|
+
let delays = [], elapsed;
|
|
272
|
+
const clearPending = () => {
|
|
273
|
+
for (const delay of delays) clearTimeout(delay);
|
|
274
|
+
delays = [];
|
|
275
|
+
if (elapsed) clearInterval(elapsed);
|
|
276
|
+
elapsed = void 0;
|
|
277
|
+
};
|
|
278
|
+
return {
|
|
279
|
+
async copy(text) {
|
|
280
|
+
if (disposed || pending) return;
|
|
281
|
+
const started = performance.now(), budgets = scope ? interactionBudgets(scope) : {};
|
|
282
|
+
pending = true;
|
|
283
|
+
if (timer) clearTimeout(timer);
|
|
284
|
+
let loading = budgets.pending === void 0, longRunning = false;
|
|
285
|
+
const announce = () => {
|
|
286
|
+
const elapsedMs = performance.now() - started;
|
|
287
|
+
change({
|
|
288
|
+
copied: false,
|
|
289
|
+
pending: true,
|
|
290
|
+
loading,
|
|
291
|
+
longRunning,
|
|
292
|
+
elapsedMs,
|
|
293
|
+
status: longRunning ? `Copying · ${Math.floor(elapsedMs / 1e3)}s elapsed. Time remaining unknown. Cannot cancel this clipboard write; waiting for confirmation.` : "Copying to clipboard"
|
|
294
|
+
});
|
|
295
|
+
};
|
|
296
|
+
announce();
|
|
297
|
+
const feedbackMs = performance.now() - started;
|
|
298
|
+
if (budgets.pending !== void 0) delays.push(setTimeout(() => {
|
|
299
|
+
loading = true;
|
|
300
|
+
announce();
|
|
301
|
+
}, budgets.pending));
|
|
302
|
+
if (budgets.longWork !== void 0) delays.push(setTimeout(() => {
|
|
303
|
+
longRunning = true;
|
|
304
|
+
announce();
|
|
305
|
+
elapsed = setInterval(announce, 1e3);
|
|
306
|
+
}, budgets.longWork));
|
|
307
|
+
let copied;
|
|
308
|
+
try {
|
|
309
|
+
await write(text);
|
|
310
|
+
copied = true;
|
|
311
|
+
} catch {
|
|
312
|
+
copied = false;
|
|
313
|
+
}
|
|
314
|
+
pending = false;
|
|
315
|
+
clearPending();
|
|
316
|
+
if (disposed) return;
|
|
317
|
+
const elapsedMs = performance.now() - started;
|
|
318
|
+
change({
|
|
319
|
+
copied,
|
|
320
|
+
status: copied ? "Copied to clipboard" : "Copy failed",
|
|
321
|
+
pending: false,
|
|
322
|
+
measurement: {
|
|
323
|
+
elapsedMs,
|
|
324
|
+
feedbackMs,
|
|
325
|
+
budgets,
|
|
326
|
+
mutationBudgetExceeded: budgets.mutation === void 0 ? void 0 : elapsedMs > budgets.mutation
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
timer = setTimeout(() => {
|
|
330
|
+
if (!disposed) change({
|
|
331
|
+
copied: false,
|
|
332
|
+
status: "",
|
|
333
|
+
pending: false
|
|
334
|
+
});
|
|
335
|
+
}, COPY_FEEDBACK_MS);
|
|
336
|
+
},
|
|
337
|
+
dispose() {
|
|
338
|
+
disposed = true;
|
|
339
|
+
clearPending();
|
|
340
|
+
if (timer) clearTimeout(timer);
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
function bindCopyButton(button, status, text) {
|
|
345
|
+
const previous = {
|
|
346
|
+
label: button.getAttribute("aria-label"),
|
|
347
|
+
status: status.textContent,
|
|
348
|
+
disabled: button.getAttribute("aria-disabled"),
|
|
349
|
+
busy: button.getAttribute("aria-busy"),
|
|
350
|
+
loading: button.getAttribute("data-copy-loading"),
|
|
351
|
+
long: status.getAttribute("data-copy-long-work")
|
|
352
|
+
};
|
|
353
|
+
const restore = (element, name, value) => {
|
|
354
|
+
if (value === null) element.removeAttribute(name);
|
|
355
|
+
else element.setAttribute(name, value);
|
|
356
|
+
};
|
|
357
|
+
const feedback = createCopyFeedback((state) => {
|
|
358
|
+
status.textContent = state.status;
|
|
359
|
+
status.setAttribute("data-copy-long-work", String(!!state.longRunning));
|
|
360
|
+
button.setAttribute("data-copy-loading", String(!!state.loading));
|
|
361
|
+
button.setAttribute("aria-label", state.pending ? "Copying to clipboard" : state.copied ? "Copied" : "Copy to clipboard");
|
|
362
|
+
if (state.pending) {
|
|
363
|
+
button.setAttribute("aria-disabled", "true");
|
|
364
|
+
button.setAttribute("aria-busy", "true");
|
|
365
|
+
} else {
|
|
366
|
+
restore(button, "aria-disabled", previous.disabled);
|
|
367
|
+
restore(button, "aria-busy", previous.busy);
|
|
368
|
+
}
|
|
369
|
+
}, writeClipboard, button);
|
|
370
|
+
const click = () => {
|
|
371
|
+
if (!button.disabled && button.getAttribute("aria-disabled") !== "true") feedback.copy(text());
|
|
372
|
+
};
|
|
373
|
+
button.addEventListener("click", click);
|
|
374
|
+
return () => {
|
|
375
|
+
button.removeEventListener("click", click);
|
|
376
|
+
feedback.dispose();
|
|
377
|
+
restore(button, "aria-label", previous.label);
|
|
378
|
+
status.textContent = previous.status;
|
|
379
|
+
restore(button, "aria-disabled", previous.disabled);
|
|
380
|
+
restore(button, "aria-busy", previous.busy);
|
|
381
|
+
restore(button, "data-copy-loading", previous.loading);
|
|
382
|
+
restore(status, "data-copy-long-work", previous.long);
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
/** Native tabs adapter. The consumer owns labels, IDs and escaped panel content. */
|
|
386
|
+
function bindCodeTabs(list, onSelect) {
|
|
387
|
+
const buttons = () => [...list.querySelectorAll("[role=\"tab\"]")];
|
|
388
|
+
const select = (index, focus) => {
|
|
389
|
+
const tabs = buttons(), tab = tabs[index];
|
|
390
|
+
if (!tab) return;
|
|
391
|
+
for (const panel of new Set(tabs.map((t) => document.getElementById(t.getAttribute("aria-controls") ?? "")))) if (panel) panel.hidden = true;
|
|
392
|
+
tabs.forEach((t, i) => {
|
|
393
|
+
t.setAttribute("aria-selected", String(i === index));
|
|
394
|
+
t.dataset.state = i === index ? "active" : "inactive";
|
|
395
|
+
t.tabIndex = i === index ? 0 : -1;
|
|
396
|
+
});
|
|
397
|
+
const panel = document.getElementById(tab.getAttribute("aria-controls") ?? "");
|
|
398
|
+
if (panel) {
|
|
399
|
+
panel.hidden = false;
|
|
400
|
+
panel.setAttribute("aria-labelledby", tab.id);
|
|
401
|
+
}
|
|
402
|
+
if (focus) tab.focus();
|
|
403
|
+
onSelect?.(index);
|
|
404
|
+
};
|
|
405
|
+
const click = (event) => {
|
|
406
|
+
const tab = event.target.closest("[role=\"tab\"]");
|
|
407
|
+
const index = buttons().findIndex((t) => t === tab);
|
|
408
|
+
if (index >= 0) select(index, false);
|
|
409
|
+
};
|
|
410
|
+
const key = (event) => {
|
|
411
|
+
const tabs = buttons(), current = tabs.findIndex((t) => t === document.activeElement);
|
|
412
|
+
if (current < 0) return;
|
|
413
|
+
const next = event.key === "Home" ? 0 : event.key === "End" ? tabs.length - 1 : event.key === "ArrowRight" ? (current + 1) % tabs.length : event.key === "ArrowLeft" ? (current + tabs.length - 1) % tabs.length : void 0;
|
|
414
|
+
if (next !== void 0) {
|
|
415
|
+
event.preventDefault();
|
|
416
|
+
select(next, true);
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
list.addEventListener("click", click);
|
|
420
|
+
list.addEventListener("keydown", key);
|
|
421
|
+
return () => {
|
|
422
|
+
list.removeEventListener("click", click);
|
|
423
|
+
list.removeEventListener("keydown", key);
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
function schemaRows(fields, parent = "", depth = 0) {
|
|
427
|
+
return fields.flatMap((field) => {
|
|
428
|
+
const path = parent ? `${parent}.${field.name}` : field.name;
|
|
429
|
+
return [{
|
|
430
|
+
...field,
|
|
431
|
+
path,
|
|
432
|
+
depth
|
|
433
|
+
}, ...schemaRows(field.children ?? [], path, depth + 1)];
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
//#endregion
|
|
437
|
+
export { LANGUAGE_NAMES, PLAIN_TEXT, SYNTAX, apiMetadataText, bindCodeTabs, bindCopyButton, codeBlockGrammar, codePreStyle, codePresentation, codeTextStyle, createCopyFeedback, highlightCode, interactionBudgets, schemaRows, tokenStyle, tokenize, writeClipboard };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { controlMotion } from "./motion.js";
|
|
1
2
|
//#region src/lib/control-presentation.d.ts
|
|
2
3
|
/**
|
|
3
4
|
* The control scale. Button, Input, Textarea and SelectTrigger take the same
|
|
@@ -7,4 +8,36 @@
|
|
|
7
8
|
* Brand stylesheet at the same pixels.
|
|
8
9
|
*/
|
|
9
10
|
export type ControlSize = "sm" | "md" | "lg";
|
|
10
|
-
|
|
11
|
+
export declare const controlHeight: {
|
|
12
|
+
readonly sm: "h-[var(--control-height-sm,2rem)]";
|
|
13
|
+
readonly md: "h-[var(--control-height,2.25rem)]";
|
|
14
|
+
readonly lg: "h-[var(--control-height-lg,2.75rem)]";
|
|
15
|
+
};
|
|
16
|
+
export declare const controlText: {
|
|
17
|
+
readonly sm: "text-[length:var(--control-text-sm,0.75rem)]";
|
|
18
|
+
readonly md: "text-[length:var(--control-text-md,0.875rem)]";
|
|
19
|
+
readonly lg: "text-[length:var(--control-text-lg,1rem)]";
|
|
20
|
+
};
|
|
21
|
+
export declare const controlRadius: {
|
|
22
|
+
readonly sm: "rounded-md";
|
|
23
|
+
readonly md: "rounded-lg";
|
|
24
|
+
readonly lg: "rounded-lg";
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Field text never drops below 16px on a coarse pointer: iOS Safari zooms the
|
|
28
|
+
* page on focus for anything smaller.
|
|
29
|
+
*/
|
|
30
|
+
export declare const fieldTouchText = "pointer-coarse:text-base";
|
|
31
|
+
/** Field sizes: the control scale with field padding. */
|
|
32
|
+
export declare const fieldSizes: {
|
|
33
|
+
readonly sm: string;
|
|
34
|
+
readonly md: string;
|
|
35
|
+
readonly lg: string;
|
|
36
|
+
};
|
|
37
|
+
export declare const controlSizes: {
|
|
38
|
+
readonly compact: "h-[var(--control-height)] px-3 py-1 text-sm";
|
|
39
|
+
readonly touch: "h-11 min-h-11 min-w-11 px-4 py-2 text-base";
|
|
40
|
+
};
|
|
41
|
+
export declare const fieldPresentation: string;
|
|
42
|
+
//#endregion
|
|
43
|
+
export { controlMotion };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { controlMotion } from "./motion.js";
|
|
1
2
|
import { ixField } from "./interaction.js";
|
|
2
3
|
import { focusField } from "./focus.js";
|
|
3
4
|
import { cn } from "./utils.js";
|
|
4
5
|
//#region src/lib/control-presentation.ts
|
|
5
|
-
const controlMotion = "duration-[var(--duration-fast)] ease-[var(--ease-standard)] motion-reduce:transition-none motion-reduce:duration-0";
|
|
6
6
|
const controlHeight = {
|
|
7
7
|
sm: "h-[var(--control-height-sm,2rem)]",
|
|
8
8
|
md: "h-[var(--control-height,2.25rem)]",
|
package/dist/lib/focus.d.ts
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* replaces it through tailwind-merge; the focus border outranks it because
|
|
28
28
|
* Tailwind emits `focus:` after `hover:`.
|
|
29
29
|
*/
|
|
30
|
-
export declare const focusField = "border-border transition-
|
|
30
|
+
export declare const focusField = "border-border [transition:color_var(--motion-control),background-color_var(--motion-control),border-color_var(--motion-control),box-shadow_var(--motion-control),transform_var(--motion-control),scale_var(--motion-control),opacity_var(--motion-control)] motion-reduce:transition-none hover:border-[var(--border-strong)] focus:outline-hidden focus:border-[var(--focus-border)] focus:ring-3 focus:ring-[var(--focus-halo)]";
|
|
31
31
|
/**
|
|
32
32
|
* The field treatment for a container that wraps a borderless field, such as a
|
|
33
33
|
* composer around a textarea and its buttons. It lights while a FIELD inside
|
|
@@ -45,7 +45,7 @@ export declare const focusField = "border-border transition-[border-color,box-sh
|
|
|
45
45
|
* own well fill, the `--field-border` hairline and the green hover edge. The
|
|
46
46
|
* borderless field inside stays transparent.
|
|
47
47
|
*/
|
|
48
|
-
export declare const focusFieldWithin = "focus-field-within transition-
|
|
48
|
+
export declare const focusFieldWithin = "focus-field-within [transition:color_var(--motion-control),background-color_var(--motion-control),border-color_var(--motion-control),box-shadow_var(--motion-control),transform_var(--motion-control),scale_var(--motion-control),opacity_var(--motion-control)] motion-reduce:transition-none bg-[var(--field-surface,var(--well))] border-[var(--field-border)] shadow-[var(--shadow-well)] hover:border-[var(--ix-hover-border)] hover:shadow-[var(--shadow-well),var(--ix-hover)] has-[:is(textarea,select,[contenteditable]:not([contenteditable=false]),input:not([type=button],[type=submit],[type=reset],[type=image],[type=checkbox],[type=radio],[type=range],[type=color],[type=file])):focus]:border-[var(--focus-border)] has-[:is(textarea,select,[contenteditable]:not([contenteditable=false]),input:not([type=button],[type=submit],[type=reset],[type=image],[type=checkbox],[type=radio],[type=range],[type=color],[type=file])):focus]:ring-3 has-[:is(textarea,select,[contenteditable]:not([contenteditable=false]),input:not([type=button],[type=submit],[type=reset],[type=image],[type=checkbox],[type=radio],[type=range],[type=color],[type=file])):focus]:ring-[var(--focus-halo)]";
|
|
49
49
|
/** Error state for a field: a danger hairline and the same focus shape in danger. Compose after `focusField`. */
|
|
50
50
|
export declare const focusFieldInvalid = "border-[var(--surface-danger-border)] hover:border-[var(--surface-danger-border)] focus:border-[var(--focus-border-danger)] focus:ring-[var(--focus-halo-danger)]";
|
|
51
51
|
/** Keyboard-only focus for a control: a 1px focus line with the halo beyond it. */
|
package/dist/lib/focus.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { controlMotion } from "./motion.js";
|
|
1
2
|
import { ixField } from "./interaction.js";
|
|
2
3
|
//#region src/lib/focus.ts
|
|
3
4
|
/**
|
|
@@ -28,7 +29,7 @@ import { ixField } from "./interaction.js";
|
|
|
28
29
|
* replaces it through tailwind-merge; the focus border outranks it because
|
|
29
30
|
* Tailwind emits `focus:` after `hover:`.
|
|
30
31
|
*/
|
|
31
|
-
const focusField =
|
|
32
|
+
const focusField = `border-border ${controlMotion} hover:border-[var(--border-strong)] focus:outline-hidden focus:border-[var(--focus-border)] focus:ring-3 focus:ring-[var(--focus-halo)]`;
|
|
32
33
|
/**
|
|
33
34
|
* The field treatment for a container that wraps a borderless field, such as a
|
|
34
35
|
* composer around a textarea and its buttons. It lights while a FIELD inside
|
|
@@ -46,7 +47,7 @@ const focusField = "border-border transition-[border-color,box-shadow] duration-
|
|
|
46
47
|
* own well fill, the `--field-border` hairline and the green hover edge. The
|
|
47
48
|
* borderless field inside stays transparent.
|
|
48
49
|
*/
|
|
49
|
-
const focusFieldWithin = `focus-field-within
|
|
50
|
+
const focusFieldWithin = `focus-field-within ${controlMotion} ${ixField} has-[:is(textarea,select,[contenteditable]:not([contenteditable=false]),input:not([type=button],[type=submit],[type=reset],[type=image],[type=checkbox],[type=radio],[type=range],[type=color],[type=file])):focus]:border-[var(--focus-border)] has-[:is(textarea,select,[contenteditable]:not([contenteditable=false]),input:not([type=button],[type=submit],[type=reset],[type=image],[type=checkbox],[type=radio],[type=range],[type=color],[type=file])):focus]:ring-3 has-[:is(textarea,select,[contenteditable]:not([contenteditable=false]),input:not([type=button],[type=submit],[type=reset],[type=image],[type=checkbox],[type=radio],[type=range],[type=color],[type=file])):focus]:ring-[var(--focus-halo)]`;
|
|
50
51
|
/** Error state for a field: a danger hairline and the same focus shape in danger. Compose after `focusField`. */
|
|
51
52
|
const focusFieldInvalid = "border-[var(--surface-danger-border)] hover:border-[var(--surface-danger-border)] focus:border-[var(--focus-border-danger)] focus:ring-[var(--focus-halo-danger)]";
|
|
52
53
|
const focusRingBase = "focus-visible:outline-1 focus-visible:outline-[var(--focus-border)] focus-visible:ring-4 focus-visible:ring-[var(--focus-halo)]";
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
//#region src/lib/motion.d.ts
|
|
2
|
+
/** Brand motion roles contain duration AND easing; consume complete shorthands. */
|
|
3
|
+
export declare const controlMotion = "[transition:color_var(--motion-control),background-color_var(--motion-control),border-color_var(--motion-control),box-shadow_var(--motion-control),transform_var(--motion-control),scale_var(--motion-control),opacity_var(--motion-control)] motion-reduce:transition-none";
|
|
4
|
+
//#endregion
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
//#region src/lib/motion.ts
|
|
2
|
+
/** Brand motion roles contain duration AND easing; consume complete shorthands. */
|
|
3
|
+
const controlMotion = "[transition:color_var(--motion-control),background-color_var(--motion-control),border-color_var(--motion-control),box-shadow_var(--motion-control),transform_var(--motion-control),scale_var(--motion-control),opacity_var(--motion-control)] motion-reduce:transition-none";
|
|
4
|
+
/** Radix presence markers carry the scoped role; SurfaceMotion owns interruptible geometry. */
|
|
5
|
+
const surfaceMotion = "data-[state=open]:[animation:tangle-surface-present_var(--motion-surface)_both] data-[state=delayed-open]:[animation:tangle-surface-present_var(--motion-surface)_both] data-[state=instant-open]:[animation:tangle-surface-present_var(--motion-surface)_both] data-[state=closed]:[animation:tangle-surface-absent_var(--motion-dismiss)_both] motion-reduce:animate-none!";
|
|
6
|
+
//#endregion
|
|
7
|
+
export { controlMotion, surfaceMotion };
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
import { animate, motionValue, styleEffect } from "motion";
|
|
6
|
+
//#region src/lib/surface-motion.tsx
|
|
7
|
+
/** Radix owns state, focus and presence; Motion only retargets the visible object. */
|
|
8
|
+
const SurfaceMotion = React.forwardRef(({ kind, side = "right", asChild, "data-state": state, ...props }, forwardedRef) => {
|
|
9
|
+
const element = React.useRef(null);
|
|
10
|
+
const values = React.useRef(null);
|
|
11
|
+
const controls = React.useRef([]);
|
|
12
|
+
const ref = React.useCallback((node) => {
|
|
13
|
+
element.current = node;
|
|
14
|
+
if (typeof forwardedRef === "function") {
|
|
15
|
+
const cleanup = forwardedRef(node);
|
|
16
|
+
if (typeof cleanup === "function") return () => {
|
|
17
|
+
element.current = null;
|
|
18
|
+
cleanup();
|
|
19
|
+
};
|
|
20
|
+
} else if (forwardedRef) forwardedRef.current = node;
|
|
21
|
+
}, [forwardedRef]);
|
|
22
|
+
React.useLayoutEffect(() => {
|
|
23
|
+
const node = element.current;
|
|
24
|
+
if (!node) return;
|
|
25
|
+
const preference = typeof matchMedia === "function" ? matchMedia("(prefers-reduced-motion: reduce)") : void 0;
|
|
26
|
+
let cancelRender;
|
|
27
|
+
const run = () => {
|
|
28
|
+
const closed = state === "closed";
|
|
29
|
+
const bounds = node.getBoundingClientRect();
|
|
30
|
+
const hidden = {
|
|
31
|
+
opacity: kind === "sheet" ? 1 : 0,
|
|
32
|
+
scale: kind === "dialog" || kind === "tooltip" || kind === "popover" ? .98 : 1,
|
|
33
|
+
x: kind === "sheet" ? side === "left" ? -bounds.width : side === "right" ? bounds.width : 0 : 0,
|
|
34
|
+
y: kind === "sheet" ? side === "top" ? -bounds.height : side === "bottom" ? bounds.height : 0 : 0
|
|
35
|
+
};
|
|
36
|
+
const target = closed ? hidden : {
|
|
37
|
+
opacity: 1,
|
|
38
|
+
scale: 1,
|
|
39
|
+
x: 0,
|
|
40
|
+
y: 0
|
|
41
|
+
};
|
|
42
|
+
const style = getComputedStyle(node);
|
|
43
|
+
const time = /^(\d+(?:\.\d+)?)(ms|s)$/.exec(style.animationDuration.split(",")[0].trim());
|
|
44
|
+
const seconds = time ? Number(time[1]) / (time[2] === "ms" ? 1e3 : 1) : 0;
|
|
45
|
+
const place = (value) => {
|
|
46
|
+
node.style.opacity = String(value.opacity);
|
|
47
|
+
node.style.transform = `translateX(${value.x}px) translateY(${value.y}px) scale(${value.scale})`;
|
|
48
|
+
};
|
|
49
|
+
if (preference?.matches || seconds <= 0 || style.animationName === "none") {
|
|
50
|
+
controls.current.forEach((control) => control.stop());
|
|
51
|
+
if (values.current) for (const key of [
|
|
52
|
+
"opacity",
|
|
53
|
+
"scale",
|
|
54
|
+
"x",
|
|
55
|
+
"y"
|
|
56
|
+
]) values.current[key].jump(target[key]);
|
|
57
|
+
place(target);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (!values.current) {
|
|
61
|
+
place(hidden);
|
|
62
|
+
values.current = {
|
|
63
|
+
opacity: motionValue(hidden.opacity),
|
|
64
|
+
scale: motionValue(hidden.scale),
|
|
65
|
+
x: motionValue(hidden.x),
|
|
66
|
+
y: motionValue(hidden.y)
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
cancelRender ??= styleEffect(node, values.current);
|
|
70
|
+
const frequency = 12 / seconds;
|
|
71
|
+
controls.current = [
|
|
72
|
+
"opacity",
|
|
73
|
+
"scale",
|
|
74
|
+
"x",
|
|
75
|
+
"y"
|
|
76
|
+
].map((key) => animate(values.current[key], target[key], {
|
|
77
|
+
type: "spring",
|
|
78
|
+
stiffness: frequency * frequency,
|
|
79
|
+
damping: 2 * frequency,
|
|
80
|
+
restDelta: .001,
|
|
81
|
+
restSpeed: .01
|
|
82
|
+
}));
|
|
83
|
+
};
|
|
84
|
+
run();
|
|
85
|
+
preference?.addEventListener("change", run);
|
|
86
|
+
return () => {
|
|
87
|
+
preference?.removeEventListener("change", run);
|
|
88
|
+
cancelRender?.();
|
|
89
|
+
};
|
|
90
|
+
}, [
|
|
91
|
+
state,
|
|
92
|
+
kind,
|
|
93
|
+
side,
|
|
94
|
+
props.style?.animation
|
|
95
|
+
]);
|
|
96
|
+
React.useEffect(() => () => controls.current.forEach((control) => control.stop()), []);
|
|
97
|
+
return /* @__PURE__ */ jsx(asChild ? Slot : "div", {
|
|
98
|
+
...props,
|
|
99
|
+
"data-state": state,
|
|
100
|
+
ref
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
SurfaceMotion.displayName = "SurfaceMotion";
|
|
104
|
+
//#endregion
|
|
105
|
+
export { SurfaceMotion };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ApiField } from "../lib/code-presentation.js";
|
|
2
|
+
//#region src/markdown/api-schema.d.ts
|
|
3
|
+
export interface ApiSchemaProps {
|
|
4
|
+
fields: readonly ApiField[];
|
|
5
|
+
label?: string;
|
|
6
|
+
}
|
|
7
|
+
/** Presentation only: the consuming API owns all fields and constraints. */
|
|
8
|
+
export declare function ApiSchema({ fields, label }: ApiSchemaProps): import("react").JSX.Element;
|
|
9
|
+
//#endregion
|