proto-plugin 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/AGENTS.md +350 -0
- package/README.md +279 -0
- package/components.json +18 -0
- package/package.json +73 -0
- package/scripts/download-mobbin-reference-images.mjs +112 -0
- package/scripts/lib/host-config.example.sh +48 -0
- package/scripts/lib/host-config.sh +25 -0
- package/scripts/lib/resolve-host-root.mjs +18 -0
- package/scripts/lib/source-path.sh +49 -0
- package/scripts/link-source-db.sh +128 -0
- package/scripts/link-source.sh +40 -0
- package/scripts/publish-package.sh +147 -0
- package/scripts/share-prototype.sh +279 -0
- package/scripts/sync-from-source.sh +74 -0
- package/scripts/verify-prototype-component-ids.mjs +151 -0
- package/scripts/verify-prototype-preview-states.mjs +135 -0
- package/src/app.ts +4 -0
- package/src/components/platform-ui/code-block.tsx +21 -0
- package/src/components/platform-ui/controls-panel-options.tsx +71 -0
- package/src/components/platform-ui/controls-panel-select.tsx +438 -0
- package/src/components/platform-ui/empty-state.tsx +15 -0
- package/src/components/platform-ui/icon-button.tsx +18 -0
- package/src/components/platform-ui/panel-select.tsx +123 -0
- package/src/components/platform-ui/panel.tsx +97 -0
- package/src/components/platform-ui/review-dropdown-menu.tsx +35 -0
- package/src/components/platform-ui/sidebar.tsx +293 -0
- package/src/components/platform-ui/toolbar-icon-button.tsx +34 -0
- package/src/components/platform-ui/toolbar.tsx +37 -0
- package/src/components/prototype-gallery-client.tsx +57 -0
- package/src/components/prototype-provider.tsx +20 -0
- package/src/components/prototypes/mini-pill-label.tsx +37 -0
- package/src/components/prototypes/prototype-change-log-panel.tsx +207 -0
- package/src/components/prototypes/prototype-comment-provider.tsx +61 -0
- package/src/components/prototypes/prototype-comment-toolbar.tsx +65 -0
- package/src/components/prototypes/prototype-control.tsx +31 -0
- package/src/components/prototypes/prototype-controls.tsx +553 -0
- package/src/components/prototypes/prototype-create-exploration-modal.tsx +117 -0
- package/src/components/prototypes/prototype-create-state-modal.tsx +117 -0
- package/src/components/prototypes/prototype-default-state-map-page.tsx +66 -0
- package/src/components/prototypes/prototype-design-brief-modal.tsx +128 -0
- package/src/components/prototypes/prototype-design-brief-panel.tsx +525 -0
- package/src/components/prototypes/prototype-design-context-panel.tsx +65 -0
- package/src/components/prototypes/prototype-floating-pill.module.scss +1341 -0
- package/src/components/prototypes/prototype-mobbin-gallery.tsx +330 -0
- package/src/components/prototypes/prototype-reference-docs.tsx +222 -0
- package/src/components/prototypes/prototype-review-chrome.module.scss +630 -0
- package/src/components/prototypes/prototype-review-chrome.tsx +1241 -0
- package/src/components/prototypes/prototype-review-dropdown-menu-item.tsx +1 -0
- package/src/components/prototypes/prototype-review-dropdown-menu-separator.tsx +1 -0
- package/src/components/prototypes/prototype-review-panel-menu-item.tsx +1 -0
- package/src/components/prototypes/prototype-review-panel-menu-separator.tsx +1 -0
- package/src/components/prototypes/prototype-review-panel-select.tsx +4 -0
- package/src/components/prototypes/prototype-review-sidebar.tsx +167 -0
- package/src/components/prototypes/prototype-share-link-restore.tsx +75 -0
- package/src/components/prototypes/prototype-share-mode.tsx +206 -0
- package/src/components/prototypes/prototype-spec-panel-content.tsx +538 -0
- package/src/components/prototypes/prototype-state-canvas-overlay.module.scss +336 -0
- package/src/components/prototypes/prototype-state-canvas-overlay.tsx +1023 -0
- package/src/components/prototypes/prototype-state-canvas.tsx +38 -0
- package/src/components/prototypes/prototype-state-screenshot-preview.tsx +47 -0
- package/src/components/prototypes/prototype-target.tsx +120 -0
- package/src/components/prototypes/prototype-tweak-highlight.tsx +132 -0
- package/src/components/prototypes/prototype-tweaks.tsx +48 -0
- package/src/components/prototypes/prototype-variant-explorer.tsx +347 -0
- package/src/components/prototypes/prototype-variant-rationale-panel.tsx +49 -0
- package/src/components/prototypes/prototype-variant-tabs.tsx +207 -0
- package/src/components/prototypes/prototype-vercel-preview-modal.tsx +120 -0
- package/src/components/prototypes/state-map-annotation-tooltip.module.scss +89 -0
- package/src/components/prototypes/state-map-annotation-tooltip.tsx +137 -0
- package/src/components/prototypes/state-map-wireframe-highlight.module.scss +25 -0
- package/src/components/prototypes/state-map-wireframe-highlight.tsx +82 -0
- package/src/components/shell/prototype-gallery-shell.tsx +79 -0
- package/src/components/shell/prototype-screenshot-capture.tsx +45 -0
- package/src/components/shell/prototype-shell.module.scss +194 -0
- package/src/components/shell/prototype-shell.tsx +160 -0
- package/src/components/shell/prototype-state-screenshot-capture.tsx +68 -0
- package/src/components/ui/button.tsx +58 -0
- package/src/components/ui/dialog.module.scss +81 -0
- package/src/components/ui/dialog.tsx +127 -0
- package/src/components/ui/dropdown-menu.tsx +200 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/label.tsx +26 -0
- package/src/components/ui/option-select.tsx +81 -0
- package/src/components/ui/scroll-area.tsx +48 -0
- package/src/components/ui/separator.tsx +31 -0
- package/src/components/ui/slider.tsx +28 -0
- package/src/components/ui/switch.tsx +29 -0
- package/src/components/ui/tabs.tsx +55 -0
- package/src/components/ui/toggle-group.tsx +80 -0
- package/src/components/ui/tooltip.tsx +32 -0
- package/src/config/create-prototype-gallery-page.tsx +65 -0
- package/src/config/create-prototype-host-app.tsx +121 -0
- package/src/config/create-prototype-page.tsx +41 -0
- package/src/config/create-prototype-site-layout.tsx +28 -0
- package/src/config/define-prototype-config.ts +7 -0
- package/src/config/prototype-icon.tsx +25 -0
- package/src/config/prototype-site-layout-client.tsx +35 -0
- package/src/config/with-prototype.ts +20 -0
- package/src/index.ts +303 -0
- package/src/lib/pr-split/build-pr-agent-prompt.ts +68 -0
- package/src/lib/pr-split/build-pr-split-prototype-url.ts +12 -0
- package/src/lib/pr-split/pr-split-highlight.ts +6 -0
- package/src/lib/pr-split/pr-split-types.ts +58 -0
- package/src/lib/pr-split/resolve-source-preview-url.ts +45 -0
- package/src/lib/pr-split/use-pr-vercel-previews.ts +69 -0
- package/src/lib/prototype-comments/core/annotation-channel.ts +41 -0
- package/src/lib/prototype-comments/core/annotation-status.ts +60 -0
- package/src/lib/prototype-comments/core/annotation-target.ts +567 -0
- package/src/lib/prototype-comments/core/capture-theme.ts +41 -0
- package/src/lib/prototype-comments/core/capture.ts +11 -0
- package/src/lib/prototype-comments/core/comment-capture-blocked.ts +47 -0
- package/src/lib/prototype-comments/core/comment-threads.ts +52 -0
- package/src/lib/prototype-comments/core/element-identification.ts +615 -0
- package/src/lib/prototype-comments/core/export.ts +35 -0
- package/src/lib/prototype-comments/core/freeze-animations.ts +266 -0
- package/src/lib/prototype-comments/core/identify-element.ts +76 -0
- package/src/lib/prototype-comments/core/normalize-viewport.ts +35 -0
- package/src/lib/prototype-comments/core/react-detection.ts +704 -0
- package/src/lib/prototype-comments/core/restore.ts +7 -0
- package/src/lib/prototype-comments/core/screenshot.ts +428 -0
- package/src/lib/prototype-comments/core/source-location.ts +906 -0
- package/src/lib/prototype-comments/core/storage.ts +80 -0
- package/src/lib/prototype-comments/core/types.ts +79 -0
- package/src/lib/prototype-comments/core/validation.ts +17 -0
- package/src/lib/prototype-comments/hooks/useLayoutShift.ts +61 -0
- package/src/lib/prototype-comments/index.ts +58 -0
- package/src/lib/prototype-comments/react/CommentCaptureToolbar.tsx +1398 -0
- package/src/lib/prototype-comments/react/CommentProvider.tsx +298 -0
- package/src/lib/prototype-comments/scss.d.ts +10 -0
- package/src/lib/prototype-comments/ui/CommentAnnotationOverlay.tsx +253 -0
- package/src/lib/prototype-comments/ui/CommentDetailPage.tsx +56 -0
- package/src/lib/prototype-comments/ui/CommentHeaderCount.tsx +28 -0
- package/src/lib/prototype-comments/ui/CommentThread.tsx +331 -0
- package/src/lib/prototype-comments/ui/CommentsGrid.tsx +780 -0
- package/src/lib/prototype-comments/ui/CommentsListPage.tsx +25 -0
- package/src/lib/prototype-comments/ui/CommentsSidebar.tsx +77 -0
- package/src/lib/prototype-comments/ui/annotation-marker/index.tsx +200 -0
- package/src/lib/prototype-comments/ui/annotation-marker/styles.module.scss +194 -0
- package/src/lib/prototype-comments/ui/annotation-popup/index.tsx +335 -0
- package/src/lib/prototype-comments/ui/annotation-popup/styles.module.scss +469 -0
- package/src/lib/prototype-comments/ui/capture-styles.module.scss +2225 -0
- package/src/lib/prototype-comments/ui/comment-highlight.ts +48 -0
- package/src/lib/prototype-comments/ui/comment-thread.module.scss +203 -0
- package/src/lib/prototype-comments/ui/comments-grid-card.module.scss +253 -0
- package/src/lib/prototype-comments/ui/comments-sidebar.module.scss +123 -0
- package/src/lib/prototype-comments/ui/icon-transitions.module.scss +51 -0
- package/src/lib/prototype-comments/ui/icons.tsx +1086 -0
- package/src/lib/prototype-comments/ui/is-comment-detail-path.ts +7 -0
- package/src/lib/prototypes/capture-prototype-viewport.ts +32 -0
- package/src/lib/prototypes/changelog-meta-context.tsx +162 -0
- package/src/lib/prototypes/changelog-meta-storage.ts +62 -0
- package/src/lib/prototypes/changelog-meta.ts +95 -0
- package/src/lib/prototypes/create-prototype-registry.ts +74 -0
- package/src/lib/prototypes/create-state-screenshot-thumbnail.ts +46 -0
- package/src/lib/prototypes/design-exploration-default-storage.ts +68 -0
- package/src/lib/prototypes/design-exploration-default.ts +51 -0
- package/src/lib/prototypes/design-exploration-types.ts +203 -0
- package/src/lib/prototypes/prototype-comment-registry.tsx +145 -0
- package/src/lib/prototypes/prototype-comment-review-bridge.tsx +40 -0
- package/src/lib/prototypes/prototype-comment-review-state.ts +57 -0
- package/src/lib/prototypes/prototype-component-registry.ts +40 -0
- package/src/lib/prototypes/prototype-config-types.ts +34 -0
- package/src/lib/prototypes/prototype-preview-state-registry.ts +443 -0
- package/src/lib/prototypes/prototype-preview-state-types.ts +89 -0
- package/src/lib/prototypes/prototype-review-context.tsx +905 -0
- package/src/lib/prototypes/prototype-review-url.ts +134 -0
- package/src/lib/prototypes/prototype-share-link.ts +175 -0
- package/src/lib/prototypes/prototype-state-canvas-constants.ts +118 -0
- package/src/lib/prototypes/prototype-state-canvas-types.ts +159 -0
- package/src/lib/prototypes/prototype-state-screenshot.ts +39 -0
- package/src/lib/prototypes/prototype-tweak-registry.ts +96 -0
- package/src/lib/prototypes/prototype-tweak-types.ts +31 -0
- package/src/lib/prototypes/reference-docs.ts +137 -0
- package/src/lib/prototypes/screenshot-manifest.ts +33 -0
- package/src/lib/prototypes/screenshot-src.ts +7 -0
- package/src/lib/prototypes/share-command.ts +8 -0
- package/src/lib/prototypes/state-map-annotation.ts +70 -0
- package/src/lib/prototypes/use-design-exploration-default.ts +114 -0
- package/src/lib/prototypes/use-persisted-local-state.ts +140 -0
- package/src/lib/prototypes/use-prototype-comments.ts +63 -0
- package/src/lib/prototypes/use-prototype-mobile-viewport-frame.ts +46 -0
- package/src/lib/prototypes/use-prototype-state-screenshot-src.ts +71 -0
- package/src/lib/prototypes/use-prototype-tool-theme.tsx +115 -0
- package/src/lib/prototypes/use-prototype-viewport-frame.ts +34 -0
- package/src/lib/prototypes/use-register-prototype-reference-docs.ts +16 -0
- package/src/lib/prototypes/use-snap-corner-position.ts +417 -0
- package/src/lib/prototypes/use-toolbar-hover-panel.ts +98 -0
- package/src/lib/prototypes/variant-set-lucide-icon.tsx +55 -0
- package/src/lib/tool-portal.ts +33 -0
- package/src/lib/use-copy-to-clipboard.ts +81 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/vercel-preview/parse-vercel-github-comment.ts +81 -0
- package/src/server/create-host-api-route.ts +100 -0
- package/src/server/create-prototype-api-routes.ts +428 -0
- package/src/server/gallery/create-gallery-api.ts +56 -0
- package/src/server/gallery/folders-route.ts +156 -0
- package/src/server/gallery/gallery-env.ts +39 -0
- package/src/server/gallery/gallery-route.ts +150 -0
- package/src/server/gallery/gallery-types.ts +15 -0
- package/src/server/gallery/image-route.ts +59 -0
- package/src/server/gallery/item-route.ts +92 -0
- package/src/server/pr-vercel-preview-route.ts +162 -0
- package/src/server/redis/client.ts +55 -0
- package/src/server/redis/prototype-changelog-meta.ts +31 -0
- package/src/server/redis/prototype-comments.ts +92 -0
- package/src/server/redis/prototype-design-exploration-default.ts +32 -0
- package/src/server.ts +51 -0
- package/src/styles/globals.css +624 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
PROTOTYPE_VIEWPORT_ID,
|
|
5
|
+
} from "@prototype/lib/tool-portal";
|
|
6
|
+
import { useEffect, useState } from "react";
|
|
7
|
+
|
|
8
|
+
function readPrototypeMobileViewportFrame(): boolean {
|
|
9
|
+
if (typeof document === "undefined") return false;
|
|
10
|
+
|
|
11
|
+
const viewport = document.getElementById(PROTOTYPE_VIEWPORT_ID);
|
|
12
|
+
return (
|
|
13
|
+
viewport?.getAttribute("data-prototype-viewport-layout") === "mobile"
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** True when the prototype is rendered inside the platform mobile device frame. */
|
|
18
|
+
export function usePrototypeMobileViewportFrame(): boolean {
|
|
19
|
+
// Initialize to `false` (the SSR value) so the first client render matches
|
|
20
|
+
// the server and avoids a hydration mismatch; the effect syncs the real value.
|
|
21
|
+
const [isActive, setIsActive] = useState(false);
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
const viewport = document.getElementById(PROTOTYPE_VIEWPORT_ID);
|
|
25
|
+
if (!viewport) {
|
|
26
|
+
setIsActive(false);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const update = () => {
|
|
31
|
+
setIsActive(readPrototypeMobileViewportFrame());
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
update();
|
|
35
|
+
|
|
36
|
+
const observer = new MutationObserver(update);
|
|
37
|
+
observer.observe(viewport, {
|
|
38
|
+
attributes: true,
|
|
39
|
+
attributeFilter: ["data-prototype-viewport-layout", "class"],
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return () => observer.disconnect();
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
return isActive;
|
|
46
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useState } from "react";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
PROTOTYPE_STATE_SCREENSHOT_CAPTURED_EVENT,
|
|
7
|
+
type PrototypeStateScreenshotCapturedDetail,
|
|
8
|
+
stateScreenshotManifestKey,
|
|
9
|
+
stateScreenshotSrc,
|
|
10
|
+
} from "./prototype-state-screenshot";
|
|
11
|
+
|
|
12
|
+
const MANIFEST_PATH = "/prototypes/screenshots/manifest.json";
|
|
13
|
+
|
|
14
|
+
type ScreenshotManifest = Record<string, number>;
|
|
15
|
+
|
|
16
|
+
async function readClientScreenshotManifest(): Promise<ScreenshotManifest> {
|
|
17
|
+
try {
|
|
18
|
+
const response = await fetch(MANIFEST_PATH, { cache: "no-store" });
|
|
19
|
+
if (!response.ok) return {};
|
|
20
|
+
return (await response.json()) as ScreenshotManifest;
|
|
21
|
+
} catch {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function usePrototypeStateScreenshotSrc(
|
|
27
|
+
slug: string,
|
|
28
|
+
stateId: string,
|
|
29
|
+
): string | null {
|
|
30
|
+
const manifestKey = stateScreenshotManifestKey(slug, stateId);
|
|
31
|
+
const [version, setVersion] = useState<number | undefined>(undefined);
|
|
32
|
+
|
|
33
|
+
const syncVersion = useCallback(
|
|
34
|
+
(manifest: ScreenshotManifest) => {
|
|
35
|
+
const nextVersion = manifest[manifestKey];
|
|
36
|
+
setVersion(nextVersion);
|
|
37
|
+
},
|
|
38
|
+
[manifestKey],
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
let cancelled = false;
|
|
43
|
+
|
|
44
|
+
void readClientScreenshotManifest().then((manifest) => {
|
|
45
|
+
if (!cancelled) syncVersion(manifest);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const handleCaptured = (event: Event) => {
|
|
49
|
+
const detail = (event as CustomEvent<PrototypeStateScreenshotCapturedDetail>)
|
|
50
|
+
.detail;
|
|
51
|
+
if (detail.slug !== slug || detail.stateId !== stateId) return;
|
|
52
|
+
setVersion(detail.version);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
window.addEventListener(
|
|
56
|
+
PROTOTYPE_STATE_SCREENSHOT_CAPTURED_EVENT,
|
|
57
|
+
handleCaptured,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
return () => {
|
|
61
|
+
cancelled = true;
|
|
62
|
+
window.removeEventListener(
|
|
63
|
+
PROTOTYPE_STATE_SCREENSHOT_CAPTURED_EVENT,
|
|
64
|
+
handleCaptured,
|
|
65
|
+
);
|
|
66
|
+
};
|
|
67
|
+
}, [slug, stateId, syncVersion]);
|
|
68
|
+
|
|
69
|
+
if (version == null) return null;
|
|
70
|
+
return stateScreenshotSrc(slug, stateId, version);
|
|
71
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { TooltipProvider } from "@prototype/components/ui/tooltip";
|
|
4
|
+
import { PROTOTYPE_ROOT_ID } from "@prototype/lib/tool-portal";
|
|
5
|
+
import { cn } from "@prototype/lib/utils";
|
|
6
|
+
import {
|
|
7
|
+
createContext,
|
|
8
|
+
useCallback,
|
|
9
|
+
useContext,
|
|
10
|
+
useMemo,
|
|
11
|
+
type ReactNode,
|
|
12
|
+
} from "react";
|
|
13
|
+
|
|
14
|
+
import { usePersistedLocalString } from "./use-persisted-local-state";
|
|
15
|
+
|
|
16
|
+
export const PROTOTYPE_TOOL_THEME_STORAGE_KEY = "prototype-tool-theme";
|
|
17
|
+
|
|
18
|
+
export type PrototypeToolTheme = "light" | "dark";
|
|
19
|
+
|
|
20
|
+
type PrototypeToolThemeContextValue = {
|
|
21
|
+
theme: PrototypeToolTheme;
|
|
22
|
+
useLightTheme: boolean;
|
|
23
|
+
commentTheme: PrototypeToolTheme;
|
|
24
|
+
setTheme: (theme: PrototypeToolTheme) => void;
|
|
25
|
+
toggleTheme: () => void;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const PrototypeToolThemeContext =
|
|
29
|
+
createContext<PrototypeToolThemeContextValue | null>(null);
|
|
30
|
+
|
|
31
|
+
const DEFAULT_THEME: PrototypeToolTheme = "dark";
|
|
32
|
+
|
|
33
|
+
function normalizeTheme(value: string): PrototypeToolTheme {
|
|
34
|
+
return value === "light" ? "light" : "dark";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function disableThemeTransitions() {
|
|
38
|
+
if (typeof document === "undefined") return;
|
|
39
|
+
|
|
40
|
+
const root = document.documentElement;
|
|
41
|
+
root.classList.add("prototype-theme-transition-disable");
|
|
42
|
+
|
|
43
|
+
window.setTimeout(() => {
|
|
44
|
+
root.classList.remove("prototype-theme-transition-disable");
|
|
45
|
+
}, 0);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type PrototypeToolThemeProviderProps = {
|
|
49
|
+
children: ReactNode;
|
|
50
|
+
className?: string;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export function PrototypeToolThemeProvider({
|
|
54
|
+
children,
|
|
55
|
+
className,
|
|
56
|
+
}: PrototypeToolThemeProviderProps) {
|
|
57
|
+
const { value, updateValue } = usePersistedLocalString(
|
|
58
|
+
PROTOTYPE_TOOL_THEME_STORAGE_KEY,
|
|
59
|
+
DEFAULT_THEME,
|
|
60
|
+
);
|
|
61
|
+
const theme = normalizeTheme(value);
|
|
62
|
+
const useLightTheme = theme === "light";
|
|
63
|
+
|
|
64
|
+
const setTheme = useCallback(
|
|
65
|
+
(next: PrototypeToolTheme) => {
|
|
66
|
+
disableThemeTransitions();
|
|
67
|
+
updateValue(next);
|
|
68
|
+
},
|
|
69
|
+
[updateValue],
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const toggleTheme = useCallback(() => {
|
|
73
|
+
setTheme(useLightTheme ? "dark" : "light");
|
|
74
|
+
}, [setTheme, useLightTheme]);
|
|
75
|
+
|
|
76
|
+
const contextValue = useMemo<PrototypeToolThemeContextValue>(
|
|
77
|
+
() => ({
|
|
78
|
+
theme,
|
|
79
|
+
useLightTheme,
|
|
80
|
+
commentTheme: theme,
|
|
81
|
+
setTheme,
|
|
82
|
+
toggleTheme,
|
|
83
|
+
}),
|
|
84
|
+
[setTheme, theme, toggleTheme, useLightTheme],
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<PrototypeToolThemeContext.Provider value={contextValue}>
|
|
89
|
+
<div
|
|
90
|
+
id={PROTOTYPE_ROOT_ID}
|
|
91
|
+
data-prototype-root
|
|
92
|
+
data-prototype-comment-theme={theme}
|
|
93
|
+
className={cn(className)}
|
|
94
|
+
>
|
|
95
|
+
<TooltipProvider delayDuration={300}>{children}</TooltipProvider>
|
|
96
|
+
</div>
|
|
97
|
+
</PrototypeToolThemeContext.Provider>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function usePrototypeToolTheme(): PrototypeToolThemeContextValue {
|
|
102
|
+
const context = useContext(PrototypeToolThemeContext);
|
|
103
|
+
|
|
104
|
+
if (!context) {
|
|
105
|
+
return {
|
|
106
|
+
theme: DEFAULT_THEME,
|
|
107
|
+
useLightTheme: false,
|
|
108
|
+
commentTheme: DEFAULT_THEME,
|
|
109
|
+
setTheme: () => {},
|
|
110
|
+
toggleTheme: () => {},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return context;
|
|
115
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { PROTOTYPE_VIEWPORT_ID } from "@prototype/lib/tool-portal";
|
|
4
|
+
import { useEffect, useState } from "react";
|
|
5
|
+
|
|
6
|
+
function readPrototypeViewportFrame(): boolean {
|
|
7
|
+
if (typeof document === "undefined") return false;
|
|
8
|
+
return document.getElementById(PROTOTYPE_VIEWPORT_ID) != null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** True when UI is rendered inside the prototype preview viewport (desktop or mobile frame). */
|
|
12
|
+
export function usePrototypeViewportFrame(): boolean {
|
|
13
|
+
// Initialize to `false` (the SSR value) so the first client render matches
|
|
14
|
+
// the server and avoids a hydration mismatch; the effect syncs the real value.
|
|
15
|
+
const [isActive, setIsActive] = useState(false);
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const update = () => setIsActive(readPrototypeViewportFrame());
|
|
19
|
+
update();
|
|
20
|
+
|
|
21
|
+
const viewport = document.getElementById(PROTOTYPE_VIEWPORT_ID);
|
|
22
|
+
if (!viewport) return;
|
|
23
|
+
|
|
24
|
+
const observer = new MutationObserver(update);
|
|
25
|
+
observer.observe(viewport, {
|
|
26
|
+
attributes: true,
|
|
27
|
+
attributeFilter: ["data-prototype-viewport-layout", "class", "id"],
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return () => observer.disconnect();
|
|
31
|
+
}, []);
|
|
32
|
+
|
|
33
|
+
return isActive;
|
|
34
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { PrototypeReferenceDoc } from "@prototype/lib/prototypes/reference-docs";
|
|
4
|
+
import { usePrototypeReviewOptional } from "@prototype/lib/prototypes/prototype-review-context";
|
|
5
|
+
import { useLayoutEffect } from "react";
|
|
6
|
+
|
|
7
|
+
export function useRegisterPrototypeReferenceDocs(
|
|
8
|
+
docs: PrototypeReferenceDoc[],
|
|
9
|
+
configFilePath?: string,
|
|
10
|
+
) {
|
|
11
|
+
const review = usePrototypeReviewOptional();
|
|
12
|
+
|
|
13
|
+
useLayoutEffect(() => {
|
|
14
|
+
review?.registerReferenceDocs(docs, configFilePath);
|
|
15
|
+
}, [review, docs, configFilePath]);
|
|
16
|
+
}
|
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
useCallback,
|
|
5
|
+
useEffect,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
type PointerEvent as ReactPointerEvent,
|
|
9
|
+
} from "react";
|
|
10
|
+
|
|
11
|
+
export type SnapCorner =
|
|
12
|
+
| "top-left"
|
|
13
|
+
| "top-center"
|
|
14
|
+
| "top-right"
|
|
15
|
+
| "left-center"
|
|
16
|
+
| "right-center"
|
|
17
|
+
| "bottom-left"
|
|
18
|
+
| "bottom-center"
|
|
19
|
+
| "bottom-right";
|
|
20
|
+
|
|
21
|
+
const DRAG_THRESHOLD_PX = 10;
|
|
22
|
+
const SNAP_PADDING_PX = 16;
|
|
23
|
+
const CORNER_INSET_PX = 16;
|
|
24
|
+
|
|
25
|
+
const SNAP_CORNERS: readonly SnapCorner[] = [
|
|
26
|
+
"top-left",
|
|
27
|
+
"top-center",
|
|
28
|
+
"top-right",
|
|
29
|
+
"left-center",
|
|
30
|
+
"right-center",
|
|
31
|
+
"bottom-left",
|
|
32
|
+
"bottom-center",
|
|
33
|
+
"bottom-right",
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
type Position = { left: number; top: number };
|
|
37
|
+
|
|
38
|
+
export function isSnapCorner(value: string): value is SnapCorner {
|
|
39
|
+
return (SNAP_CORNERS as readonly string[]).includes(value);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getNearestCorner(
|
|
43
|
+
centerX: number,
|
|
44
|
+
centerY: number,
|
|
45
|
+
containerWidth: number,
|
|
46
|
+
containerHeight: number,
|
|
47
|
+
): SnapCorner {
|
|
48
|
+
const centerAnchorX = containerWidth / 2;
|
|
49
|
+
const centerAnchorY = containerHeight / 2;
|
|
50
|
+
const edgeX = SNAP_PADDING_PX;
|
|
51
|
+
const edgeRightX = containerWidth - SNAP_PADDING_PX;
|
|
52
|
+
const edgeY = SNAP_PADDING_PX;
|
|
53
|
+
const edgeBottomY = containerHeight - SNAP_PADDING_PX;
|
|
54
|
+
|
|
55
|
+
const anchors: { corner: SnapCorner; x: number; y: number }[] = [
|
|
56
|
+
{ corner: "top-left", x: edgeX, y: edgeY },
|
|
57
|
+
{ corner: "top-center", x: centerAnchorX, y: edgeY },
|
|
58
|
+
{ corner: "top-right", x: edgeRightX, y: edgeY },
|
|
59
|
+
{ corner: "left-center", x: edgeX, y: centerAnchorY },
|
|
60
|
+
{ corner: "right-center", x: edgeRightX, y: centerAnchorY },
|
|
61
|
+
{ corner: "bottom-left", x: edgeX, y: edgeBottomY },
|
|
62
|
+
{ corner: "bottom-center", x: centerAnchorX, y: edgeBottomY },
|
|
63
|
+
{ corner: "bottom-right", x: edgeRightX, y: edgeBottomY },
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
let nearest = anchors[0]!;
|
|
67
|
+
let nearestDistance = Infinity;
|
|
68
|
+
|
|
69
|
+
for (const anchor of anchors) {
|
|
70
|
+
const distance = Math.hypot(centerX - anchor.x, centerY - anchor.y);
|
|
71
|
+
if (distance < nearestDistance) {
|
|
72
|
+
nearestDistance = distance;
|
|
73
|
+
nearest = anchor;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return nearest.corner;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function clampPosition(
|
|
81
|
+
position: Position,
|
|
82
|
+
containerWidth: number,
|
|
83
|
+
containerHeight: number,
|
|
84
|
+
elementWidth: number,
|
|
85
|
+
elementHeight: number,
|
|
86
|
+
padding: number,
|
|
87
|
+
): Position {
|
|
88
|
+
const maxLeft = Math.max(padding, containerWidth - elementWidth - padding);
|
|
89
|
+
const maxTop = Math.max(padding, containerHeight - elementHeight - padding);
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
left: Math.min(Math.max(position.left, padding), maxLeft),
|
|
93
|
+
top: Math.min(Math.max(position.top, padding), maxTop),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function getRelativePosition(
|
|
98
|
+
container: Element,
|
|
99
|
+
element: HTMLElement,
|
|
100
|
+
): Position {
|
|
101
|
+
const containerRect = container.getBoundingClientRect();
|
|
102
|
+
const elementRect = element.getBoundingClientRect();
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
left: elementRect.left - containerRect.left,
|
|
106
|
+
top: elementRect.top - containerRect.top,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Pixel position matching prototype-review-chrome corner classes (1rem inset). */
|
|
111
|
+
export function getCornerPosition(
|
|
112
|
+
corner: SnapCorner,
|
|
113
|
+
containerWidth: number,
|
|
114
|
+
containerHeight: number,
|
|
115
|
+
elementWidth: number,
|
|
116
|
+
elementHeight: number,
|
|
117
|
+
inset = CORNER_INSET_PX,
|
|
118
|
+
): Position {
|
|
119
|
+
const maxLeft = containerWidth - elementWidth - inset;
|
|
120
|
+
const maxTop = containerHeight - elementHeight - inset;
|
|
121
|
+
const centerLeft = (containerWidth - elementWidth) / 2;
|
|
122
|
+
const centerTop = (containerHeight - elementHeight) / 2;
|
|
123
|
+
|
|
124
|
+
switch (corner) {
|
|
125
|
+
case "top-left":
|
|
126
|
+
return { left: inset, top: inset };
|
|
127
|
+
case "top-center":
|
|
128
|
+
return { left: centerLeft, top: inset };
|
|
129
|
+
case "top-right":
|
|
130
|
+
return { left: maxLeft, top: inset };
|
|
131
|
+
case "left-center":
|
|
132
|
+
return { left: inset, top: centerTop };
|
|
133
|
+
case "right-center":
|
|
134
|
+
return { left: maxLeft, top: centerTop };
|
|
135
|
+
case "bottom-left":
|
|
136
|
+
return { left: inset, top: maxTop };
|
|
137
|
+
case "bottom-center":
|
|
138
|
+
return { left: centerLeft, top: maxTop };
|
|
139
|
+
case "bottom-right":
|
|
140
|
+
return { left: maxLeft, top: maxTop };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
type UseSnapCornerPositionOptions = {
|
|
145
|
+
container: Element | null;
|
|
146
|
+
elementRef: React.RefObject<HTMLElement | null>;
|
|
147
|
+
initialCorner?: SnapCorner;
|
|
148
|
+
persistCorner?: (corner: SnapCorner) => void;
|
|
149
|
+
readPersistedCorner?: () => SnapCorner | null;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export function useSnapCornerPosition({
|
|
153
|
+
container,
|
|
154
|
+
elementRef,
|
|
155
|
+
initialCorner = "bottom-right",
|
|
156
|
+
persistCorner,
|
|
157
|
+
readPersistedCorner,
|
|
158
|
+
}: UseSnapCornerPositionOptions) {
|
|
159
|
+
const [corner, setCorner] = useState<SnapCorner>(() => {
|
|
160
|
+
const persisted = readPersistedCorner?.();
|
|
161
|
+
return persisted && isSnapCorner(persisted) ? persisted : initialCorner;
|
|
162
|
+
});
|
|
163
|
+
const [dragPosition, setDragPosition] = useState<Position | null>(null);
|
|
164
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
165
|
+
const [snapPosition, setSnapPosition] = useState<Position | null>(null);
|
|
166
|
+
const [isSnapping, setIsSnapping] = useState(false);
|
|
167
|
+
const pendingSnapCornerRef = useRef<SnapCorner | null>(null);
|
|
168
|
+
const snapTargetScheduledRef = useRef(false);
|
|
169
|
+
|
|
170
|
+
const dragStateRef = useRef<{
|
|
171
|
+
pointerId: number;
|
|
172
|
+
startClientX: number;
|
|
173
|
+
startClientY: number;
|
|
174
|
+
originLeft: number;
|
|
175
|
+
originTop: number;
|
|
176
|
+
didDrag: boolean;
|
|
177
|
+
} | null>(null);
|
|
178
|
+
const lastDragPositionRef = useRef<Position | null>(null);
|
|
179
|
+
const didDragRef = useRef(false);
|
|
180
|
+
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
const persisted = readPersistedCorner?.();
|
|
183
|
+
if (persisted && isSnapCorner(persisted)) {
|
|
184
|
+
setCorner(persisted);
|
|
185
|
+
}
|
|
186
|
+
}, [readPersistedCorner]);
|
|
187
|
+
|
|
188
|
+
const completeSnap = useCallback(() => {
|
|
189
|
+
const nextCorner = pendingSnapCornerRef.current;
|
|
190
|
+
pendingSnapCornerRef.current = null;
|
|
191
|
+
snapTargetScheduledRef.current = false;
|
|
192
|
+
setIsSnapping(false);
|
|
193
|
+
setSnapPosition(null);
|
|
194
|
+
|
|
195
|
+
if (nextCorner) {
|
|
196
|
+
setCorner(nextCorner);
|
|
197
|
+
persistCorner?.(nextCorner);
|
|
198
|
+
}
|
|
199
|
+
}, [persistCorner]);
|
|
200
|
+
|
|
201
|
+
useEffect(() => {
|
|
202
|
+
if (!isSnapping || snapTargetScheduledRef.current) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (!container || !elementRef.current) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const nextCorner = pendingSnapCornerRef.current;
|
|
210
|
+
if (!nextCorner) return;
|
|
211
|
+
|
|
212
|
+
snapTargetScheduledRef.current = true;
|
|
213
|
+
|
|
214
|
+
const prefersReducedMotion =
|
|
215
|
+
typeof window !== "undefined" &&
|
|
216
|
+
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
217
|
+
|
|
218
|
+
if (prefersReducedMotion) {
|
|
219
|
+
completeSnap();
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const elementWidth = elementRef.current.offsetWidth;
|
|
224
|
+
const elementHeight = elementRef.current.offsetHeight;
|
|
225
|
+
const target = getCornerPosition(
|
|
226
|
+
nextCorner,
|
|
227
|
+
container.clientWidth,
|
|
228
|
+
container.clientHeight,
|
|
229
|
+
elementWidth,
|
|
230
|
+
elementHeight,
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
const frame = requestAnimationFrame(() => {
|
|
234
|
+
requestAnimationFrame(() => {
|
|
235
|
+
setSnapPosition(target);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
return () => cancelAnimationFrame(frame);
|
|
240
|
+
}, [isSnapping, container, elementRef, completeSnap]);
|
|
241
|
+
|
|
242
|
+
useEffect(() => {
|
|
243
|
+
if (!isSnapping || !elementRef.current) return;
|
|
244
|
+
|
|
245
|
+
const element = elementRef.current;
|
|
246
|
+
let didComplete = false;
|
|
247
|
+
|
|
248
|
+
const finishOnce = () => {
|
|
249
|
+
if (didComplete) return;
|
|
250
|
+
didComplete = true;
|
|
251
|
+
completeSnap();
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const handleTransitionEnd = (event: TransitionEvent) => {
|
|
255
|
+
if (event.target !== element) return;
|
|
256
|
+
if (event.propertyName !== "left" && event.propertyName !== "top") {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
finishOnce();
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const fallbackTimer = window.setTimeout(finishOnce, 350);
|
|
263
|
+
|
|
264
|
+
element.addEventListener("transitionend", handleTransitionEnd);
|
|
265
|
+
return () => {
|
|
266
|
+
window.clearTimeout(fallbackTimer);
|
|
267
|
+
element.removeEventListener("transitionend", handleTransitionEnd);
|
|
268
|
+
};
|
|
269
|
+
}, [isSnapping, elementRef, completeSnap]);
|
|
270
|
+
|
|
271
|
+
const handleBarPointerDown = useCallback(
|
|
272
|
+
(event: ReactPointerEvent<HTMLDivElement>) => {
|
|
273
|
+
if (!container || !elementRef.current) return;
|
|
274
|
+
if (event.button !== 0) return;
|
|
275
|
+
|
|
276
|
+
didDragRef.current = false;
|
|
277
|
+
|
|
278
|
+
const origin = getRelativePosition(container, elementRef.current);
|
|
279
|
+
dragStateRef.current = {
|
|
280
|
+
pointerId: event.pointerId,
|
|
281
|
+
startClientX: event.clientX,
|
|
282
|
+
startClientY: event.clientY,
|
|
283
|
+
originLeft: origin.left,
|
|
284
|
+
originTop: origin.top,
|
|
285
|
+
didDrag: false,
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
289
|
+
},
|
|
290
|
+
[container, elementRef],
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
const handleBarPointerMove = useCallback(
|
|
294
|
+
(event: ReactPointerEvent<HTMLDivElement>) => {
|
|
295
|
+
const dragState = dragStateRef.current;
|
|
296
|
+
if (!dragState || dragState.pointerId !== event.pointerId) return;
|
|
297
|
+
if (!container || !elementRef.current) return;
|
|
298
|
+
|
|
299
|
+
const deltaX = event.clientX - dragState.startClientX;
|
|
300
|
+
const deltaY = event.clientY - dragState.startClientY;
|
|
301
|
+
|
|
302
|
+
if (!dragState.didDrag) {
|
|
303
|
+
if (Math.hypot(deltaX, deltaY) < DRAG_THRESHOLD_PX) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
dragState.didDrag = true;
|
|
308
|
+
didDragRef.current = true;
|
|
309
|
+
setIsDragging(true);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const padding = 16;
|
|
313
|
+
const containerWidth = container.clientWidth;
|
|
314
|
+
const containerHeight = container.clientHeight;
|
|
315
|
+
const elementWidth = elementRef.current.offsetWidth;
|
|
316
|
+
const elementHeight = elementRef.current.offsetHeight;
|
|
317
|
+
|
|
318
|
+
const nextPosition = clampPosition(
|
|
319
|
+
{
|
|
320
|
+
left: dragState.originLeft + deltaX,
|
|
321
|
+
top: dragState.originTop + deltaY,
|
|
322
|
+
},
|
|
323
|
+
containerWidth,
|
|
324
|
+
containerHeight,
|
|
325
|
+
elementWidth,
|
|
326
|
+
elementHeight,
|
|
327
|
+
padding,
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
lastDragPositionRef.current = nextPosition;
|
|
331
|
+
setDragPosition(nextPosition);
|
|
332
|
+
},
|
|
333
|
+
[container, elementRef],
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
const finishDrag = useCallback(
|
|
337
|
+
(event: ReactPointerEvent<HTMLDivElement>) => {
|
|
338
|
+
const dragState = dragStateRef.current;
|
|
339
|
+
if (!dragState || dragState.pointerId !== event.pointerId) return;
|
|
340
|
+
|
|
341
|
+
const didDrag = dragState.didDrag;
|
|
342
|
+
dragStateRef.current = null;
|
|
343
|
+
|
|
344
|
+
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
|
|
345
|
+
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
setIsDragging(false);
|
|
349
|
+
|
|
350
|
+
if (!didDrag || !container || !elementRef.current) {
|
|
351
|
+
lastDragPositionRef.current = null;
|
|
352
|
+
setDragPosition(null);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const currentPosition =
|
|
357
|
+
lastDragPositionRef.current ??
|
|
358
|
+
getRelativePosition(container, elementRef.current);
|
|
359
|
+
lastDragPositionRef.current = null;
|
|
360
|
+
const elementWidth = elementRef.current.offsetWidth;
|
|
361
|
+
const elementHeight = elementRef.current.offsetHeight;
|
|
362
|
+
const containerWidth = container.clientWidth;
|
|
363
|
+
const containerHeight = container.clientHeight;
|
|
364
|
+
const centerX = currentPosition.left + elementWidth / 2;
|
|
365
|
+
const centerY = currentPosition.top + elementHeight / 2;
|
|
366
|
+
const nextCorner = getNearestCorner(
|
|
367
|
+
centerX,
|
|
368
|
+
centerY,
|
|
369
|
+
containerWidth,
|
|
370
|
+
containerHeight,
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
pendingSnapCornerRef.current = nextCorner;
|
|
374
|
+
setDragPosition(null);
|
|
375
|
+
setSnapPosition(currentPosition);
|
|
376
|
+
setIsSnapping(true);
|
|
377
|
+
},
|
|
378
|
+
[container, elementRef],
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
const handleBarPointerUp = useCallback(
|
|
382
|
+
(event: ReactPointerEvent<HTMLDivElement>) => {
|
|
383
|
+
finishDrag(event);
|
|
384
|
+
},
|
|
385
|
+
[finishDrag],
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
const handleBarPointerCancel = useCallback(
|
|
389
|
+
(event: ReactPointerEvent<HTMLDivElement>) => {
|
|
390
|
+
finishDrag(event);
|
|
391
|
+
},
|
|
392
|
+
[finishDrag],
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
const wasDragGesture = useCallback(() => {
|
|
396
|
+
const wasDrag = didDragRef.current;
|
|
397
|
+
didDragRef.current = false;
|
|
398
|
+
return wasDrag;
|
|
399
|
+
}, []);
|
|
400
|
+
|
|
401
|
+
const freePosition = isDragging ? dragPosition : isSnapping ? snapPosition : null;
|
|
402
|
+
const useFreePosition = Boolean(freePosition);
|
|
403
|
+
|
|
404
|
+
return {
|
|
405
|
+
corner,
|
|
406
|
+
dragPosition,
|
|
407
|
+
freePosition,
|
|
408
|
+
isDragging,
|
|
409
|
+
isSnapping,
|
|
410
|
+
useFreePosition,
|
|
411
|
+
onBarPointerCancel: handleBarPointerCancel,
|
|
412
|
+
onBarPointerDown: handleBarPointerDown,
|
|
413
|
+
onBarPointerMove: handleBarPointerMove,
|
|
414
|
+
onBarPointerUp: handleBarPointerUp,
|
|
415
|
+
wasDragGesture,
|
|
416
|
+
};
|
|
417
|
+
}
|