@1agh/maude 0.19.1 → 0.21.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/README.md +7 -0
- package/cli/bin/maude.mjs +5 -1
- package/cli/commands/design-link.test.mjs +207 -0
- package/cli/commands/design.mjs +42 -12
- package/cli/commands/doctor.mjs +350 -0
- package/cli/commands/doctor.test.mjs +185 -0
- package/cli/commands/help.mjs +24 -0
- package/cli/commands/hub.mjs +231 -0
- package/cli/commands/hub.test.mjs +87 -0
- package/cli/lib/config-lint.mjs +141 -0
- package/cli/lib/config-lint.test.mjs +117 -0
- package/cli/lib/design-link.mjs +216 -0
- package/cli/lib/hubs-config.mjs +123 -0
- package/cli/lib/hubs-config.test.mjs +100 -0
- package/cli/lib/preflight.mjs +232 -0
- package/cli/lib/stack-detect.mjs +344 -0
- package/cli/lib/stack-detect.test.mjs +121 -0
- package/package.json +17 -9
- package/plugins/design/dependencies.json +147 -0
- package/plugins/design/dependencies.schema.json +107 -0
- package/plugins/design/dev-server/ai-banner.tsx +188 -0
- package/plugins/design/dev-server/annotations-context-toolbar.tsx +115 -41
- package/plugins/design/dev-server/annotations-layer.tsx +256 -107
- package/plugins/design/dev-server/api.ts +17 -1
- package/plugins/design/dev-server/artboard-marquee.tsx +10 -6
- package/plugins/design/dev-server/bin/asset-sweep.sh +180 -0
- package/plugins/design/dev-server/bin/preflight.sh +32 -0
- package/plugins/design/dev-server/bin/runtime-health.sh +169 -0
- package/plugins/design/dev-server/bin/visual-sanity.sh +221 -0
- package/plugins/design/dev-server/canvas-lib.tsx +534 -32
- package/plugins/design/dev-server/canvas-shell.tsx +479 -29
- package/plugins/design/dev-server/client/app.jsx +72 -0
- package/plugins/design/dev-server/client/hmr.mjs +28 -0
- package/plugins/design/dev-server/collab/ai-activity.ts +127 -0
- package/plugins/design/dev-server/collab/git-lifecycle.ts +124 -0
- package/plugins/design/dev-server/collab/index.ts +47 -0
- package/plugins/design/dev-server/collab/persistence.ts +123 -0
- package/plugins/design/dev-server/collab/protocol.ts +108 -0
- package/plugins/design/dev-server/collab/registry.ts +110 -0
- package/plugins/design/dev-server/collab/room.ts +215 -0
- package/plugins/design/dev-server/commands/annotation-strokes-command.ts +127 -0
- package/plugins/design/dev-server/commands/equal-spacing-command.ts +28 -0
- package/plugins/design/dev-server/commands/move-artboards-command.ts +158 -0
- package/plugins/design/dev-server/comments-overlay.tsx +41 -4
- package/plugins/design/dev-server/contextual-toolbar.tsx +241 -0
- package/plugins/design/dev-server/cursors-overlay.tsx +296 -0
- package/plugins/design/dev-server/dist/client.bundle.js +75 -3
- package/plugins/design/dev-server/dist/runtime/motion.js +165 -0
- package/plugins/design/dev-server/dist/runtime/motion_react.js +409 -0
- package/plugins/design/dev-server/dist/runtime/y-protocols_awareness.js +587 -0
- package/plugins/design/dev-server/dist/runtime/y-protocols_sync.js +257 -0
- package/plugins/design/dev-server/dist/runtime/yjs.js +7107 -0
- package/plugins/design/dev-server/equal-spacing-detector.ts +98 -0
- package/plugins/design/dev-server/equal-spacing-handles.tsx +289 -0
- package/plugins/design/dev-server/export-dialog.tsx +1 -1
- package/plugins/design/dev-server/handoff.ts +24 -0
- package/plugins/design/dev-server/hmr-broadcast.ts +15 -2
- package/plugins/design/dev-server/http.ts +91 -1
- package/plugins/design/dev-server/input-router.tsx +52 -2
- package/plugins/design/dev-server/marquee-overlay.tsx +282 -0
- package/plugins/design/dev-server/participants-chrome.tsx +261 -0
- package/plugins/design/dev-server/runtime-bundle.ts +15 -0
- package/plugins/design/dev-server/server.ts +78 -11
- package/plugins/design/dev-server/test/ai-activity.test.ts +113 -0
- package/plugins/design/dev-server/test/annotation-strokes-command.test.ts +149 -0
- package/plugins/design/dev-server/test/annotations-layer.test.ts +44 -0
- package/plugins/design/dev-server/test/canvas-lib-motion.test.ts +131 -0
- package/plugins/design/dev-server/test/collab-annotations-bridge.test.ts +55 -0
- package/plugins/design/dev-server/test/collab-bridge.test.ts +81 -0
- package/plugins/design/dev-server/test/collab-loopback.test.ts +63 -0
- package/plugins/design/dev-server/test/collab-protocol.test.ts +146 -0
- package/plugins/design/dev-server/test/collab-room.test.ts +182 -0
- package/plugins/design/dev-server/test/collab-stress.test.ts +121 -0
- package/plugins/design/dev-server/test/equal-spacing-detector.test.ts +93 -0
- package/plugins/design/dev-server/test/git-lifecycle.test.ts +101 -0
- package/plugins/design/dev-server/test/input-router.test.ts +87 -1
- package/plugins/design/dev-server/test/marquee-overlay.test.ts +94 -0
- package/plugins/design/dev-server/test/move-artboards-command.test.ts +108 -0
- package/plugins/design/dev-server/test/participants-chrome.test.ts +30 -0
- package/plugins/design/dev-server/test/undo-stack.test.ts +211 -0
- package/plugins/design/dev-server/test/use-collab.test.ts +71 -0
- package/plugins/design/dev-server/test/use-cursor-modifiers.test.ts +59 -0
- package/plugins/design/dev-server/test/use-keyboard-discipline.test.ts +27 -0
- package/plugins/design/dev-server/test/use-undo-stack.test.tsx +193 -0
- package/plugins/design/dev-server/tool-palette.tsx +7 -7
- package/plugins/design/dev-server/undo-hud.tsx +95 -0
- package/plugins/design/dev-server/undo-stack.ts +240 -0
- package/plugins/design/dev-server/use-annotation-resize.tsx +1 -1
- package/plugins/design/dev-server/use-artboard-drag.tsx +6 -3
- package/plugins/design/dev-server/use-collab.tsx +478 -0
- package/plugins/design/dev-server/use-cursor-modifiers.tsx +122 -0
- package/plugins/design/dev-server/use-keyboard-discipline.tsx +125 -0
- package/plugins/design/dev-server/use-undo-stack.tsx +355 -0
- package/plugins/design/dev-server/ws.ts +123 -7
- package/plugins/design/templates/_shell.html +51 -6
- package/plugins/design/templates/design-system-inspiration/SUB-AGENT-PROMPTS.md +245 -0
- package/plugins/design/templates/design-system-inspiration/_MAPPING.md +2 -2
- package/plugins/design/templates/design-system-inspiration/core/preview/_components.css.tpl +129 -0
- package/plugins/design/templates/design-system-inspiration/core/preview/_motion-readme.md.tpl +63 -0
- package/plugins/design/templates/design-system-inspiration/core/preview/motion.css.tpl +106 -0
- package/plugins/design/templates/design-system-inspiration/core/preview/motion.tsx.tpl +208 -0
- package/plugins/flow/.claude-plugin/config.schema.json +12 -0
- package/plugins/flow/dependencies.json +143 -0
- package/plugins/flow/dependencies.schema.json +107 -0
- /package/plugins/design/templates/design-system-inspiration/core/preview/{motion.html → .archive/motion.html} +0 -0
|
@@ -81,10 +81,22 @@ import {
|
|
|
81
81
|
useState,
|
|
82
82
|
} from 'react';
|
|
83
83
|
|
|
84
|
+
import {
|
|
85
|
+
AnimatePresence as _MotionAnimatePresence,
|
|
86
|
+
motion as _motionImpl,
|
|
87
|
+
useReducedMotion as _useReducedMotion,
|
|
88
|
+
} from 'motion/react';
|
|
89
|
+
|
|
84
90
|
import { CanvasShell } from './canvas-shell.tsx';
|
|
91
|
+
import {
|
|
92
|
+
buildMoveArtboardsRecord,
|
|
93
|
+
diffLayoutPositions,
|
|
94
|
+
} from './commands/move-artboards-command.ts';
|
|
85
95
|
import { type DragState, useArtboardDrag } from './use-artboard-drag.tsx';
|
|
96
|
+
import { CollabProvider, canvasSlugFromPath } from './use-collab.tsx';
|
|
86
97
|
import { useSelectionSetOptional } from './use-selection-set.tsx';
|
|
87
98
|
import { ToolProvider, useToolModeOptional } from './use-tool-mode.tsx';
|
|
99
|
+
import { UndoStackProvider, useUndoSinks, useUndoStackOptional } from './use-undo-stack.tsx';
|
|
88
100
|
|
|
89
101
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
90
102
|
// Module constants
|
|
@@ -216,7 +228,7 @@ button.dc-artboard-label {
|
|
|
216
228
|
display: block;
|
|
217
229
|
width: 100%;
|
|
218
230
|
}
|
|
219
|
-
button.dc-artboard-label:focus-visible { outline: 2px solid var(--accent, #d63b1f); outline-offset: -2px; }
|
|
231
|
+
button.dc-artboard-label:focus-visible { outline: 2px solid var(--maude-hud-accent, #d63b1f); outline-offset: -2px; }
|
|
220
232
|
/* Active-artboard ring is in canvas-shell HALO_CSS (subtle 1 px tint). */
|
|
221
233
|
/* Phase 4.2 — drag chrome. */
|
|
222
234
|
.dc-canvas[data-active-tool="move"] .dc-artboard-label { cursor: grab; }
|
|
@@ -448,6 +460,26 @@ interface PersistedArtboardLayout {
|
|
|
448
460
|
y: number;
|
|
449
461
|
}
|
|
450
462
|
|
|
463
|
+
/**
|
|
464
|
+
* Phase 20 (DDR-050) — last timestamp at which THIS iframe wrote canvas
|
|
465
|
+
* meta. Read by `use-undo-stack.tsx` to discriminate self-echo fs:json
|
|
466
|
+
* events from genuine external edits.
|
|
467
|
+
*/
|
|
468
|
+
declare global {
|
|
469
|
+
interface Window {
|
|
470
|
+
__maude_last_meta_self_write_at?: number;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const META_SELF_ECHO_WINDOW_MS = 500;
|
|
475
|
+
|
|
476
|
+
/** Recent enough to be our own PATCH echoing back through fs-watch. */
|
|
477
|
+
export function isMetaSelfEcho(now: number = Date.now()): boolean {
|
|
478
|
+
if (typeof window === 'undefined') return false;
|
|
479
|
+
const last = window.__maude_last_meta_self_write_at ?? 0;
|
|
480
|
+
return now - last < META_SELF_ECHO_WINDOW_MS;
|
|
481
|
+
}
|
|
482
|
+
|
|
451
483
|
function patchCanvasMeta(patch: {
|
|
452
484
|
viewport?: ViewportState;
|
|
453
485
|
layout?: { artboards: ArtboardRect[] };
|
|
@@ -469,6 +501,10 @@ function patchCanvasMeta(patch: {
|
|
|
469
501
|
})),
|
|
470
502
|
};
|
|
471
503
|
}
|
|
504
|
+
// Stamp the self-write timestamp BEFORE the fetch so the round-trip
|
|
505
|
+
// (PATCH → server write → fs:json broadcast → iframe message) lands
|
|
506
|
+
// safely inside the echo window even on a fast network.
|
|
507
|
+
window.__maude_last_meta_self_write_at = Date.now();
|
|
472
508
|
fetch('/_api/canvas-meta', {
|
|
473
509
|
method: 'PATCH',
|
|
474
510
|
headers: { 'content-type': 'application/json' },
|
|
@@ -700,9 +736,22 @@ export function useViewportController(opts: ViewportControllerOptions): Viewport
|
|
|
700
736
|
[applyViewport]
|
|
701
737
|
);
|
|
702
738
|
|
|
739
|
+
// Forward-decl ref — fit / reset run animations that need `animateTo`, but
|
|
740
|
+
// `animateTo` is declared further down (depends on applyViewport). Resolve
|
|
741
|
+
// the ordering with a ref the animateTo callback writes to on definition.
|
|
742
|
+
const animateToRef = useRef<((t: ViewportState, d?: number) => void) | null>(null);
|
|
743
|
+
|
|
744
|
+
// T33 — programmatic zoom is eased. Direct user-driven gestures
|
|
745
|
+
// (wheel, pinch, drag-to-pan) still call `applyViewport` directly so
|
|
746
|
+
// input feel stays 1:1 with the cursor; only fit/reset/zoom-to-rect
|
|
747
|
+
// ease over 200 ms. `animateTo` honors `prefers-reduced-motion` and
|
|
748
|
+
// returns instantly under that media query.
|
|
749
|
+
const PROGRAMMATIC_EASE_MS = 200;
|
|
703
750
|
const fit = useCallback(() => {
|
|
704
751
|
const next = computeFitRef.current();
|
|
705
|
-
|
|
752
|
+
const a = animateToRef.current;
|
|
753
|
+
if (a) a(next, PROGRAMMATIC_EASE_MS);
|
|
754
|
+
else applyViewport(next);
|
|
706
755
|
}, [applyViewport]);
|
|
707
756
|
|
|
708
757
|
const reset = useCallback(() => {
|
|
@@ -713,8 +762,15 @@ export function useViewportController(opts: ViewportControllerOptions): Viewport
|
|
|
713
762
|
}
|
|
714
763
|
const cx = host.clientWidth / 2;
|
|
715
764
|
const cy = host.clientHeight / 2;
|
|
716
|
-
|
|
717
|
-
|
|
765
|
+
const v = vpRef.current;
|
|
766
|
+
// Target: same world-coord under cursor, zoom = 1.
|
|
767
|
+
const wx = (cx - v.x) / v.zoom;
|
|
768
|
+
const wy = (cy - v.y) / v.zoom;
|
|
769
|
+
const target: ViewportState = { x: cx - wx, y: cy - wy, zoom: 1 };
|
|
770
|
+
const a = animateToRef.current;
|
|
771
|
+
if (a) a(target, PROGRAMMATIC_EASE_MS);
|
|
772
|
+
else applyViewport(target);
|
|
773
|
+
}, [hostRef, applyViewport]);
|
|
718
774
|
|
|
719
775
|
const zoomIn = useCallback(() => {
|
|
720
776
|
const host = hostRef.current;
|
|
@@ -767,6 +823,10 @@ export function useViewportController(opts: ViewportControllerOptions): Viewport
|
|
|
767
823
|
[applyViewport]
|
|
768
824
|
);
|
|
769
825
|
|
|
826
|
+
// Keep the ref pointed at the latest `animateTo` so the earlier-declared
|
|
827
|
+
// fit / reset callbacks can call it without circular dependency.
|
|
828
|
+
animateToRef.current = animateTo;
|
|
829
|
+
|
|
770
830
|
const jumpTo = useCallback(
|
|
771
831
|
(rect: ArtboardRect) => {
|
|
772
832
|
const host = hostRef.current;
|
|
@@ -844,7 +904,13 @@ export function useViewportController(opts: ViewportControllerOptions): Viewport
|
|
|
844
904
|
// without a physical Ctrl press — so the same branch covers both
|
|
845
905
|
// Ctrl+wheel (mouse) and pinch-zoom (trackpad).
|
|
846
906
|
if (e.ctrlKey || e.metaKey) {
|
|
847
|
-
|
|
907
|
+
// T32 — clamp deltaY into [-50, 50] before the exp() to bring
|
|
908
|
+
// trackpad-pinch (small per-frame delta) and mouse-wheel (one
|
|
909
|
+
// notch = ±100) onto the same perceived-speed curve. Mouse-wheel
|
|
910
|
+
// users still get smooth zoom (clamped notches accumulate at the
|
|
911
|
+
// same exp rate), trackpad-pinch users no longer outpace them.
|
|
912
|
+
const clamped = Math.max(-50, Math.min(50, e.deltaY));
|
|
913
|
+
const factor = Math.exp(-clamped * WHEEL_ZOOM_K);
|
|
848
914
|
zoomAt(factor, cx, cy);
|
|
849
915
|
return;
|
|
850
916
|
}
|
|
@@ -1052,11 +1118,24 @@ export function useViewportControllerContext(): ViewportControllerHandle | null
|
|
|
1052
1118
|
// drag). A single source-of-truth: only one drag can be active at a time, so
|
|
1053
1119
|
// the bus holds a single DragState. Each DCArtboard's hook writes here when
|
|
1054
1120
|
// non-idle and resets to idle on release.
|
|
1121
|
+
/** Optional metadata accompanying a position commit. */
|
|
1122
|
+
export interface CommitPositionsOptions {
|
|
1123
|
+
/**
|
|
1124
|
+
* HUD / undo-stack label override. Default = `"move N artboard(s)"`.
|
|
1125
|
+
* Distribute / align gestures pass their own label so undo HUD reads
|
|
1126
|
+
* `"Undo: equal-space 4 artboards"` instead of `"Undo: move 4 artboards"`.
|
|
1127
|
+
*/
|
|
1128
|
+
label?: string;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1055
1131
|
interface DragStateBus {
|
|
1056
1132
|
current: DragState;
|
|
1057
1133
|
setCurrent: (s: DragState) => void;
|
|
1058
1134
|
/** Commit drag positions — DesignCanvas wires this to patchCanvasMeta. */
|
|
1059
|
-
commitPositions: (
|
|
1135
|
+
commitPositions: (
|
|
1136
|
+
moved: { id: string; x: number; y: number }[],
|
|
1137
|
+
opts?: CommitPositionsOptions
|
|
1138
|
+
) => void;
|
|
1060
1139
|
}
|
|
1061
1140
|
|
|
1062
1141
|
const DragStateContext = createContext<DragStateBus | null>(null);
|
|
@@ -1085,11 +1164,29 @@ interface DesignCanvasProps {
|
|
|
1085
1164
|
* via `useToolModeOptional` (hand-mode bare-drag pan).
|
|
1086
1165
|
*/
|
|
1087
1166
|
export function DesignCanvas(props: DesignCanvasProps) {
|
|
1088
|
-
|
|
1167
|
+
// Phase 20 — per-canvas undo/redo stack (DDR-050 rev 2). The provider
|
|
1168
|
+
// wraps both DesignCanvasInner (so artboard commits push records) AND
|
|
1169
|
+
// the CanvasShell tree (so input-router Cmd+Z / Cmd+Shift+Z + the HUD
|
|
1170
|
+
// share the same context). Stack state is keyed by canvas file path in
|
|
1171
|
+
// `window.top.__maude_undo_stacks` so it survives canvas switches —
|
|
1172
|
+
// close Foo.tsx, open Bar.tsx, come back to Foo.tsx → history intact.
|
|
1173
|
+
const canvasFile = readCanvasMetaFile() ?? undefined;
|
|
1174
|
+
// Phase 8 / DDR-051 — open a Yjs collab session for this canvas iff we can
|
|
1175
|
+
// derive a stable slug. The slug must match `api.fileSlug` server-side so
|
|
1176
|
+
// both ends agree on the room key. When the canvas was opened via a URL
|
|
1177
|
+
// that doesn't yield a slug (e.g. preview iframes without `canvas=`),
|
|
1178
|
+
// CollabProvider is omitted; useCollab() falls back gracefully to null.
|
|
1179
|
+
const collabSlug = canvasSlugFromPath(canvasFile);
|
|
1180
|
+
const inner = (
|
|
1089
1181
|
<ToolProvider>
|
|
1090
1182
|
<DesignCanvasInner {...props} />
|
|
1091
1183
|
</ToolProvider>
|
|
1092
1184
|
);
|
|
1185
|
+
return (
|
|
1186
|
+
<UndoStackProvider canvasFile={canvasFile}>
|
|
1187
|
+
{collabSlug ? <CollabProvider slug={collabSlug}>{inner}</CollabProvider> : inner}
|
|
1188
|
+
</UndoStackProvider>
|
|
1189
|
+
);
|
|
1093
1190
|
}
|
|
1094
1191
|
DesignCanvas.displayName = 'DesignCanvas';
|
|
1095
1192
|
|
|
@@ -1141,6 +1238,22 @@ function DesignCanvasInner({ children, controls }: DesignCanvasProps) {
|
|
|
1141
1238
|
setArtboards(initialArtboards());
|
|
1142
1239
|
}, [initialArtboards]);
|
|
1143
1240
|
|
|
1241
|
+
// Phase 8 — foreign canvas-meta change. The shell-level HMR client
|
|
1242
|
+
// re-fetches `<canvas>.meta.json` and dispatches `maude:meta-refreshed`
|
|
1243
|
+
// when *another* tab PATCHed the layout (drag, distribute, align). We
|
|
1244
|
+
// re-apply positions in-place — no full reload — so the user's tool mode,
|
|
1245
|
+
// undo stack, scroll, and selection state survive. Self-writes are
|
|
1246
|
+
// suppressed at the dispatch site via the `__maude_last_meta_self_write_at`
|
|
1247
|
+
// echo timestamp.
|
|
1248
|
+
useEffect(() => {
|
|
1249
|
+
if (typeof document === 'undefined') return;
|
|
1250
|
+
const onRefresh = () => {
|
|
1251
|
+
setArtboards(initialArtboards());
|
|
1252
|
+
};
|
|
1253
|
+
document.addEventListener('maude:meta-refreshed', onRefresh);
|
|
1254
|
+
return () => document.removeEventListener('maude:meta-refreshed', onRefresh);
|
|
1255
|
+
}, [initialArtboards]);
|
|
1256
|
+
|
|
1144
1257
|
// Stable refs so the controller's callbacks always see the latest values.
|
|
1145
1258
|
const artboardsRef = useRef(artboards);
|
|
1146
1259
|
artboardsRef.current = artboards;
|
|
@@ -1239,20 +1352,56 @@ function DesignCanvasInner({ children, controls }: DesignCanvasProps) {
|
|
|
1239
1352
|
// hook is non-idle; SnapGuideOverlay (in canvas-shell) reads guides.
|
|
1240
1353
|
const [dragCurrent, setDragCurrent] = useState<DragState>({ kind: 'idle' });
|
|
1241
1354
|
|
|
1242
|
-
const
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1355
|
+
const undoStack = useUndoStackOptional();
|
|
1356
|
+
const undoSinks = useUndoSinks();
|
|
1357
|
+
// Stable ref so the commit callback (memoized once, on mount) always reads
|
|
1358
|
+
// the latest stack value without re-creating the callback on every render.
|
|
1359
|
+
const undoStackRef = useRef(undoStack);
|
|
1360
|
+
undoStackRef.current = undoStack;
|
|
1361
|
+
|
|
1362
|
+
/**
|
|
1363
|
+
* Applies a full artboard layout: optimistic local React state update +
|
|
1364
|
+
* server PATCH. Used as the `layoutPatchFn` sink registered with the
|
|
1365
|
+
* undo stack — both the initial commit (do) and every undo/redo replay
|
|
1366
|
+
* route through here, so React state always tracks the server.
|
|
1367
|
+
*/
|
|
1368
|
+
const applyArtboardLayout = useCallback((layout: unknown) => {
|
|
1369
|
+
setArtboards(layout as ArtboardRect[]);
|
|
1370
|
+
patchCanvasMeta({ layout: { artboards: layout as ArtboardRect[] } });
|
|
1254
1371
|
}, []);
|
|
1255
1372
|
|
|
1373
|
+
// Register the layout patch sink with the undo provider so the rebuilt
|
|
1374
|
+
// MoveArtboardsCommand (after a canvas switch + return) can apply layouts
|
|
1375
|
+
// through THIS iframe's React state, not the gone iframe's stale closures.
|
|
1376
|
+
useEffect(() => {
|
|
1377
|
+
undoSinks.setSink('layoutPatchFn', applyArtboardLayout);
|
|
1378
|
+
return () => undoSinks.setSink('layoutPatchFn', undefined);
|
|
1379
|
+
}, [undoSinks, applyArtboardLayout]);
|
|
1380
|
+
|
|
1381
|
+
const commitArtboardPositions = useCallback(
|
|
1382
|
+
(moved: { id: string; x: number; y: number }[], opts?: CommitPositionsOptions) => {
|
|
1383
|
+
const movedById = new Map(moved.map((m) => [m.id, m]));
|
|
1384
|
+
const before = artboardsRef.current;
|
|
1385
|
+
const next = before.map((r) => {
|
|
1386
|
+
const m = movedById.get(r.id);
|
|
1387
|
+
if (m) return { ...r, x: m.x, y: m.y };
|
|
1388
|
+
return r;
|
|
1389
|
+
});
|
|
1390
|
+
// Phase 20 — skip pushing no-op drags (click-without-movement). The
|
|
1391
|
+
// user got back what they had, no edit happened, undo would do nothing.
|
|
1392
|
+
if (!diffLayoutPositions(before, next)) return;
|
|
1393
|
+
const record = buildMoveArtboardsRecord({
|
|
1394
|
+
before: before.map((r) => ({ id: r.id, x: r.x, y: r.y, w: r.w, h: r.h })),
|
|
1395
|
+
after: next.map((r) => ({ id: r.id, x: r.x, y: r.y, w: r.w, h: r.h })),
|
|
1396
|
+
...(opts?.label ? { label: opts.label } : {}),
|
|
1397
|
+
});
|
|
1398
|
+
// push() invokes the rebuilt cmd.do() = applyArtboardLayout(next) BEFORE
|
|
1399
|
+
// updating the stack, so the optimistic local + PATCH flow is preserved.
|
|
1400
|
+
void undoStackRef.current.push(record);
|
|
1401
|
+
},
|
|
1402
|
+
[]
|
|
1403
|
+
);
|
|
1404
|
+
|
|
1256
1405
|
const dragBus = useMemo<DragStateBus>(
|
|
1257
1406
|
() => ({
|
|
1258
1407
|
current: dragCurrent,
|
|
@@ -1348,7 +1497,7 @@ export function DCArtboard({
|
|
|
1348
1497
|
children: ReactNode;
|
|
1349
1498
|
}) {
|
|
1350
1499
|
const ctx = useWorldContext();
|
|
1351
|
-
const
|
|
1500
|
+
const _controller = useViewportControllerContext();
|
|
1352
1501
|
const toolMode = useToolModeOptional();
|
|
1353
1502
|
const selSet = useSelectionSetOptional();
|
|
1354
1503
|
const dragBus = useDragStateContext();
|
|
@@ -1394,9 +1543,11 @@ export function DCArtboard({
|
|
|
1394
1543
|
);
|
|
1395
1544
|
}
|
|
1396
1545
|
const isActive = ctx.activeArtboardId === id;
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1546
|
+
// G2v2 — earlier the label single-click called `controller.jumpTo(rect)`,
|
|
1547
|
+
// auto-zooming on every click. Per post-Wave-3.5 feedback this was
|
|
1548
|
+
// surprising. The label button stays for a11y (focus + screen-reader
|
|
1549
|
+
// label) but no longer mutates the viewport. Cmd+1 + the zoom HUD still
|
|
1550
|
+
// expose the manual zoom path.
|
|
1400
1551
|
|
|
1401
1552
|
// Am I involved in the current drag (as leader or follower)?
|
|
1402
1553
|
const busDrag = dragBus?.current;
|
|
@@ -1431,12 +1582,7 @@ export function DCArtboard({
|
|
|
1431
1582
|
style={{ left: liveX, top: liveY, width: rect.w, height: rect.h }}
|
|
1432
1583
|
{...handleProps}
|
|
1433
1584
|
>
|
|
1434
|
-
<button
|
|
1435
|
-
type="button"
|
|
1436
|
-
className="dc-artboard-label sku"
|
|
1437
|
-
onClick={onFocus}
|
|
1438
|
-
aria-label={`Focus artboard ${label}`}
|
|
1439
|
-
>
|
|
1585
|
+
<button type="button" className="dc-artboard-label sku" aria-label={`Artboard ${label}`}>
|
|
1440
1586
|
{label}
|
|
1441
1587
|
</button>
|
|
1442
1588
|
<div className="dc-artboard-body">{children}</div>
|
|
@@ -1616,8 +1762,8 @@ const OVERLAY_CSS = `
|
|
|
1616
1762
|
outline-only. Reads from a glance as "what slice of the world you're on". */
|
|
1617
1763
|
.dc-mm-vp {
|
|
1618
1764
|
position: absolute;
|
|
1619
|
-
background: color-mix(in oklab, var(--accent, #d63b1f) 12%, transparent);
|
|
1620
|
-
border: 1.5px solid var(--accent, #d63b1f);
|
|
1765
|
+
background: color-mix(in oklab, var(--maude-hud-accent, #d63b1f) 12%, transparent);
|
|
1766
|
+
border: 1.5px solid var(--maude-hud-accent, #d63b1f);
|
|
1621
1767
|
border-radius: 1px;
|
|
1622
1768
|
pointer-events: none;
|
|
1623
1769
|
}
|
|
@@ -1653,7 +1799,7 @@ const OVERLAY_CSS = `
|
|
|
1653
1799
|
}
|
|
1654
1800
|
.dc-zoom-tb button:last-child { border-right: 0; }
|
|
1655
1801
|
.dc-zoom-tb button:hover { background: color-mix(in oklab, var(--fg-0, #1c1917) 5%, transparent); }
|
|
1656
|
-
.dc-zoom-tb button:focus-visible { outline: 2px solid var(--accent, #d63b1f); outline-offset: -2px; }
|
|
1802
|
+
.dc-zoom-tb button:focus-visible { outline: 2px solid var(--maude-hud-accent, #d63b1f); outline-offset: -2px; }
|
|
1657
1803
|
.dc-zoom-tb-pct { font-variant-numeric: tabular-nums; min-width: 52px; }
|
|
1658
1804
|
`.trim();
|
|
1659
1805
|
|
|
@@ -2088,3 +2234,359 @@ export function useArtboardBounds(ref: RefObject<HTMLElement | null>): {
|
|
|
2088
2234
|
// Re-export `useRef` so `useArtboardBounds` consumers can keep a single
|
|
2089
2235
|
// import line from `@maude/canvas-lib`.
|
|
2090
2236
|
export { useRef };
|
|
2237
|
+
|
|
2238
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2239
|
+
// Motion subsystem (Phase 3.7 / DDR-049 — Motion One is the canonical runtime)
|
|
2240
|
+
//
|
|
2241
|
+
// These helpers are tree-shakeable. A canvas that does not import any of them
|
|
2242
|
+
// pays no bundle cost (motion/react is externalised via RUNTIME_PACKAGES, so
|
|
2243
|
+
// even when imported the byte cost lives in a single shared runtime bundle,
|
|
2244
|
+
// not per-canvas).
|
|
2245
|
+
//
|
|
2246
|
+
// Roles map 1:1 to the 8 motion-vocabulary names enforced by motion-critic +
|
|
2247
|
+
// design-system-keeper. Each role binds to a DS duration + easing token from
|
|
2248
|
+
// colors_and_type.css; useMotionTokens() reads the live CSS custom property
|
|
2249
|
+
// values so the binding survives token edits without rebuilding canvas-lib.
|
|
2250
|
+
//
|
|
2251
|
+
// Bounded-geometry guarantee — every <MotionDemo> root sets `overflow: hidden`
|
|
2252
|
+
// in inline style. That defends against sparkle-on-tile overflow regardless of
|
|
2253
|
+
// the host class chrome. See SUB-AGENT-PROMPTS.md → ANIMATION SAFETY.
|
|
2254
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2255
|
+
|
|
2256
|
+
export type MotionRole =
|
|
2257
|
+
| 'flip'
|
|
2258
|
+
| 'panel'
|
|
2259
|
+
| 'route'
|
|
2260
|
+
| 'soft'
|
|
2261
|
+
| 'spring'
|
|
2262
|
+
| 'scroll'
|
|
2263
|
+
| 'drag'
|
|
2264
|
+
| 'presence';
|
|
2265
|
+
|
|
2266
|
+
export type MotionLoop = 'always' | 'hover' | 'once';
|
|
2267
|
+
|
|
2268
|
+
interface RoleConfig {
|
|
2269
|
+
durationToken: string;
|
|
2270
|
+
easingToken: string;
|
|
2271
|
+
keyframes: Record<string, number[]>;
|
|
2272
|
+
fallbackMs: number;
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
export const MOTION_ROLE_DEFAULTS: Record<MotionRole, RoleConfig> = {
|
|
2276
|
+
flip: {
|
|
2277
|
+
durationToken: '--dur-flip',
|
|
2278
|
+
easingToken: '--ease-out',
|
|
2279
|
+
keyframes: { y: [0, -12, 0] },
|
|
2280
|
+
fallbackMs: 220,
|
|
2281
|
+
},
|
|
2282
|
+
panel: {
|
|
2283
|
+
durationToken: '--dur-panel',
|
|
2284
|
+
easingToken: '--ease-in-out',
|
|
2285
|
+
keyframes: { x: [-80, 0, -80] },
|
|
2286
|
+
fallbackMs: 320,
|
|
2287
|
+
},
|
|
2288
|
+
route: {
|
|
2289
|
+
durationToken: '--dur-route',
|
|
2290
|
+
easingToken: '--ease-out',
|
|
2291
|
+
keyframes: { opacity: [0, 1, 0], scale: [0.92, 1, 0.92] },
|
|
2292
|
+
fallbackMs: 480,
|
|
2293
|
+
},
|
|
2294
|
+
soft: {
|
|
2295
|
+
durationToken: '--dur-soft',
|
|
2296
|
+
easingToken: '--ease-out',
|
|
2297
|
+
keyframes: { opacity: [0, 1, 0] },
|
|
2298
|
+
fallbackMs: 160,
|
|
2299
|
+
},
|
|
2300
|
+
spring: {
|
|
2301
|
+
durationToken: '--dur-panel',
|
|
2302
|
+
easingToken: 'spring',
|
|
2303
|
+
keyframes: { y: [0, -16, 0] },
|
|
2304
|
+
fallbackMs: 320,
|
|
2305
|
+
},
|
|
2306
|
+
scroll: {
|
|
2307
|
+
durationToken: '--dur-route',
|
|
2308
|
+
easingToken: '--ease-in-out',
|
|
2309
|
+
keyframes: { x: [0, 24, 0] },
|
|
2310
|
+
fallbackMs: 480,
|
|
2311
|
+
},
|
|
2312
|
+
drag: {
|
|
2313
|
+
durationToken: '--dur-flip',
|
|
2314
|
+
easingToken: '--ease-out',
|
|
2315
|
+
keyframes: { rotate: [0, 4, 0] },
|
|
2316
|
+
fallbackMs: 220,
|
|
2317
|
+
},
|
|
2318
|
+
presence: {
|
|
2319
|
+
durationToken: '--dur-soft',
|
|
2320
|
+
easingToken: '--ease-out',
|
|
2321
|
+
keyframes: { opacity: [0, 1], scale: [0.9, 1] },
|
|
2322
|
+
fallbackMs: 160,
|
|
2323
|
+
},
|
|
2324
|
+
};
|
|
2325
|
+
|
|
2326
|
+
/**
|
|
2327
|
+
* Reads --dur-* + --ease-* CSS custom properties from documentElement and
|
|
2328
|
+
* returns a plain map suitable for plugging into motion/react's transition
|
|
2329
|
+
* config. ms values parsed to numbers; easing tokens returned as strings (the
|
|
2330
|
+
* raw token value, e.g. "cubic-bezier(0, 0, 0.2, 1)" — motion/react accepts
|
|
2331
|
+
* the string form).
|
|
2332
|
+
*/
|
|
2333
|
+
export function useMotionTokens(): {
|
|
2334
|
+
durations: Record<string, number>;
|
|
2335
|
+
easings: Record<string, string>;
|
|
2336
|
+
} {
|
|
2337
|
+
const [snap, setSnap] = useState(() => readMotionTokensOnce());
|
|
2338
|
+
useEffect(() => {
|
|
2339
|
+
setSnap(readMotionTokensOnce());
|
|
2340
|
+
if (typeof MutationObserver === 'undefined') return;
|
|
2341
|
+
const obs = new MutationObserver(() => setSnap(readMotionTokensOnce()));
|
|
2342
|
+
obs.observe(document.documentElement, {
|
|
2343
|
+
attributes: true,
|
|
2344
|
+
attributeFilter: ['data-theme', 'data-reduced-motion'],
|
|
2345
|
+
});
|
|
2346
|
+
return () => obs.disconnect();
|
|
2347
|
+
}, []);
|
|
2348
|
+
return snap;
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
function readMotionTokensOnce(): {
|
|
2352
|
+
durations: Record<string, number>;
|
|
2353
|
+
easings: Record<string, string>;
|
|
2354
|
+
} {
|
|
2355
|
+
if (typeof window === 'undefined' || typeof getComputedStyle === 'undefined') {
|
|
2356
|
+
return { durations: {}, easings: {} };
|
|
2357
|
+
}
|
|
2358
|
+
const cs = getComputedStyle(document.documentElement);
|
|
2359
|
+
const durations: Record<string, number> = {};
|
|
2360
|
+
const easings: Record<string, string> = {};
|
|
2361
|
+
const durKeys = ['--dur-flip', '--dur-panel', '--dur-route', '--dur-soft'];
|
|
2362
|
+
const easeKeys = ['--ease-out', '--ease-in', '--ease-in-out'];
|
|
2363
|
+
for (const k of durKeys) {
|
|
2364
|
+
const raw = cs.getPropertyValue(k).trim();
|
|
2365
|
+
if (!raw) continue;
|
|
2366
|
+
const n = Number.parseFloat(raw);
|
|
2367
|
+
if (Number.isFinite(n)) {
|
|
2368
|
+
durations[k] = raw.endsWith('s') && !raw.endsWith('ms') ? n * 1000 : n;
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
for (const k of easeKeys) {
|
|
2372
|
+
const raw = cs.getPropertyValue(k).trim();
|
|
2373
|
+
if (raw) easings[k] = raw;
|
|
2374
|
+
}
|
|
2375
|
+
return { durations, easings };
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
/**
|
|
2379
|
+
* Maps a DS easing token name to the value motion/react's `transition.ease`
|
|
2380
|
+
* accepts. Returns the live CSS string when readable, otherwise a sane default
|
|
2381
|
+
* matching Material's "standard" curve.
|
|
2382
|
+
*/
|
|
2383
|
+
export function easingFromToken(
|
|
2384
|
+
token: string,
|
|
2385
|
+
easings: Record<string, string>
|
|
2386
|
+
): string | undefined {
|
|
2387
|
+
const live = easings[token];
|
|
2388
|
+
if (live) return live;
|
|
2389
|
+
if (token === '--ease-out') return 'cubic-bezier(0, 0, 0.2, 1)';
|
|
2390
|
+
if (token === '--ease-in') return 'cubic-bezier(0.4, 0, 1, 1)';
|
|
2391
|
+
if (token === '--ease-in-out') return 'cubic-bezier(0.4, 0, 0.2, 1)';
|
|
2392
|
+
return undefined;
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
interface MotionDemoProps {
|
|
2396
|
+
role: MotionRole;
|
|
2397
|
+
loop?: MotionLoop;
|
|
2398
|
+
children?: ReactNode;
|
|
2399
|
+
small?: boolean;
|
|
2400
|
+
className?: string;
|
|
2401
|
+
label?: string;
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
/**
|
|
2405
|
+
* The foundational motion building block. Wraps motion/react's animated <div>
|
|
2406
|
+
* with token-bound duration + easing + reduced-motion short-circuit.
|
|
2407
|
+
*
|
|
2408
|
+
* Default loop="always" so initial paint shows motion — the "looks dead at
|
|
2409
|
+
* rest" failure mode is the regression Phase 3.7 exists to prevent.
|
|
2410
|
+
*/
|
|
2411
|
+
export function MotionDemo({
|
|
2412
|
+
role,
|
|
2413
|
+
loop = 'always',
|
|
2414
|
+
children,
|
|
2415
|
+
small = false,
|
|
2416
|
+
className,
|
|
2417
|
+
label,
|
|
2418
|
+
}: MotionDemoProps) {
|
|
2419
|
+
const cfg = MOTION_ROLE_DEFAULTS[role];
|
|
2420
|
+
const tokens = useMotionTokens();
|
|
2421
|
+
const reduced = _useReducedMotion();
|
|
2422
|
+
const durationMs = tokens.durations[cfg.durationToken] ?? cfg.fallbackMs;
|
|
2423
|
+
const isSpring = cfg.easingToken === 'spring';
|
|
2424
|
+
const ease = isSpring ? undefined : easingFromToken(cfg.easingToken, tokens.easings);
|
|
2425
|
+
const repeat = reduced || loop === 'once' ? 0 : Number.POSITIVE_INFINITY;
|
|
2426
|
+
const repeatType: 'reverse' | 'loop' = loop === 'always' ? 'reverse' : 'loop';
|
|
2427
|
+
const animate = reduced ? undefined : cfg.keyframes;
|
|
2428
|
+
|
|
2429
|
+
return (
|
|
2430
|
+
<div
|
|
2431
|
+
className={`motion-demo${className ? ` ${className}` : ''}`}
|
|
2432
|
+
data-role={role}
|
|
2433
|
+
data-small={small ? 'true' : undefined}
|
|
2434
|
+
style={{ overflow: 'hidden', position: 'relative' }}
|
|
2435
|
+
>
|
|
2436
|
+
<_motionImpl.div
|
|
2437
|
+
animate={animate}
|
|
2438
|
+
transition={{
|
|
2439
|
+
duration: durationMs / 1000,
|
|
2440
|
+
ease,
|
|
2441
|
+
type: isSpring ? 'spring' : 'tween',
|
|
2442
|
+
repeat,
|
|
2443
|
+
repeatType,
|
|
2444
|
+
}}
|
|
2445
|
+
className="motion-demo__target"
|
|
2446
|
+
aria-label={label}
|
|
2447
|
+
style={small ? { width: 32, height: 32 } : undefined}
|
|
2448
|
+
>
|
|
2449
|
+
{children ?? <div className="motion-demo__chip" />}
|
|
2450
|
+
</_motionImpl.div>
|
|
2451
|
+
</div>
|
|
2452
|
+
);
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2455
|
+
interface MotionTrackProps {
|
|
2456
|
+
children: ReactNode;
|
|
2457
|
+
staggerMs?: number;
|
|
2458
|
+
className?: string;
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
/**
|
|
2462
|
+
* Row container with CSS animation-delay stagger between children.
|
|
2463
|
+
*/
|
|
2464
|
+
export function MotionTrack({ children, staggerMs = 40, className }: MotionTrackProps) {
|
|
2465
|
+
const items = Array.isArray(children) ? children : [children];
|
|
2466
|
+
return (
|
|
2467
|
+
<div
|
|
2468
|
+
className={`motion-track${className ? ` ${className}` : ''}`}
|
|
2469
|
+
style={{ display: 'flex', gap: 12, alignItems: 'center' }}
|
|
2470
|
+
>
|
|
2471
|
+
{items.map((c, i) => (
|
|
2472
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: stagger row is index-positional by design; no reorder/insertion semantics
|
|
2473
|
+
<div key={i} style={{ animationDelay: `${i * staggerMs}ms` }}>
|
|
2474
|
+
{c}
|
|
2475
|
+
</div>
|
|
2476
|
+
))}
|
|
2477
|
+
</div>
|
|
2478
|
+
);
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
interface TokenPlaybackProps {
|
|
2482
|
+
duration: string;
|
|
2483
|
+
easing?: string;
|
|
2484
|
+
label?: string;
|
|
2485
|
+
keyframes?: Record<string, number[]>;
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
/**
|
|
2489
|
+
* Click-to-fire single-shot replay chip. Used in the motion specimen so
|
|
2490
|
+
* reviewers can probe a single token without hovering a card.
|
|
2491
|
+
*/
|
|
2492
|
+
export function TokenPlayback({
|
|
2493
|
+
duration,
|
|
2494
|
+
easing = '--ease-out',
|
|
2495
|
+
label,
|
|
2496
|
+
keyframes = { y: [0, -8, 0] },
|
|
2497
|
+
}: TokenPlaybackProps) {
|
|
2498
|
+
const tokens = useMotionTokens();
|
|
2499
|
+
const reduced = _useReducedMotion();
|
|
2500
|
+
const durationMs = tokens.durations[duration] ?? 220;
|
|
2501
|
+
const ease = easing === 'spring' ? undefined : easingFromToken(easing, tokens.easings);
|
|
2502
|
+
const [tick, setTick] = useState(0);
|
|
2503
|
+
const fire = useCallback(() => {
|
|
2504
|
+
if (!reduced) setTick((n) => n + 1);
|
|
2505
|
+
}, [reduced]);
|
|
2506
|
+
return (
|
|
2507
|
+
<button
|
|
2508
|
+
type="button"
|
|
2509
|
+
className="token-playback"
|
|
2510
|
+
onClick={fire}
|
|
2511
|
+
style={{
|
|
2512
|
+
display: 'inline-flex',
|
|
2513
|
+
alignItems: 'center',
|
|
2514
|
+
gap: 8,
|
|
2515
|
+
fontFamily: 'var(--font-mono, ui-monospace, monospace)',
|
|
2516
|
+
fontSize: 12,
|
|
2517
|
+
padding: '6px 10px',
|
|
2518
|
+
border: '1px solid var(--border-1, currentColor)',
|
|
2519
|
+
borderRadius: 4,
|
|
2520
|
+
background: 'transparent',
|
|
2521
|
+
color: 'inherit',
|
|
2522
|
+
cursor: 'pointer',
|
|
2523
|
+
}}
|
|
2524
|
+
>
|
|
2525
|
+
<span style={{ opacity: 0.6 }}>{label ?? duration}</span>
|
|
2526
|
+
<_motionImpl.span
|
|
2527
|
+
key={tick}
|
|
2528
|
+
animate={tick === 0 ? undefined : keyframes}
|
|
2529
|
+
transition={{
|
|
2530
|
+
duration: durationMs / 1000,
|
|
2531
|
+
ease,
|
|
2532
|
+
type: easing === 'spring' ? 'spring' : 'tween',
|
|
2533
|
+
}}
|
|
2534
|
+
style={{
|
|
2535
|
+
display: 'inline-block',
|
|
2536
|
+
width: 8,
|
|
2537
|
+
height: 8,
|
|
2538
|
+
background: 'var(--accent, currentColor)',
|
|
2539
|
+
borderRadius: '50%',
|
|
2540
|
+
}}
|
|
2541
|
+
/>
|
|
2542
|
+
<span style={{ fontVariantNumeric: 'tabular-nums' }}>{durationMs}ms</span>
|
|
2543
|
+
</button>
|
|
2544
|
+
);
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
/**
|
|
2548
|
+
* Chrome toggle for the motion specimen — flips data-reduced-motion="true"
|
|
2549
|
+
* on <html> so reviewers can eyeball both branches without OS settings.
|
|
2550
|
+
* Inspection aid, never a replacement for prefers-reduced-motion.
|
|
2551
|
+
*/
|
|
2552
|
+
export function ReducedMotionToggle() {
|
|
2553
|
+
const [on, setOn] = useState(false);
|
|
2554
|
+
useEffect(() => {
|
|
2555
|
+
const el = document.documentElement;
|
|
2556
|
+
const initial = el.getAttribute('data-reduced-motion') === 'true';
|
|
2557
|
+
setOn(initial);
|
|
2558
|
+
}, []);
|
|
2559
|
+
const toggle = useCallback(() => {
|
|
2560
|
+
const el = document.documentElement;
|
|
2561
|
+
const next = !on;
|
|
2562
|
+
if (next) el.setAttribute('data-reduced-motion', 'true');
|
|
2563
|
+
else el.removeAttribute('data-reduced-motion');
|
|
2564
|
+
setOn(next);
|
|
2565
|
+
}, [on]);
|
|
2566
|
+
return (
|
|
2567
|
+
<button
|
|
2568
|
+
type="button"
|
|
2569
|
+
className="reduced-motion-toggle"
|
|
2570
|
+
onClick={toggle}
|
|
2571
|
+
aria-pressed={on}
|
|
2572
|
+
style={{
|
|
2573
|
+
fontFamily: 'var(--font-mono, ui-monospace, monospace)',
|
|
2574
|
+
fontSize: 11,
|
|
2575
|
+
padding: '4px 8px',
|
|
2576
|
+
border: '1px solid var(--border-1, currentColor)',
|
|
2577
|
+
borderRadius: 3,
|
|
2578
|
+
background: on ? 'var(--accent, currentColor)' : 'transparent',
|
|
2579
|
+
color: on ? 'var(--bg-0, white)' : 'inherit',
|
|
2580
|
+
cursor: 'pointer',
|
|
2581
|
+
}}
|
|
2582
|
+
>
|
|
2583
|
+
reduced-motion: {on ? 'on' : 'off'}
|
|
2584
|
+
</button>
|
|
2585
|
+
);
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2588
|
+
export {
|
|
2589
|
+
_motionImpl as motion,
|
|
2590
|
+
_useReducedMotion as useReducedMotion,
|
|
2591
|
+
_MotionAnimatePresence as AnimatePresence,
|
|
2592
|
+
};
|