vite-plugin-nora 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NOTICE +26 -0
- package/README.md +113 -0
- package/licenses/geist-OFL.txt +93 -0
- package/licenses/lucide-ISC.txt +17 -0
- package/package.json +90 -0
- package/src/cli.js +101 -0
- package/src/client/App.jsx +216 -0
- package/src/client/canvas/Canvas.jsx +95 -0
- package/src/client/canvas/ErrorBoundary.jsx +56 -0
- package/src/client/canvas/Preview.jsx +70 -0
- package/src/client/canvas/Viewport.jsx +211 -0
- package/src/client/chords.js +38 -0
- package/src/client/css.d.ts +5 -0
- package/src/client/fonts.css +48 -0
- package/src/client/frame.css +107 -0
- package/src/client/frame.html +11 -0
- package/src/client/frame.jsx +73 -0
- package/src/client/index.html +12 -0
- package/src/client/main.jsx +10 -0
- package/src/client/open-folder.js +84 -0
- package/src/client/selection.js +22 -0
- package/src/client/styles.css +1264 -0
- package/src/client/sweep/SweepPanel.jsx +206 -0
- package/src/client/sweep/measure.js +230 -0
- package/src/client/sweep/report.js +54 -0
- package/src/client/sweep/run-sweep.js +185 -0
- package/src/client/toolbar/Picker.jsx +343 -0
- package/src/client/toolbar/Toolbar.jsx +231 -0
- package/src/client/toolbar/ViewportMenu.jsx +83 -0
- package/src/client/toolbar/bar-shape.js +402 -0
- package/src/client/toolbar/icons.jsx +129 -0
- package/src/client/toolbar/use-draggable-bar.js +204 -0
- package/src/client/viewports.js +43 -0
- package/src/client/virtual.d.ts +28 -0
- package/src/index.js +4 -0
- package/src/server/create-server.js +147 -0
- package/src/server/plugin.js +207 -0
- package/src/server/safe-path.js +42 -0
- package/src/server/scan.js +350 -0
- package/types/index.d.ts +3 -0
- package/types/server/create-server.d.ts +22 -0
- package/types/server/plugin.d.ts +20 -0
- package/types/server/safe-path.d.ts +25 -0
- package/types/server/scan.d.ts +106 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { forwardRef } from "react";
|
|
2
|
+
import { Viewport } from "./Viewport.jsx";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Shell-side canvas. Decides between an empty state and the framed preview.
|
|
6
|
+
*
|
|
7
|
+
* Anything that can be known before a component loads is answered here, in the
|
|
8
|
+
* full width of the window. Only an actual selection gets an iframe — an empty
|
|
9
|
+
* state squeezed into a 375px frame reads as a broken component rather than an
|
|
10
|
+
* instruction.
|
|
11
|
+
*/
|
|
12
|
+
export const Canvas = forwardRef(
|
|
13
|
+
/**
|
|
14
|
+
* @param {object} props
|
|
15
|
+
* @param {string | null} props.selectedId
|
|
16
|
+
* @param {import("virtual:nora/registry").NoraEntry} [props.entry]
|
|
17
|
+
* @param {number} props.entryCount
|
|
18
|
+
* @param {string | null} props.currentDir
|
|
19
|
+
* @param {"light" | "dark"} props.theme
|
|
20
|
+
* @param {import("../viewports.js").NoraViewport} props.viewport
|
|
21
|
+
* @param {boolean} props.sweeping
|
|
22
|
+
* @param {number} [props.sweepProgress] 0 to 1
|
|
23
|
+
* @param {(scale: number) => void} [props.onScale]
|
|
24
|
+
* @param {import("react").ForwardedRef<any>} ref
|
|
25
|
+
*/
|
|
26
|
+
function Canvas(
|
|
27
|
+
{
|
|
28
|
+
selectedId,
|
|
29
|
+
entry,
|
|
30
|
+
entryCount,
|
|
31
|
+
currentDir,
|
|
32
|
+
theme,
|
|
33
|
+
viewport,
|
|
34
|
+
sweeping,
|
|
35
|
+
sweepProgress,
|
|
36
|
+
onScale,
|
|
37
|
+
},
|
|
38
|
+
ref,
|
|
39
|
+
) {
|
|
40
|
+
if (!currentDir) {
|
|
41
|
+
return (
|
|
42
|
+
<Empty
|
|
43
|
+
title="No Folder Selected"
|
|
44
|
+
lines={["Open the bar, then pick a folder to preview."]}
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (entryCount === 0) {
|
|
50
|
+
return (
|
|
51
|
+
<Empty
|
|
52
|
+
title="No Components Found"
|
|
53
|
+
lines={[
|
|
54
|
+
`Nothing in ${currentDir} matched.`,
|
|
55
|
+
"Index files and tests are skipped by default — adjust include/exclude in nora.config to change that.",
|
|
56
|
+
]}
|
|
57
|
+
/>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!entry) {
|
|
62
|
+
return (
|
|
63
|
+
<Empty
|
|
64
|
+
title={`${entryCount} Component${entryCount === 1 ? "" : "s"} in ${currentDir}`}
|
|
65
|
+
lines={["Press ⌘K to pick one."]}
|
|
66
|
+
/>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<Viewport
|
|
72
|
+
ref={ref}
|
|
73
|
+
selectedId={selectedId}
|
|
74
|
+
theme={theme}
|
|
75
|
+
viewport={viewport}
|
|
76
|
+
sweeping={sweeping}
|
|
77
|
+
sweepProgress={sweepProgress}
|
|
78
|
+
onScale={onScale}
|
|
79
|
+
/>
|
|
80
|
+
);
|
|
81
|
+
},
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
function Empty({ title, lines }) {
|
|
85
|
+
return (
|
|
86
|
+
<div className="nora-empty">
|
|
87
|
+
<div className="nora-empty-title">{title}</div>
|
|
88
|
+
{lines.map((l) => (
|
|
89
|
+
<div className="nora-empty-line" key={l}>
|
|
90
|
+
{l}
|
|
91
|
+
</div>
|
|
92
|
+
))}
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Component } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A component rendered outside its app will often throw — a missing provider,
|
|
5
|
+
* a required prop, a hook called with no context. The boundary's job is to say
|
|
6
|
+
* which of those it was, because the raw stack rarely does.
|
|
7
|
+
*
|
|
8
|
+
* This runs inside the iframe, so its classes are namespaced noraf- and styled by
|
|
9
|
+
* frame.css rather than the shell's stylesheet.
|
|
10
|
+
*/
|
|
11
|
+
export class ErrorBoundary extends Component {
|
|
12
|
+
constructor(props) {
|
|
13
|
+
super(props);
|
|
14
|
+
this.state = { error: null };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
static getDerivedStateFromError(error) {
|
|
18
|
+
return { error };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
render() {
|
|
22
|
+
const { error } = this.state;
|
|
23
|
+
if (!error) return this.props.children;
|
|
24
|
+
|
|
25
|
+
const message = String(error.message ?? error);
|
|
26
|
+
const hint = diagnose(message);
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className="noraf-error">
|
|
30
|
+
<div className="noraf-error-head">{this.props.name} Failed to Render</div>
|
|
31
|
+
<pre className="noraf-error-msg">{message}</pre>
|
|
32
|
+
{hint ? <div className="noraf-error-hint">{hint}</div> : null}
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Turn the common failure modes into something actionable. */
|
|
39
|
+
function diagnose(message) {
|
|
40
|
+
if (/invalid hook call/i.test(message)) {
|
|
41
|
+
return "Two copies of React are loaded. Check that react and react-dom resolve to one instance — this is what resolve.dedupe is for.";
|
|
42
|
+
}
|
|
43
|
+
if (/cannot read propert(y|ies) of (undefined|null)/i.test(message)) {
|
|
44
|
+
return "Often a missing prop, or a context this component expects. Add a wrapper in nora.config to supply your providers, or give it props once the controls panel exists.";
|
|
45
|
+
}
|
|
46
|
+
if (/useContext|context/i.test(message)) {
|
|
47
|
+
return "This component reads a React context that isn't present. Wrap it via the wrapper option in nora.config.";
|
|
48
|
+
}
|
|
49
|
+
if (/useNavigate|useLocation|useRouter|router/i.test(message)) {
|
|
50
|
+
return "This component needs a router. Put your router provider in the wrapper option in nora.config.";
|
|
51
|
+
}
|
|
52
|
+
if (/is not a function/i.test(message)) {
|
|
53
|
+
return "A prop this component calls wasn't passed. Until the controls panel lands, a wrapper that supplies defaults is the workaround.";
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Suspense, lazy, useMemo } from "react";
|
|
2
|
+
import { ErrorBoundary } from "./ErrorBoundary.jsx";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Renders one component. Lives inside the iframe, so everything it mounts sees
|
|
6
|
+
* the viewport width the toolbar selected.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* One lazy wrapper per entry, kept for the life of the page.
|
|
10
|
+
*
|
|
11
|
+
* `lazy()` returns a new component *type* on every call, and a new type means
|
|
12
|
+
* React unmounts whatever was there and mounts this instead. Building it during
|
|
13
|
+
* render meant every return to a component you had already looked at produced a
|
|
14
|
+
* fresh type, so it suspended and remounted again even though its module had
|
|
15
|
+
* long since been fetched. Keyed by entry id, it suspends once.
|
|
16
|
+
*
|
|
17
|
+
* The map is bounded by the number of components in the scanned folder, and
|
|
18
|
+
* changing folders reloads the page, so it cannot grow without limit.
|
|
19
|
+
*/
|
|
20
|
+
const lazyByEntry = new Map();
|
|
21
|
+
|
|
22
|
+
function lazyFor(entry) {
|
|
23
|
+
const cached = lazyByEntry.get(entry.id);
|
|
24
|
+
if (cached) return cached;
|
|
25
|
+
|
|
26
|
+
const Component = lazy(async () => {
|
|
27
|
+
const mod = await entry.load();
|
|
28
|
+
const Loaded = mod[entry.exportName];
|
|
29
|
+
if (!Loaded) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`${entry.file} has no export named "${entry.exportName}". It may have been renamed or removed.`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
return { default: Loaded };
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
lazyByEntry.set(entry.id, Component);
|
|
38
|
+
return Component;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function Preview({ entry, wrapper: Wrapper }) {
|
|
42
|
+
const Lazy = useMemo(() => (!entry || entry.unsupported ? null : lazyFor(entry)), [entry]);
|
|
43
|
+
|
|
44
|
+
if (!entry) return null;
|
|
45
|
+
|
|
46
|
+
if (entry.unsupported) {
|
|
47
|
+
return (
|
|
48
|
+
<div className="noraf-error">
|
|
49
|
+
<div className="noraf-error-head">{entry.name} cannot be previewed</div>
|
|
50
|
+
<div className="noraf-error-hint">{entry.unsupported}</div>
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const content = (
|
|
56
|
+
<ErrorBoundary key={entry.id} name={entry.name}>
|
|
57
|
+
<Suspense fallback={<div className="noraf-loading">Loading {entry.name}…</div>}>
|
|
58
|
+
{/* eslint-disable-next-line react-hooks/static-components --
|
|
59
|
+
Which component renders here is data, not code: that is what a
|
|
60
|
+
registry-driven previewer is. `lazyFor` makes the type stable per
|
|
61
|
+
entry, so the identity change this rule guards against now happens
|
|
62
|
+
only when the subject genuinely changes — exactly when a remount is
|
|
63
|
+
what you want. */}
|
|
64
|
+
<Lazy />
|
|
65
|
+
</Suspense>
|
|
66
|
+
</ErrorBoundary>
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
return <div className="noraf-stage">{Wrapper ? <Wrapper>{content}</Wrapper> : content}</div>;
|
|
70
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import {
|
|
2
|
+
forwardRef,
|
|
3
|
+
useEffect,
|
|
4
|
+
useImperativeHandle,
|
|
5
|
+
useLayoutEffect,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
} from "react";
|
|
9
|
+
|
|
10
|
+
import { SWEEP_MIN, SWEEP_MAX } from "../sweep/run-sweep.js";
|
|
11
|
+
|
|
12
|
+
const CHANNEL = "nora";
|
|
13
|
+
const FRAME_URL = "/__nora/frame";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Holds the preview iframe and sizes it to the chosen viewport.
|
|
17
|
+
*
|
|
18
|
+
* Two things matter here. The iframe's `width` is a real viewport width, so
|
|
19
|
+
* media queries inside it fire the way they would on a device. And when the
|
|
20
|
+
* chosen width is wider than the space available, the whole frame is scaled
|
|
21
|
+
* down rather than clipped — otherwise 1440 is unusable on a 1280 screen.
|
|
22
|
+
* Scaling is a visual transform only; the iframe still reports its true width
|
|
23
|
+
* to the component inside, which is what keeps the breakpoints honest.
|
|
24
|
+
*
|
|
25
|
+
* Chrome — padding, border, shadow — is tied to whether a fixed width is set,
|
|
26
|
+
* and nothing else. That looks like a styling detail and is really a
|
|
27
|
+
* correctness one: an earlier version decided it by comparing the frame's width
|
|
28
|
+
* against the space available, which fed back on itself, because the padding it
|
|
29
|
+
* switched on was part of what "available" measured. Any frame landing inside
|
|
30
|
+
* that 40px band flipped between the two states forever. A rule that depends
|
|
31
|
+
* only on the chosen width cannot oscillate, and since the padding is always
|
|
32
|
+
* reserved for a preset, the border always sits in real space rather than
|
|
33
|
+
* hugging the canvas edge.
|
|
34
|
+
*
|
|
35
|
+
* A sweep drives *this* frame through its widths, behind an opaque cover. The
|
|
36
|
+
* obvious alternative — measuring a second, hidden iframe — keeps the preview
|
|
37
|
+
* visible but mounts the component twice, firing every mount-time fetch, timer
|
|
38
|
+
* and analytics call a second time. Covering the real frame costs nothing but
|
|
39
|
+
* six seconds of not watching, and it means the sweep measures the component
|
|
40
|
+
* you actually have, in whatever state you have put it in.
|
|
41
|
+
*/
|
|
42
|
+
export const Viewport = forwardRef(
|
|
43
|
+
/**
|
|
44
|
+
* @param {object} props
|
|
45
|
+
* @param {string | null} props.selectedId
|
|
46
|
+
* @param {"light" | "dark"} props.theme
|
|
47
|
+
* @param {import("../viewports.js").NoraViewport} props.viewport
|
|
48
|
+
* @param {boolean} props.sweeping
|
|
49
|
+
* @param {number} [props.sweepProgress] 0 to 1
|
|
50
|
+
* @param {(scale: number) => void} [props.onScale]
|
|
51
|
+
* @param {import("react").ForwardedRef<any>} ref
|
|
52
|
+
*/
|
|
53
|
+
function Viewport({ selectedId, theme, viewport, sweeping, sweepProgress, onScale }, ref) {
|
|
54
|
+
const containerRef = useRef(null);
|
|
55
|
+
const holderRef = useRef(null);
|
|
56
|
+
const frameRef = useRef(null);
|
|
57
|
+
const [avail, setAvail] = useState({ w: 0, h: 0 });
|
|
58
|
+
const [ready, setReady] = useState(false);
|
|
59
|
+
|
|
60
|
+
useLayoutEffect(() => {
|
|
61
|
+
const el = containerRef.current;
|
|
62
|
+
if (!el) return;
|
|
63
|
+
const observer = new ResizeObserver(([entry]) => {
|
|
64
|
+
const box = entry.contentRect;
|
|
65
|
+
setAvail({ w: box.width, h: box.height });
|
|
66
|
+
});
|
|
67
|
+
observer.observe(el);
|
|
68
|
+
return () => observer.disconnect();
|
|
69
|
+
}, []);
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
const onMessage = (event) => {
|
|
73
|
+
if (event.origin !== location.origin) return;
|
|
74
|
+
if (event.data?.source === CHANNEL && event.data.type === "ready") setReady(true);
|
|
75
|
+
};
|
|
76
|
+
window.addEventListener("message", onMessage);
|
|
77
|
+
return () => window.removeEventListener("message", onMessage);
|
|
78
|
+
}, []);
|
|
79
|
+
|
|
80
|
+
const post = () => {
|
|
81
|
+
frameRef.current?.contentWindow?.postMessage(
|
|
82
|
+
{ source: CHANNEL, type: "select", id: selectedId, theme },
|
|
83
|
+
location.origin,
|
|
84
|
+
);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
useEffect(post, [selectedId, theme, ready]);
|
|
88
|
+
|
|
89
|
+
const width = viewport.width;
|
|
90
|
+
const scale = width && avail.w > 0 ? Math.min(1, avail.w / width) : 1;
|
|
91
|
+
|
|
92
|
+
// The width itself already lives on the bar's viewport button, so the canvas
|
|
93
|
+
// carries no label. The scale is the one thing the button can't infer, so
|
|
94
|
+
// it's handed up rather than drawn here.
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
onScale?.(scale);
|
|
97
|
+
}, [scale, onScale]);
|
|
98
|
+
|
|
99
|
+
useImperativeHandle(ref, () => ({
|
|
100
|
+
/** Nothing to build — the frame is already mounted. */
|
|
101
|
+
async begin() {
|
|
102
|
+
// One frame so the cover is painted before the resizing starts.
|
|
103
|
+
await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)));
|
|
104
|
+
return true;
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
readDoc: () => frameRef.current?.contentDocument,
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Written straight to the DOM: going through React state would mean a
|
|
111
|
+
* hundred renders in a couple of seconds.
|
|
112
|
+
*
|
|
113
|
+
* The space available is measured here rather than read from `avail`,
|
|
114
|
+
* and that is load-bearing. A sweep captures this handle once and then
|
|
115
|
+
* calls it across several seconds; closing over a value from the render
|
|
116
|
+
* that produced the handle meant a window resize part-way through left
|
|
117
|
+
* every later call sizing the frame against a dead measurement. The
|
|
118
|
+
* width is set exactly either way, so the overflow and breakpoint
|
|
119
|
+
* findings were never wrong — but the frame's *height* is derived from
|
|
120
|
+
* it, and so was anything the component laid out against viewport
|
|
121
|
+
* height. Reading live makes the handle safe to hold.
|
|
122
|
+
*/
|
|
123
|
+
setWidth(w) {
|
|
124
|
+
const frame = frameRef.current;
|
|
125
|
+
const holder = holderRef.current;
|
|
126
|
+
const box = containerRef.current;
|
|
127
|
+
if (!frame || !holder || !box) return;
|
|
128
|
+
const rect = box.getBoundingClientRect();
|
|
129
|
+
const s = rect.width > 0 ? Math.min(1, rect.width / w) : 1;
|
|
130
|
+
frame.style.width = `${w}px`;
|
|
131
|
+
frame.style.height = `${Math.max(0, rect.height) / s}px`;
|
|
132
|
+
frame.style.transform = `scale(${s})`;
|
|
133
|
+
frame.style.transformOrigin = "top left";
|
|
134
|
+
holder.style.width = `${w * s}px`;
|
|
135
|
+
holder.style.height = `${Math.max(0, rect.height)}px`;
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Drop the imperative styles. React re-applies its own on the next render
|
|
140
|
+
* — both branches of frameStyle name every property written above, so
|
|
141
|
+
* nothing can survive the handover.
|
|
142
|
+
*/
|
|
143
|
+
end() {
|
|
144
|
+
for (const el of [frameRef.current, holderRef.current]) {
|
|
145
|
+
if (!el) continue;
|
|
146
|
+
el.style.removeProperty("width");
|
|
147
|
+
el.style.removeProperty("height");
|
|
148
|
+
el.style.removeProperty("transform");
|
|
149
|
+
el.style.removeProperty("transform-origin");
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
}));
|
|
153
|
+
|
|
154
|
+
const frameStyle = width
|
|
155
|
+
? {
|
|
156
|
+
width: `${width}px`,
|
|
157
|
+
height: `${Math.max(0, avail.h) / scale}px`,
|
|
158
|
+
transform: `scale(${scale})`,
|
|
159
|
+
transformOrigin: "top left",
|
|
160
|
+
}
|
|
161
|
+
: { width: "100%", height: "100%", transform: "none", transformOrigin: "top left" };
|
|
162
|
+
|
|
163
|
+
const holderStyle = width
|
|
164
|
+
? { width: `${width * scale}px`, height: `${Math.max(0, avail.h)}px` }
|
|
165
|
+
: { width: "100%", height: "100%" };
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<div
|
|
169
|
+
className="nora-viewport"
|
|
170
|
+
data-framed={width ? "true" : "false"}
|
|
171
|
+
data-sweeping={sweeping ? "true" : "false"}
|
|
172
|
+
ref={containerRef}
|
|
173
|
+
>
|
|
174
|
+
<div
|
|
175
|
+
className="nora-frame-holder"
|
|
176
|
+
ref={holderRef}
|
|
177
|
+
style={sweeping ? undefined : holderStyle}
|
|
178
|
+
>
|
|
179
|
+
<iframe
|
|
180
|
+
ref={frameRef}
|
|
181
|
+
className="nora-frame"
|
|
182
|
+
src={FRAME_URL}
|
|
183
|
+
title="Component preview"
|
|
184
|
+
style={sweeping ? undefined : frameStyle}
|
|
185
|
+
onLoad={post}
|
|
186
|
+
/>
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
{/* Opaque, so the hundred width changes underneath never reach the eye.
|
|
190
|
+
The frame keeps laying out and painting behind it, which is what the
|
|
191
|
+
measurements need. */}
|
|
192
|
+
{sweeping ? (
|
|
193
|
+
<div className="nora-sweep-cover" aria-live="polite">
|
|
194
|
+
<span className="nora-sweep-cover-label">
|
|
195
|
+
Checking breakpoints, {SWEEP_MIN}–{SWEEP_MAX}px…
|
|
196
|
+
</span>
|
|
197
|
+
{/* Progress belongs where the waiting happens. It used to be a
|
|
198
|
+
hairline inside the button that started the sweep, which is the
|
|
199
|
+
one place nobody is looking once the canvas is covered. */}
|
|
200
|
+
<span className="nora-sweep-cover-track">
|
|
201
|
+
<span
|
|
202
|
+
className="nora-sweep-cover-fill"
|
|
203
|
+
style={{ transform: `scaleX(${sweepProgress ?? 0})` }}
|
|
204
|
+
/>
|
|
205
|
+
</span>
|
|
206
|
+
</div>
|
|
207
|
+
) : null}
|
|
208
|
+
</div>
|
|
209
|
+
);
|
|
210
|
+
},
|
|
211
|
+
);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The chords nora claims, written down once.
|
|
3
|
+
*
|
|
4
|
+
* Two documents need this list and they are not the same page. The shell
|
|
5
|
+
* listens on its own window; the frame listens on its own and relays what it
|
|
6
|
+
* catches, because clicking a previewed component moves focus inside the iframe
|
|
7
|
+
* and every shortcut would otherwise stop working. Those two lists used to be
|
|
8
|
+
* written separately and had to be kept in step by hand: the frame decided what
|
|
9
|
+
* was "ours", and the shell independently decided what to do with it.
|
|
10
|
+
*
|
|
11
|
+
* Recognising a chord and acting on one are now separate jobs. This module only
|
|
12
|
+
* names them, so both documents can agree on the vocabulary without either
|
|
13
|
+
* knowing what the other does about it.
|
|
14
|
+
*
|
|
15
|
+
* Order matters: the first match wins, so the more specific chords come first.
|
|
16
|
+
*
|
|
17
|
+
* @typedef {"picker" | "previous" | "next" | "dismiss"} Chord
|
|
18
|
+
* @typedef {{ key: string, metaKey?: boolean, ctrlKey?: boolean, altKey?: boolean }} Chordable
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** @type {[Chord, (e: Chordable) => boolean][]} */
|
|
22
|
+
const CHORDS = [
|
|
23
|
+
["picker", (e) => (e.metaKey || e.ctrlKey) && /^[ko]$/.test(e.key.toLowerCase())],
|
|
24
|
+
["previous", (e) => e.altKey && e.key === "ArrowUp"],
|
|
25
|
+
["next", (e) => e.altKey && e.key === "ArrowDown"],
|
|
26
|
+
["dismiss", (e) => e.key === "Escape"],
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {Chordable} [event]
|
|
31
|
+
* @returns {Chord | null} null for anything nora does not claim, which is most
|
|
32
|
+
* keys: a previewed text field, slider or menu must keep working normally.
|
|
33
|
+
*/
|
|
34
|
+
export function matchChord(event) {
|
|
35
|
+
if (!event?.key) return null;
|
|
36
|
+
for (const [name, test] of CHORDS) if (test(event)) return name;
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/* =========================================================================
|
|
2
|
+
Type — Geist and Geist Mono, subset and inlined.
|
|
3
|
+
|
|
4
|
+
Bundled as base64 rather than fetched from a CDN because this tool boots a
|
|
5
|
+
local dev server: it has to look the same on a plane, behind a proxy, and on
|
|
6
|
+
a machine whose Vite config blocks outside hosts. A brand that only shows up
|
|
7
|
+
when the network cooperates is not a brand.
|
|
8
|
+
|
|
9
|
+
The cost of that decision is measured, not assumed. Each face is subset to
|
|
10
|
+
the glyphs this UI actually draws (ASCII plus a handful of typographic and
|
|
11
|
+
arrow characters; the mono face carries U+2190 as well, because the picker
|
|
12
|
+
draws a left arrow and the sans face never does) and its weight axis clamped to 400-600, which is the range
|
|
13
|
+
the styles use:
|
|
14
|
+
|
|
15
|
+
Geist 28.7 KB -> 11.7 KB (15.5 KB as base64)
|
|
16
|
+
Geist Mono 22.6 KB -> 8.4 KB (11.2 KB as base64)
|
|
17
|
+
|
|
18
|
+
~27 KB of CSS, parsed once, no request, no FOUT. A `unicode-range` is
|
|
19
|
+
declared so that anything outside the subset - a component name with an
|
|
20
|
+
accent, the macOS command and option glyphs, which Geist does not carry -
|
|
21
|
+
falls through to the stack behind it rather than rendering as tofu.
|
|
22
|
+
|
|
23
|
+
The families are namespaced rather than called "Geist": these faces carry
|
|
24
|
+
only part of the character set, and a project that loads the real Geist
|
|
25
|
+
should not find our partial copy shadowing it.
|
|
26
|
+
|
|
27
|
+
Geist and Geist Mono (v5.3.0, via @fontsource-variable) are licensed under
|
|
28
|
+
the SIL Open Font License 1.1. Copyright 2024 The Geist Project Authors,
|
|
29
|
+
https://github.com/vercel/geist-font. Full text: licenses/geist-OFL.txt.
|
|
30
|
+
========================================================================= */
|
|
31
|
+
|
|
32
|
+
@font-face {
|
|
33
|
+
font-family: "Nora Geist";
|
|
34
|
+
font-style: normal;
|
|
35
|
+
font-weight: 400 600;
|
|
36
|
+
font-display: block;
|
|
37
|
+
unicode-range: U+0020-007E, U+00B7, U+2013-2014, U+201C-201D, U+2026, U+2191-2193, U+21B5;
|
|
38
|
+
src: url(data:font/woff2;base64,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) format("woff2");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@font-face {
|
|
42
|
+
font-family: "Nora Geist Mono";
|
|
43
|
+
font-style: normal;
|
|
44
|
+
font-weight: 400 600;
|
|
45
|
+
font-display: block;
|
|
46
|
+
unicode-range: U+0020-007E, U+00B7, U+2013-2014, U+201C-201D, U+2026, U+2190-2193, U+21B5;
|
|
47
|
+
src: url(data:font/woff2;base64,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) format("woff2");
|
|
48
|
+
}
|