@cueplusplus/ui 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +142 -0
- package/README.md +109 -3
- package/dist/chrome/index.d.ts +2 -1
- package/dist/chrome/index.js +3 -2
- package/dist/chrome/inspector.d.ts +61 -0
- package/dist/chrome/inspector.js +121 -0
- package/dist/forms/input.d.ts +12 -0
- package/dist/forms/textarea.d.ts +12 -0
- package/dist/forms/toggle.d.ts +6 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +5 -2
- package/dist/instruments/code-block.d.ts +35 -0
- package/dist/instruments/code-block.js +173 -0
- package/dist/instruments/index.d.ts +2 -1
- package/dist/instruments/index.js +2 -1
- package/dist/layout/bento.js +1 -1
- package/dist/layout/index.d.ts +2 -1
- package/dist/layout/index.js +2 -1
- package/dist/layout/preview.d.ts +110 -0
- package/dist/layout/preview.js +459 -0
- package/manifest/components/code-block.json +106 -0
- package/manifest/components/input.json +20 -0
- package/manifest/components/inspector.json +199 -0
- package/manifest/components/password-input.json +20 -0
- package/manifest/components/preview.json +202 -0
- package/manifest/components/search-input.json +20 -0
- package/manifest/components/textarea.json +20 -0
- package/manifest/components/toggle.json +10 -0
- package/manifest/fixtures.json +5 -0
- package/manifest/manifest.json +46 -7
- package/manifest/tokens.json +1 -1
- package/package.json +5 -5
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../lib/cn.js";
|
|
3
|
+
import { useIsomorphicLayoutEffect } from "../system/use-isomorphic-layout-effect.js";
|
|
4
|
+
import { AmbientThemeContext, DensityContext } from "../system/density.js";
|
|
5
|
+
import { OverridesContext } from "../system/overrides.js";
|
|
6
|
+
import { ThemeContext } from "../system/theme-provider.js";
|
|
7
|
+
import * as React from "react";
|
|
8
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
//#region src/layout/preview.tsx
|
|
10
|
+
const RESIZE_DEFAULTS = {
|
|
11
|
+
axis: "both",
|
|
12
|
+
minWidth: 160,
|
|
13
|
+
maxWidth: 1280,
|
|
14
|
+
minHeight: 96,
|
|
15
|
+
maxHeight: 960
|
|
16
|
+
};
|
|
17
|
+
/** One arrow press, in px, and one Shift+arrow press. */
|
|
18
|
+
const STEP = 8;
|
|
19
|
+
const BIG_STEP = 64;
|
|
20
|
+
const ROOT_CLASS = "relative flex min-w-0 flex-col rounded-(--radius-surface) border border-border bg-surface-1 text-fg";
|
|
21
|
+
const BENCH_CLASS = "relative flex min-w-0 flex-1 overflow-auto rounded-t-(--radius-surface)";
|
|
22
|
+
/**
|
|
23
|
+
* The stage: centred, dotted in the stage's own `--cue-hair-strong`, and at
|
|
24
|
+
* least three top rungs of the space ladder tall so a preview reserves its room
|
|
25
|
+
* before its specimen arrives. Static literals, because Tailwind scans text.
|
|
26
|
+
*
|
|
27
|
+
* Centred **safely** (`place-items: safe center`): a specimen wider than the
|
|
28
|
+
* stage starts at the stage's edge and overflows at its end, where the bench
|
|
29
|
+
* scrolls to it. Plain `center` split the overflow across both sides, and the
|
|
30
|
+
* left half sat before the scroll origin, where nothing reaches it.
|
|
31
|
+
*/
|
|
32
|
+
const STAGE_CLASS = "grid h-full w-full min-h-[calc(var(--cue-space-8)*3)] place-items-center-safe bg-surface-1 bg-[radial-gradient(circle,var(--cue-hair-strong)_1px,transparent_1.3px)] bg-[length:14px_14px] p-(--cue-space-6) text-fg";
|
|
33
|
+
const TOOLS_CLASS = "absolute top-(--cue-space-2) right-(--cue-space-2) z-10 flex items-center gap-px rounded-(--radius-control) border border-border bg-surface-2 p-px";
|
|
34
|
+
/**
|
|
35
|
+
* The room the tools take, kept clear on the stage — only when there are tools.
|
|
36
|
+
*
|
|
37
|
+
* The tools sit over the stage's top right, so a specimen that reached that
|
|
38
|
+
* corner sat under them, and its own controls there with it: the index's
|
|
39
|
+
* Calendar paged by an arrow the wrench was drawn over. So a Preview with tools
|
|
40
|
+
* pads its stage's top by the tools' whole depth — their offset, their height
|
|
41
|
+
* (an `sm` control, plus the one-pixel border and padding on each side), and one
|
|
42
|
+
* rung of space below them.
|
|
43
|
+
*
|
|
44
|
+
* **Declared on the bench and read on the stage**, because the two sit in
|
|
45
|
+
* different densities: the tools are drawn in the page's, and the stage may set
|
|
46
|
+
* its own. A custom property's `var()`s are resolved where it is declared, so
|
|
47
|
+
* the stage inherits the depth measured at the page's rung — a compact stage
|
|
48
|
+
* still clears tools drawn at the page's size. And **only with tools**: a bare
|
|
49
|
+
* Preview carries none of it, which is the one the docs' front page draws
|
|
50
|
+
* seventeen times under a byte ceiling.
|
|
51
|
+
*/
|
|
52
|
+
const TOOLS_RESERVE_BENCH = "[--preview-tools-clear:calc(var(--cue-space-2)*2+var(--cue-control-sm)+4px)]";
|
|
53
|
+
const TOOLS_RESERVE_STAGE = "pt-(--preview-tools-clear)";
|
|
54
|
+
const FOOTER_CLASS = "flex min-w-0 items-center gap-(--cue-space-2) border-t border-border px-(--cue-space-4) py-(--cue-space-2)";
|
|
55
|
+
const GRIP_FOCUS = "touch-none outline-none hover:bg-(--cue-border-strong) focus-visible:bg-accent";
|
|
56
|
+
const GRIP_X_CLASS = `absolute top-0 right-0 z-10 h-full w-(--cue-space-2) cursor-col-resize ${GRIP_FOCUS}`;
|
|
57
|
+
const GRIP_Y_CLASS = `absolute bottom-0 left-0 z-10 h-(--cue-space-2) w-full cursor-row-resize ${GRIP_FOCUS}`;
|
|
58
|
+
const GRIP_CORNER_CLASS = "absolute right-0 bottom-0 z-20 size-(--cue-space-4) cursor-nwse-resize touch-none";
|
|
59
|
+
const READOUT_CLASS = "pointer-events-none absolute right-(--cue-space-5) bottom-(--cue-space-5) z-20 rounded-(--radius-control) bg-surface-3 px-(--cue-space-2) py-px font-mono text-(length:--cue-text-label) text-fg-muted";
|
|
60
|
+
function clamp(value, min, max) {
|
|
61
|
+
return Math.min(max, Math.max(min, value));
|
|
62
|
+
}
|
|
63
|
+
/** The bounds a `resizable` prop means, or `null` for a fixed stage. */
|
|
64
|
+
function resolveResize(resizable) {
|
|
65
|
+
if (resizable === void 0 || resizable === false) return null;
|
|
66
|
+
const given = resizable === true ? {} : resizable;
|
|
67
|
+
return {
|
|
68
|
+
axis: given.axis ?? RESIZE_DEFAULTS.axis,
|
|
69
|
+
minWidth: given.minWidth ?? RESIZE_DEFAULTS.minWidth,
|
|
70
|
+
maxWidth: given.maxWidth ?? RESIZE_DEFAULTS.maxWidth,
|
|
71
|
+
minHeight: given.minHeight ?? RESIZE_DEFAULTS.minHeight,
|
|
72
|
+
maxHeight: given.maxHeight ?? RESIZE_DEFAULTS.maxHeight
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The room the stage has: the bench's inline size in whole px, measured at
|
|
77
|
+
* mount and again whenever a `ResizeObserver` says the bench changed — the
|
|
78
|
+
* viewport resizing, a grid track reflowing around it.
|
|
79
|
+
*
|
|
80
|
+
* `null` means "no bound from the room". That covers a fixed Preview, which
|
|
81
|
+
* measures and observes nothing, so a frame without `resizable` costs no
|
|
82
|
+
* observer. It covers a bench that measures 0, which is not laid out
|
|
83
|
+
* (`display: none`, detached, or jsdom). And it covers the first render, which
|
|
84
|
+
* is also the server's, so the markup a server writes and the first client
|
|
85
|
+
* render agree. A host with no `ResizeObserver` gets the measurement at mount
|
|
86
|
+
* and nothing after, as `elements/clamp.tsx` does.
|
|
87
|
+
*/
|
|
88
|
+
function useAvailableWidth(bench, enabled) {
|
|
89
|
+
const [available, setAvailable] = React.useState(null);
|
|
90
|
+
useIsomorphicLayoutEffect(() => {
|
|
91
|
+
const node = bench.current;
|
|
92
|
+
if (!enabled || node === null) return;
|
|
93
|
+
const measure = () => {
|
|
94
|
+
const width = Math.floor(node.getBoundingClientRect().width);
|
|
95
|
+
setAvailable(width > 0 ? width : null);
|
|
96
|
+
};
|
|
97
|
+
measure();
|
|
98
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
99
|
+
const observer = new ResizeObserver(measure);
|
|
100
|
+
observer.observe(node);
|
|
101
|
+
return () => observer.disconnect();
|
|
102
|
+
}, [bench, enabled]);
|
|
103
|
+
return available;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* The bounds in force: the given ones, with the widest narrowed to the room
|
|
107
|
+
* the bench has, so a stage never outgrows the frame — or the grid track — it
|
|
108
|
+
* sits in. A room narrower than `minWidth` brings the floor down with it,
|
|
109
|
+
* because a floor wider than the room is a stage that overhangs its tile.
|
|
110
|
+
* Height takes no bound from the room: a taller stage grows its frame.
|
|
111
|
+
*/
|
|
112
|
+
function effectiveBounds(options, available) {
|
|
113
|
+
if (available === null) return options;
|
|
114
|
+
const maxWidth = Math.min(options.maxWidth, available);
|
|
115
|
+
return {
|
|
116
|
+
...options,
|
|
117
|
+
maxWidth,
|
|
118
|
+
minWidth: Math.min(options.minWidth, maxWidth)
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The attributes the stage carries — and, when nothing is asked, none at all.
|
|
123
|
+
*
|
|
124
|
+
* Inheriting is the *absence* of attributes, which is why every branch below
|
|
125
|
+
* is conditional. Three branches add a second attribute, each for a reason in
|
|
126
|
+
* the stylesheets rather than in taste.
|
|
127
|
+
*
|
|
128
|
+
* A theme's light block is the compound selector
|
|
129
|
+
* `[data-theme="t"][data-mode="light"]`, so a mode with no theme restates the
|
|
130
|
+
* page's theme on the same element. Read the other way, a theme with no mode
|
|
131
|
+
* repeats the page's mode. Otherwise the stage would match the theme's dark
|
|
132
|
+
* block under a light page, while every popup opened on it came up light:
|
|
133
|
+
* popups are stamped from the context `StageScope` publishes, which carries
|
|
134
|
+
* the page's mode.
|
|
135
|
+
*
|
|
136
|
+
* A theme's density overrides are addressed through `data-cue-theme`, the
|
|
137
|
+
* stamp `<Density>` writes, so a density names the nearest theme the same way.
|
|
138
|
+
*/
|
|
139
|
+
function stageAttributes(asked, page) {
|
|
140
|
+
const attributes = {};
|
|
141
|
+
const scoped = asked.theme ?? (asked.mode === void 0 ? null : page?.theme ?? null);
|
|
142
|
+
if (scoped !== null) attributes["data-theme"] = scoped;
|
|
143
|
+
const moded = asked.mode ?? (asked.theme === void 0 ? void 0 : page?.mode);
|
|
144
|
+
if (moded !== void 0) attributes["data-mode"] = moded;
|
|
145
|
+
if (asked.density !== void 0) {
|
|
146
|
+
attributes["data-density"] = asked.density;
|
|
147
|
+
const nearest = asked.theme ?? page?.theme ?? null;
|
|
148
|
+
if (nearest !== null) attributes["data-cue-theme"] = nearest;
|
|
149
|
+
}
|
|
150
|
+
if (asked.font !== void 0) attributes["data-font"] = asked.font;
|
|
151
|
+
return attributes;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* The contexts the stage republishes, so an overlay opened on it follows it.
|
|
155
|
+
*
|
|
156
|
+
* `useCuePortalProps()` stamps a portal container from `ThemeContext` and
|
|
157
|
+
* `DensityContext`; a popup mounts on `<body>` and inherits nothing from the
|
|
158
|
+
* stage's DOM. So the stage's subtree reads a theme context with the stage's
|
|
159
|
+
* theme and mode, and a density context with the stage's rung. The tools and
|
|
160
|
+
* footer are outside this subtree, so what they open follows the page. A theme
|
|
161
|
+
* the provider did not register leaves `manifest` null, which is what
|
|
162
|
+
* `useTheme()` already reports for a name nobody registered.
|
|
163
|
+
*/
|
|
164
|
+
function StageScope({ theme, mode, density, children }) {
|
|
165
|
+
const outer = React.useContext(ThemeContext);
|
|
166
|
+
const outerOverrides = React.useContext(OverridesContext);
|
|
167
|
+
const outerDensity = React.useContext(DensityContext);
|
|
168
|
+
const outerAmbient = React.useContext(AmbientThemeContext);
|
|
169
|
+
const value = React.useMemo(() => {
|
|
170
|
+
if (outer === null || theme === void 0 && mode === void 0) return outer;
|
|
171
|
+
const scoped = theme ?? outer.theme;
|
|
172
|
+
return {
|
|
173
|
+
...outer,
|
|
174
|
+
theme: scoped,
|
|
175
|
+
manifest: scoped === outer.theme ? outer.manifest : null,
|
|
176
|
+
mode: mode ?? outer.mode,
|
|
177
|
+
resolvedMode: mode === "dark" || mode === "light" ? mode : outer.resolvedMode
|
|
178
|
+
};
|
|
179
|
+
}, [
|
|
180
|
+
outer,
|
|
181
|
+
theme,
|
|
182
|
+
mode
|
|
183
|
+
]);
|
|
184
|
+
const stamped = theme !== void 0 || mode !== void 0;
|
|
185
|
+
return /* @__PURE__ */ jsx(AmbientThemeContext.Provider, {
|
|
186
|
+
value: theme ?? outerAmbient,
|
|
187
|
+
children: /* @__PURE__ */ jsx(DensityContext.Provider, {
|
|
188
|
+
value: density ?? outerDensity,
|
|
189
|
+
children: /* @__PURE__ */ jsx(OverridesContext.Provider, {
|
|
190
|
+
value: stamped ? null : outerOverrides,
|
|
191
|
+
children: /* @__PURE__ */ jsx(ThemeContext.Provider, {
|
|
192
|
+
value,
|
|
193
|
+
children
|
|
194
|
+
})
|
|
195
|
+
})
|
|
196
|
+
})
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* The stage's size: explicit per axis once a reader has moved it, and the size
|
|
201
|
+
* the layout gives it before that — `null` on an axis, which is also what a
|
|
202
|
+
* double-click returns an axis to.
|
|
203
|
+
*
|
|
204
|
+
* An axis nobody set is measured live, not once. Its value is what the grip
|
|
205
|
+
* announces on focus, before any key, and where the first key or drag starts
|
|
206
|
+
* from, so it has to follow the sizer at rest: the track widening under it, the
|
|
207
|
+
* specimen growing after mount (every listing specimen loads behind a
|
|
208
|
+
* skeleton), a Preview mounted hidden and shown later. Observing the bench is
|
|
209
|
+
* not enough for that. The bench reports a width, and a specimen that grows
|
|
210
|
+
* moves only the height, so the sizer is observed as well, for as long as any
|
|
211
|
+
* axis is unset.
|
|
212
|
+
*/
|
|
213
|
+
function useStageSize(options) {
|
|
214
|
+
const sizer = React.useRef(null);
|
|
215
|
+
const [width, setWidth] = React.useState(null);
|
|
216
|
+
const [height, setHeight] = React.useState(null);
|
|
217
|
+
const [natural, setNatural] = React.useState({
|
|
218
|
+
width: 0,
|
|
219
|
+
height: 0
|
|
220
|
+
});
|
|
221
|
+
const [drag, setDrag] = React.useState(null);
|
|
222
|
+
const enabled = options !== null;
|
|
223
|
+
const widthSet = width !== null;
|
|
224
|
+
const heightSet = height !== null;
|
|
225
|
+
useIsomorphicLayoutEffect(() => {
|
|
226
|
+
const node = sizer.current;
|
|
227
|
+
if (!enabled || node === null || widthSet && heightSet) return;
|
|
228
|
+
const measure = () => {
|
|
229
|
+
const rect = node.getBoundingClientRect();
|
|
230
|
+
setNatural((previous) => {
|
|
231
|
+
const next = {
|
|
232
|
+
width: widthSet ? previous.width : Math.round(rect.width),
|
|
233
|
+
height: heightSet ? previous.height : Math.round(rect.height)
|
|
234
|
+
};
|
|
235
|
+
return next.width === previous.width && next.height === previous.height ? previous : next;
|
|
236
|
+
});
|
|
237
|
+
};
|
|
238
|
+
measure();
|
|
239
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
240
|
+
const observer = new ResizeObserver(measure);
|
|
241
|
+
observer.observe(node);
|
|
242
|
+
return () => observer.disconnect();
|
|
243
|
+
}, [
|
|
244
|
+
enabled,
|
|
245
|
+
widthSet,
|
|
246
|
+
heightSet
|
|
247
|
+
]);
|
|
248
|
+
return {
|
|
249
|
+
sizer,
|
|
250
|
+
width,
|
|
251
|
+
height,
|
|
252
|
+
setWidth,
|
|
253
|
+
setHeight,
|
|
254
|
+
shownWidth: options === null ? 0 : clamp(width ?? natural.width, options.minWidth, options.maxWidth),
|
|
255
|
+
shownHeight: options === null ? 0 : clamp(height ?? natural.height, options.minHeight, options.maxHeight),
|
|
256
|
+
drag,
|
|
257
|
+
setDrag
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
/** The two keyboard-operable edges, the corner, and the readout. Chrome, never stage. */
|
|
261
|
+
function Grips({ label, options, size }) {
|
|
262
|
+
const { shownWidth, shownHeight, drag, setDrag, setWidth, setHeight } = size;
|
|
263
|
+
const stepWidth = (event) => {
|
|
264
|
+
const step = event.shiftKey ? BIG_STEP : STEP;
|
|
265
|
+
let next = null;
|
|
266
|
+
if (event.key === "ArrowRight") next = shownWidth + step;
|
|
267
|
+
else if (event.key === "ArrowLeft") next = shownWidth - step;
|
|
268
|
+
else if (event.key === "Home") next = options.minWidth;
|
|
269
|
+
else if (event.key === "End") next = options.maxWidth;
|
|
270
|
+
if (next === null) return;
|
|
271
|
+
event.preventDefault();
|
|
272
|
+
setWidth(clamp(next, options.minWidth, options.maxWidth));
|
|
273
|
+
};
|
|
274
|
+
const stepHeight = (event) => {
|
|
275
|
+
const step = event.shiftKey ? BIG_STEP : STEP;
|
|
276
|
+
let next = null;
|
|
277
|
+
if (event.key === "ArrowDown") next = shownHeight + step;
|
|
278
|
+
else if (event.key === "ArrowUp") next = shownHeight - step;
|
|
279
|
+
else if (event.key === "Home") next = options.minHeight;
|
|
280
|
+
else if (event.key === "End") next = options.maxHeight;
|
|
281
|
+
if (next === null) return;
|
|
282
|
+
event.preventDefault();
|
|
283
|
+
setHeight(clamp(next, options.minHeight, options.maxHeight));
|
|
284
|
+
};
|
|
285
|
+
const pointer = (axis) => ({
|
|
286
|
+
onPointerDown: (event) => {
|
|
287
|
+
if (event.button !== 0) return;
|
|
288
|
+
event.preventDefault();
|
|
289
|
+
event.currentTarget.setPointerCapture?.(event.pointerId);
|
|
290
|
+
setDrag({
|
|
291
|
+
axis,
|
|
292
|
+
x: event.clientX,
|
|
293
|
+
y: event.clientY,
|
|
294
|
+
width: shownWidth,
|
|
295
|
+
height: shownHeight
|
|
296
|
+
});
|
|
297
|
+
},
|
|
298
|
+
onPointerMove: (event) => {
|
|
299
|
+
if (drag === null) return;
|
|
300
|
+
if (drag.axis !== "y") setWidth(clamp(drag.width + event.clientX - drag.x, options.minWidth, options.maxWidth));
|
|
301
|
+
if (drag.axis !== "x") setHeight(clamp(drag.height + event.clientY - drag.y, options.minHeight, options.maxHeight));
|
|
302
|
+
},
|
|
303
|
+
onPointerUp: (event) => {
|
|
304
|
+
if (drag === null) return;
|
|
305
|
+
event.currentTarget.releasePointerCapture?.(event.pointerId);
|
|
306
|
+
setDrag(null);
|
|
307
|
+
},
|
|
308
|
+
onPointerCancel: () => setDrag(null)
|
|
309
|
+
});
|
|
310
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
311
|
+
options.axis === "y" ? null : /* @__PURE__ */ jsx("div", {
|
|
312
|
+
role: "separator",
|
|
313
|
+
tabIndex: 0,
|
|
314
|
+
"aria-orientation": "vertical",
|
|
315
|
+
"aria-label": `Resize ${label} width`,
|
|
316
|
+
"aria-valuenow": shownWidth,
|
|
317
|
+
"aria-valuemin": options.minWidth,
|
|
318
|
+
"aria-valuemax": options.maxWidth,
|
|
319
|
+
"aria-valuetext": `${shownWidth} pixels wide`,
|
|
320
|
+
"data-slot": "preview-grip-x",
|
|
321
|
+
className: GRIP_X_CLASS,
|
|
322
|
+
onKeyDown: stepWidth,
|
|
323
|
+
onDoubleClick: () => setWidth(null),
|
|
324
|
+
...pointer("x")
|
|
325
|
+
}),
|
|
326
|
+
options.axis === "x" ? null : /* @__PURE__ */ jsx("div", {
|
|
327
|
+
role: "separator",
|
|
328
|
+
tabIndex: 0,
|
|
329
|
+
"aria-orientation": "horizontal",
|
|
330
|
+
"aria-label": `Resize ${label} height`,
|
|
331
|
+
"aria-valuenow": shownHeight,
|
|
332
|
+
"aria-valuemin": options.minHeight,
|
|
333
|
+
"aria-valuemax": options.maxHeight,
|
|
334
|
+
"aria-valuetext": `${shownHeight} pixels tall`,
|
|
335
|
+
"data-slot": "preview-grip-y",
|
|
336
|
+
className: GRIP_Y_CLASS,
|
|
337
|
+
onKeyDown: stepHeight,
|
|
338
|
+
onDoubleClick: () => setHeight(null),
|
|
339
|
+
...pointer("y")
|
|
340
|
+
}),
|
|
341
|
+
options.axis === "both" ? /* @__PURE__ */ jsx("div", {
|
|
342
|
+
"aria-hidden": "true",
|
|
343
|
+
"data-slot": "preview-grip-corner",
|
|
344
|
+
className: GRIP_CORNER_CLASS,
|
|
345
|
+
onDoubleClick: () => {
|
|
346
|
+
setWidth(null);
|
|
347
|
+
setHeight(null);
|
|
348
|
+
},
|
|
349
|
+
...pointer("both")
|
|
350
|
+
}) : null,
|
|
351
|
+
drag === null ? null : /* @__PURE__ */ jsx("span", {
|
|
352
|
+
"aria-hidden": "true",
|
|
353
|
+
"data-slot": "preview-readout",
|
|
354
|
+
className: READOUT_CLASS,
|
|
355
|
+
children: `${shownWidth} × ${shownHeight}`
|
|
356
|
+
})
|
|
357
|
+
] });
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* A frame for one component: a stage that is its own theme island, a slot for
|
|
361
|
+
* tools at the top right, and a footer.
|
|
362
|
+
*
|
|
363
|
+
* **The stage is the island; the chrome is not.** `theme`, `mode`, `density`
|
|
364
|
+
* and `font` land on the stage element only, and the stage republishes the
|
|
365
|
+
* theme and density contexts so an overlay opened on it follows it. The tools,
|
|
366
|
+
* the footer, the grips and anything they open stay in the scope around the
|
|
367
|
+
* Preview — a `terminal` preview's own controls do not turn cyan. With none of
|
|
368
|
+
* the four set, the stage carries no attribute and costs what a `<div>` costs.
|
|
369
|
+
* A stage `theme` with no `mode` repeats the page's mode beside it.
|
|
370
|
+
*
|
|
371
|
+
* **`resizable` is handles of its own**, both axes by default: an edge grip per
|
|
372
|
+
* axis that is a keyboard-operable `separator` valued in pixels, and a corner
|
|
373
|
+
* grip for the pointer. Double-click resets an axis. The width never outgrows
|
|
374
|
+
* the frame: its bound in force is the frame's own width, observed live, when
|
|
375
|
+
* that is under `maxWidth`. Not `Resizable`, which is split panes on an
|
|
376
|
+
* optional peer.
|
|
377
|
+
*
|
|
378
|
+
* Props, not a compound: one stage and two slots, each saying what it is.
|
|
379
|
+
*
|
|
380
|
+
* @example
|
|
381
|
+
* <Preview label="Button preview" tools={<IconButton aria-label="Configure Button" icon={Wrench} />}>
|
|
382
|
+
* <Button variant="primary">Take cue</Button>
|
|
383
|
+
* </Preview>
|
|
384
|
+
* @example
|
|
385
|
+
* <Preview label="Chip preview" theme="terminal" density="compact" resizable>
|
|
386
|
+
* <Chip tone="accent">Standby</Chip>
|
|
387
|
+
* </Preview>
|
|
388
|
+
*/
|
|
389
|
+
const Preview = React.forwardRef(function Preview({ children, label, theme, mode, density, font, resizable, tools, footer, className, ...elementProps }, ref) {
|
|
390
|
+
const outer = React.useContext(ThemeContext);
|
|
391
|
+
const bench = React.useRef(null);
|
|
392
|
+
const given = resolveResize(resizable);
|
|
393
|
+
const available = useAvailableWidth(bench, given !== null);
|
|
394
|
+
const options = given === null ? null : effectiveBounds(given, available);
|
|
395
|
+
const size = useStageSize(options);
|
|
396
|
+
const attributes = stageAttributes({
|
|
397
|
+
theme,
|
|
398
|
+
mode,
|
|
399
|
+
density,
|
|
400
|
+
font
|
|
401
|
+
}, outer);
|
|
402
|
+
const stage = /* @__PURE__ */ jsx("div", {
|
|
403
|
+
"data-slot": "preview-stage",
|
|
404
|
+
...attributes,
|
|
405
|
+
className: cn(STAGE_CLASS, options !== null && "flex-1", tools !== void 0 && TOOLS_RESERVE_STAGE),
|
|
406
|
+
style: size.height === null ? void 0 : { minHeight: 0 },
|
|
407
|
+
children: /* @__PURE__ */ jsx(StageScope, {
|
|
408
|
+
theme,
|
|
409
|
+
mode,
|
|
410
|
+
density,
|
|
411
|
+
children
|
|
412
|
+
})
|
|
413
|
+
});
|
|
414
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
415
|
+
...elementProps,
|
|
416
|
+
ref,
|
|
417
|
+
role: "group",
|
|
418
|
+
"aria-label": label,
|
|
419
|
+
"data-slot": "preview",
|
|
420
|
+
className: cn(ROOT_CLASS, options !== null && "w-full", className),
|
|
421
|
+
children: [
|
|
422
|
+
/* @__PURE__ */ jsx("div", {
|
|
423
|
+
ref: bench,
|
|
424
|
+
"data-slot": "preview-bench",
|
|
425
|
+
className: cn(BENCH_CLASS, footer === void 0 && "rounded-b-(--radius-surface)", tools !== void 0 && TOOLS_RESERVE_BENCH),
|
|
426
|
+
children: options === null ? stage : /* @__PURE__ */ jsxs("div", {
|
|
427
|
+
ref: size.sizer,
|
|
428
|
+
"data-slot": "preview-sizer",
|
|
429
|
+
className: cn("relative flex flex-col", size.width === null && "w-full"),
|
|
430
|
+
style: {
|
|
431
|
+
width: size.width === null ? void 0 : `${size.shownWidth}px`,
|
|
432
|
+
height: size.height === null ? void 0 : `${size.shownHeight}px`,
|
|
433
|
+
minWidth: `${options.minWidth}px`,
|
|
434
|
+
maxWidth: `${options.maxWidth}px`,
|
|
435
|
+
minHeight: `${options.minHeight}px`,
|
|
436
|
+
maxHeight: `${options.maxHeight}px`
|
|
437
|
+
},
|
|
438
|
+
children: [stage, /* @__PURE__ */ jsx(Grips, {
|
|
439
|
+
label,
|
|
440
|
+
options,
|
|
441
|
+
size
|
|
442
|
+
})]
|
|
443
|
+
})
|
|
444
|
+
}),
|
|
445
|
+
tools === void 0 ? null : /* @__PURE__ */ jsx("div", {
|
|
446
|
+
"data-slot": "preview-tools",
|
|
447
|
+
className: TOOLS_CLASS,
|
|
448
|
+
children: tools
|
|
449
|
+
}),
|
|
450
|
+
footer === void 0 ? null : /* @__PURE__ */ jsx("div", {
|
|
451
|
+
"data-slot": "preview-footer",
|
|
452
|
+
className: FOOTER_CLASS,
|
|
453
|
+
children: footer
|
|
454
|
+
})
|
|
455
|
+
]
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
//#endregion
|
|
459
|
+
export { Preview };
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "CodeBlock",
|
|
3
|
+
"slug": "code-block",
|
|
4
|
+
"group": "instruments",
|
|
5
|
+
"importPath": "@cueplusplus/ui",
|
|
6
|
+
"peerDependencies": [],
|
|
7
|
+
"clientOnly": false,
|
|
8
|
+
"description": "A block of code with a copy button and no syntax-highlighter palette.\n\nEvery colour a token-driven site paints has to come from the token layer, and\na highlighter theme is a second palette that would need porting to every\npreset before it could sit beside them — so there is none. What `language=\"jsx\"`\nadds is emphasis in three inks the theme already has: tag names in `--cue-fg`,\nattribute names in `--cue-fg-muted`, punctuation in `--cue-fg-subtle`. Mono\ntype, the sunken ground every input uses, and a copy button that works.\n\nNo directive: the copy button is the only client code, and a server page can\ndraw the rest.",
|
|
9
|
+
"props": [
|
|
10
|
+
{
|
|
11
|
+
"name": "code",
|
|
12
|
+
"type": "string",
|
|
13
|
+
"required": true,
|
|
14
|
+
"defaultValue": null,
|
|
15
|
+
"description": "The code. Printed verbatim, already dedented by whoever produced it; the copy button copies exactly this.",
|
|
16
|
+
"control": {
|
|
17
|
+
"kind": "string"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"name": "label",
|
|
22
|
+
"type": "string",
|
|
23
|
+
"required": false,
|
|
24
|
+
"defaultValue": null,
|
|
25
|
+
"description": "What the block is, in the caption strip: a filename, an example's title. Defaults to the language.",
|
|
26
|
+
"control": {
|
|
27
|
+
"kind": "string"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"name": "language",
|
|
32
|
+
"type": "string",
|
|
33
|
+
"required": false,
|
|
34
|
+
"defaultValue": "tsx",
|
|
35
|
+
"description": "The fence language, on the `<pre>` as `data-language`. Defaults to `\"tsx\"`.\n`\"jsx\"` turns on emphasis: tag names, attribute names and punctuation in\nthree inks the theme already has. Every other value prints plain.",
|
|
36
|
+
"control": {
|
|
37
|
+
"kind": "string"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
"typeReferences": [
|
|
42
|
+
"Omit<React.ComponentPropsWithoutRef<\"div\">, \"children\">"
|
|
43
|
+
],
|
|
44
|
+
"subcomponents": [],
|
|
45
|
+
"parts": [],
|
|
46
|
+
"variants": {},
|
|
47
|
+
"defaultVariants": {},
|
|
48
|
+
"tokensUsed": [
|
|
49
|
+
"--cue-border",
|
|
50
|
+
"--cue-fg",
|
|
51
|
+
"--cue-fg-muted",
|
|
52
|
+
"--cue-fg-subtle",
|
|
53
|
+
"--cue-font-mono",
|
|
54
|
+
"--cue-pad-row-x",
|
|
55
|
+
"--cue-pad-row-y",
|
|
56
|
+
"--cue-space-4",
|
|
57
|
+
"--cue-sunken",
|
|
58
|
+
"--cue-text-micro",
|
|
59
|
+
"--cue-text-ui"
|
|
60
|
+
],
|
|
61
|
+
"summary": "A block of code with a copy button and no highlighter palette; `language=\"jsx\"` adds emphasis in three inks the theme already has.",
|
|
62
|
+
"examples": [
|
|
63
|
+
{
|
|
64
|
+
"title": "An install line",
|
|
65
|
+
"code": "<CodeBlock label=\"install\" language=\"sh\" code=\"pnpm add @cueplusplus/ui\" />",
|
|
66
|
+
"language": "tsx"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"title": "JSX, in three inks",
|
|
70
|
+
"code": "<CodeBlock label=\"JSX\" language=\"jsx\" code={'<Button variant=\"primary\">Take cue</Button>'} />",
|
|
71
|
+
"language": "tsx"
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"title": "Usage",
|
|
75
|
+
"code": "<CodeBlock label=\"install\" code=\"pnpm add\n<CodeBlock label=\"JSX\" language=\"jsx\" code={'<Button variant=\"primary\">Take cue</Button>'} />",
|
|
76
|
+
"language": "tsx"
|
|
77
|
+
}
|
|
78
|
+
],
|
|
79
|
+
"status": "stable",
|
|
80
|
+
"url": "/docs/components/code-block",
|
|
81
|
+
"mdUrl": "/docs/components/code-block.md",
|
|
82
|
+
"jsonUrl": "/r/components/code-block.json",
|
|
83
|
+
"whenToUse": [
|
|
84
|
+
"Any snippet a reader will copy: an install line, an example, the source of a state on screen.",
|
|
85
|
+
"JSX a reader should scan by structure — tag names, then attributes — without a second palette to port to every theme."
|
|
86
|
+
],
|
|
87
|
+
"whenNotToUse": [
|
|
88
|
+
"Inline code in a sentence. That is a `<code>` in prose.",
|
|
89
|
+
"A diff, or code a reader runs. Those are `CodeDiff` and `CodeRunner`."
|
|
90
|
+
],
|
|
91
|
+
"commonMistakes": [
|
|
92
|
+
"Expecting colour for a language other than `jsx`. There is no highlighter; every other language prints plain, on purpose.",
|
|
93
|
+
"Passing code that still carries its indentation from the file it came from. The block prints verbatim — dedent it first."
|
|
94
|
+
],
|
|
95
|
+
"specimens": [
|
|
96
|
+
{
|
|
97
|
+
"title": "CodeBlock",
|
|
98
|
+
"group": "instruments",
|
|
99
|
+
"components": [
|
|
100
|
+
"CodeBlock"
|
|
101
|
+
],
|
|
102
|
+
"code": "<div className=\"flex w-full flex-col gap-(--cue-space-4)\">\n <CodeBlock label=\"install\" language=\"sh\" code=\"pnpm add @cueplusplus/ui\" />\n <CodeBlock label=\"JSX\" language=\"jsx\" code={'<Button variant=\"primary\" size=\"sm\">\\n Take cue\\n</Button>'} />\n</div>",
|
|
103
|
+
"interaction": "the copy button is the one control; the code is plain, selectable text."
|
|
104
|
+
}
|
|
105
|
+
]
|
|
106
|
+
}
|
|
@@ -17,6 +17,16 @@
|
|
|
17
17
|
"kind": "string"
|
|
18
18
|
}
|
|
19
19
|
},
|
|
20
|
+
{
|
|
21
|
+
"name": "aria-label",
|
|
22
|
+
"type": "string",
|
|
23
|
+
"required": false,
|
|
24
|
+
"defaultValue": null,
|
|
25
|
+
"description": "Accessible name for the input, when no `FieldLabel` names it. Declared here\nrather than left to the inherited attribute so the props table carries the\nrow: a bare input outside a `Field` has no other way to be named.",
|
|
26
|
+
"control": {
|
|
27
|
+
"kind": "string"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
20
30
|
{
|
|
21
31
|
"name": "size",
|
|
22
32
|
"type": "\"sm\" | \"md\" | \"lg\"",
|
|
@@ -31,6 +41,16 @@
|
|
|
31
41
|
"lg"
|
|
32
42
|
]
|
|
33
43
|
}
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"name": "placeholder",
|
|
47
|
+
"type": "string",
|
|
48
|
+
"required": false,
|
|
49
|
+
"defaultValue": null,
|
|
50
|
+
"description": "An example of what goes in the field, shown while it is empty. Not a name:\nit disappears on the first keystroke, so pair it with `aria-label` or a\n`FieldLabel`. Declared for the props-table row, as `aria-label` is above.",
|
|
51
|
+
"control": {
|
|
52
|
+
"kind": "string"
|
|
53
|
+
}
|
|
34
54
|
}
|
|
35
55
|
],
|
|
36
56
|
"typeReferences": [
|