@backmatter/studio 0.0.7 → 0.0.9

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.
Files changed (72) hide show
  1. package/dist/chunks/UiKitApp-b8qFsmA-.js +2401 -0
  2. package/dist/chunks/WorkspaceRoutes-BKKEkAq7.js +90126 -0
  3. package/dist/chunks/{dashboard-DCHRI0WD.js → dashboard-BNUcbYU0.js} +380 -382
  4. package/dist/chunks/{index-BS0-Bvbk.js → index-laWAz5_p.js} +111 -112
  5. package/dist/dashboard.js +2 -2
  6. package/dist/index.js +287 -146
  7. package/dist/site-widget.css +1615 -0
  8. package/dist/site-widget.js +30896 -630
  9. package/dist/studio.css +303 -98
  10. package/package.json +4 -4
  11. package/src/App.tsx +12 -29
  12. package/src/api.ts +55 -0
  13. package/src/chat/ChangeCard.tsx +2 -0
  14. package/src/chat/ChatMessage.tsx +73 -62
  15. package/src/chat/ChatPanel.tsx +110 -15
  16. package/src/chat/DocListCard.tsx +2 -0
  17. package/src/chat/LiveStatus.tsx +78 -8
  18. package/src/chat/Reveal.tsx +34 -0
  19. package/src/chat/Shimmer.tsx +65 -23
  20. package/src/chat/chat-helpers.ts +35 -0
  21. package/src/chat/motion.ts +73 -0
  22. package/src/chat/useStickToBottom.ts +129 -14
  23. package/src/chrome/AppShell.tsx +47 -10
  24. package/src/chrome/AppSidebar.tsx +9 -34
  25. package/src/chrome/ContentExplorerSidebar.tsx +149 -96
  26. package/src/chrome/UserMenu.tsx +3 -1
  27. package/src/chrome/content-tree-visibility.ts +55 -0
  28. package/src/chrome/explorer-icons.tsx +2 -4
  29. package/src/devbar-state.ts +125 -0
  30. package/src/index.css +3 -131
  31. package/src/pages/CliAuthorizePage.tsx +61 -28
  32. package/src/pages/MediaGalleryPage.tsx +144 -85
  33. package/src/pages/MediaLightbox.tsx +195 -96
  34. package/src/pages/SelectRepositoriesLink.tsx +28 -4
  35. package/src/pages/SettingsGeneralPage.tsx +25 -28
  36. package/src/pages/SettingsGithubPage.tsx +413 -124
  37. package/src/pages/SettingsUsagePage.tsx +23 -1
  38. package/src/pages/media-grid.ts +14 -1
  39. package/src/pages/ui-kit/UiKitApp.tsx +164 -0
  40. package/src/pages/ui-kit/buttons.tsx +30 -0
  41. package/src/pages/ui-kit/catalog.tsx +41 -0
  42. package/src/pages/ui-kit/colors.tsx +95 -0
  43. package/src/pages/ui-kit/devbar.tsx +160 -0
  44. package/src/pages/ui-kit/feedback.tsx +39 -0
  45. package/src/pages/ui-kit/fields.tsx +47 -0
  46. package/src/pages/ui-kit/lists.tsx +19 -0
  47. package/src/pages/ui-kit/overlays.tsx +39 -0
  48. package/src/pages/ui-kit/path.ts +9 -0
  49. package/src/pages/ui-kit/sections.tsx +32 -0
  50. package/src/pages/ui-kit/surfaces.tsx +121 -0
  51. package/src/pages/ui-kit/topic.tsx +112 -0
  52. package/src/pages/ui-kit/topics.ts +71 -0
  53. package/src/pages/ui-kit/type.tsx +55 -0
  54. package/src/preview-frame-radius.ts +51 -0
  55. package/src/preview-frame.tsx +93 -49
  56. package/src/shell-resets.css +155 -0
  57. package/src/site-bar.tsx +1791 -425
  58. package/src/site-dashboard-href.ts +21 -0
  59. package/src/site-devbar-overlay-inject.ts +15 -0
  60. package/src/site-devbar-overlay.css +217 -0
  61. package/src/site-pointer.ts +1 -1
  62. package/src/site-publish-menu.tsx +33 -0
  63. package/src/site-widget-styles.ts +7 -0
  64. package/src/site-widget.tsx +61 -21
  65. package/src/studio-prose-ui.css +39 -0
  66. package/src/theme/ThemeProvider.tsx +4 -1
  67. package/src/workspace-path.ts +18 -1
  68. package/dist/chunks/EmbedPublishPage-88VsmOhi.js +0 -149
  69. package/dist/chunks/UiKitPage-X4hkFL2d.js +0 -173
  70. package/dist/chunks/WorkspaceRoutes-DXzdWqN3.js +0 -59523
  71. package/dist/chunks/status-bar-action-wj1KZ51m.js +0 -30612
  72. package/src/pages/UiKitPage.tsx +0 -331
@@ -0,0 +1,121 @@
1
+ // packages/studio/src/pages/ui-kit/surfaces.tsx
2
+ import * as stylex from "@stylexjs/stylex";
3
+ import { colors, radius, space } from "../../styles/tokens.stylex";
4
+ import { Text } from "../../ui/Text";
5
+ import { Specimen, Topic } from "./topic";
6
+ import { UI_KIT_TOPICS } from "./topics";
7
+
8
+ const topic = UI_KIT_TOPICS.find((item) => item.slug === "surfaces")!;
9
+
10
+ export function SurfacesTopic() {
11
+ return (
12
+ <Topic title={topic.title} lede={topic.lede}>
13
+ <Specimen label="Space">
14
+ <div {...stylex.props(styles.scale)}>
15
+ <Scale label="xs" bar={styles.xs} />
16
+ <Scale label="sm" bar={styles.sm} />
17
+ <Scale label="md" bar={styles.md} />
18
+ <Scale label="lg" bar={styles.lg} />
19
+ <Scale label="xl" bar={styles.xl} />
20
+ <Scale label="xxl" bar={styles.xxl} />
21
+ </div>
22
+ </Specimen>
23
+ <Specimen label="Radius">
24
+ <div {...stylex.props(styles.scale)}>
25
+ <Scale label="sm" box={styles.radiusSm} />
26
+ <Scale label="md" box={styles.radiusMd} />
27
+ </div>
28
+ </Specimen>
29
+ <Specimen label="Backgrounds">
30
+ <div {...stylex.props(styles.role, styles.app)}>
31
+ <Text size="xs" tone="muted" mono>
32
+ bgApp
33
+ </Text>
34
+ <div {...stylex.props(styles.role, styles.sidebar)}>
35
+ <Text size="xs" tone="muted" mono>
36
+ bgSidebar
37
+ </Text>
38
+ <div {...stylex.props(styles.role, styles.panel)}>
39
+ <Text size="xs" tone="muted" mono>
40
+ bgPanel
41
+ </Text>
42
+ <div {...stylex.props(styles.role, styles.surface)}>
43
+ <Text size="xs" mono>
44
+ bgSurface
45
+ </Text>
46
+ </div>
47
+ </div>
48
+ </div>
49
+ </div>
50
+ </Specimen>
51
+ </Topic>
52
+ );
53
+ }
54
+
55
+ function Scale({
56
+ label,
57
+ bar,
58
+ box,
59
+ }: {
60
+ label: string;
61
+ bar?: stylex.StyleXStyles;
62
+ box?: stylex.StyleXStyles;
63
+ }) {
64
+ return (
65
+ <div {...stylex.props(styles.tick)}>
66
+ <div {...stylex.props(bar ? styles.bar : styles.radiusBox, bar, box)} />
67
+ <Text size="xs" tone="muted" mono>
68
+ {label}
69
+ </Text>
70
+ </div>
71
+ );
72
+ }
73
+
74
+ const styles = stylex.create({
75
+ bar: {
76
+ height: "16px",
77
+ backgroundColor: colors.accent,
78
+ borderRadius: radius.sm,
79
+ },
80
+ scale: {
81
+ display: "flex",
82
+ flexWrap: "wrap",
83
+ alignItems: "flex-end",
84
+ gap: space.xl,
85
+ },
86
+ tick: {
87
+ display: "flex",
88
+ flexDirection: "column",
89
+ alignItems: "flex-start",
90
+ gap: space.sm,
91
+ },
92
+ xs: { width: space.xs },
93
+ sm: { width: space.sm },
94
+ md: { width: space.md },
95
+ lg: { width: space.lg },
96
+ xl: { width: space.xl },
97
+ xxl: { width: space.xxl },
98
+ radiusBox: {
99
+ width: "64px",
100
+ height: "40px",
101
+ backgroundColor: colors.bgSurface,
102
+ borderWidth: 1,
103
+ borderStyle: "solid",
104
+ borderColor: colors.border,
105
+ },
106
+ radiusSm: { borderRadius: radius.sm },
107
+ radiusMd: { borderRadius: radius.md },
108
+ role: {
109
+ padding: space.lg,
110
+ borderRadius: radius.md,
111
+ },
112
+ app: { backgroundColor: colors.bgApp },
113
+ sidebar: { backgroundColor: colors.bgSidebar },
114
+ panel: { backgroundColor: colors.bgPanel },
115
+ surface: {
116
+ backgroundColor: colors.bgSurface,
117
+ borderWidth: 1,
118
+ borderStyle: "solid",
119
+ borderColor: colors.border,
120
+ },
121
+ });
@@ -0,0 +1,112 @@
1
+ // packages/studio/src/pages/ui-kit/topic.tsx
2
+ import * as stylex from "@stylexjs/stylex";
3
+ import { colors, space, type } from "../../styles/tokens.stylex";
4
+ import { Text } from "../../ui/Text";
5
+
6
+ export function Topic({
7
+ title,
8
+ lede,
9
+ children,
10
+ }: {
11
+ title: string;
12
+ lede: string;
13
+ children: React.ReactNode;
14
+ }) {
15
+ return (
16
+ <div {...stylex.props(styles.topic)}>
17
+ <header {...stylex.props(styles.intro)}>
18
+ <Text as="h1" size="display" weight="semibold">
19
+ {title}
20
+ </Text>
21
+ <Text tone="muted" size="sm">
22
+ {lede}
23
+ </Text>
24
+ </header>
25
+ <div {...stylex.props(styles.body)}>{children}</div>
26
+ </div>
27
+ );
28
+ }
29
+
30
+ /** Two aligned columns. Sections share a top edge across the row. */
31
+ export function Columns({ children }: { children: React.ReactNode }) {
32
+ return <div {...stylex.props(styles.columns)}>{children}</div>;
33
+ }
34
+
35
+ export function Row({ children }: { children: React.ReactNode }) {
36
+ return <div {...stylex.props(styles.row)}>{children}</div>;
37
+ }
38
+
39
+ export function Specimen({
40
+ label,
41
+ children,
42
+ }: {
43
+ label: string;
44
+ children: React.ReactNode;
45
+ /** Kept so older call sites still compile. Sections always stack. */
46
+ block?: boolean;
47
+ }) {
48
+ return (
49
+ <section {...stylex.props(styles.specimen)}>
50
+ <h2 {...stylex.props(styles.heading)}>{label}</h2>
51
+ <div {...stylex.props(styles.example)}>{children}</div>
52
+ </section>
53
+ );
54
+ }
55
+
56
+ const styles = stylex.create({
57
+ topic: {
58
+ display: "flex",
59
+ flexDirection: "column",
60
+ gap: "40px",
61
+ maxWidth: "920px",
62
+ },
63
+ intro: {
64
+ display: "flex",
65
+ flexDirection: "column",
66
+ gap: space.md,
67
+ maxWidth: "62ch",
68
+ },
69
+ body: {
70
+ display: "flex",
71
+ flexDirection: "column",
72
+ gap: "40px",
73
+ },
74
+ columns: {
75
+ display: "grid",
76
+ gridTemplateColumns: "repeat(2, minmax(0, 1fr))",
77
+ columnGap: "48px",
78
+ rowGap: "40px",
79
+ alignItems: "start",
80
+ "@media (max-width: 800px)": {
81
+ gridTemplateColumns: "1fr",
82
+ },
83
+ },
84
+ specimen: {
85
+ display: "flex",
86
+ flexDirection: "column",
87
+ gap: space.md,
88
+ minWidth: 0,
89
+ },
90
+ heading: {
91
+ margin: 0,
92
+ fontSize: type.lg,
93
+ fontWeight: 600,
94
+ letterSpacing: "-0.01em",
95
+ lineHeight: 1.3,
96
+ color: colors.text,
97
+ },
98
+ example: {
99
+ display: "flex",
100
+ flexDirection: "column",
101
+ alignItems: "stretch",
102
+ gap: space.sm,
103
+ width: "100%",
104
+ minWidth: 0,
105
+ },
106
+ row: {
107
+ display: "flex",
108
+ flexWrap: "wrap",
109
+ alignItems: "center",
110
+ gap: space.sm,
111
+ },
112
+ });
@@ -0,0 +1,71 @@
1
+ // packages/studio/src/pages/ui-kit/topics.ts
2
+
3
+ export const UI_KIT_TOPICS = [
4
+ {
5
+ slug: "colors",
6
+ title: "Colors",
7
+ group: "Foundations",
8
+ lede: "Color tokens paint surfaces, text, and status, and follow the active theme.",
9
+ },
10
+ {
11
+ slug: "type",
12
+ title: "Type",
13
+ group: "Foundations",
14
+ lede: "Text sizes and tones for titles, body copy, and metadata.",
15
+ },
16
+ {
17
+ slug: "surfaces",
18
+ title: "Surfaces",
19
+ group: "Foundations",
20
+ lede: "Spacing, radii, and the background roles stacked the way a page stacks them.",
21
+ },
22
+ {
23
+ slug: "sections",
24
+ title: "Sections",
25
+ group: "Foundations",
26
+ lede: "Page header, panel, and stack are how a settings page is built.",
27
+ },
28
+ {
29
+ slug: "buttons",
30
+ title: "Buttons",
31
+ group: "Components",
32
+ lede: "Variants for a primary action, a secondary action, a quiet action, and danger.",
33
+ },
34
+ {
35
+ slug: "fields",
36
+ title: "Fields",
37
+ group: "Components",
38
+ lede: "Text, select, and multiline fields, including a hint and a disabled control.",
39
+ },
40
+ {
41
+ slug: "lists",
42
+ title: "Lists",
43
+ group: "Components",
44
+ lede: "A list of rows that link to a document.",
45
+ },
46
+ {
47
+ slug: "overlays",
48
+ title: "Overlays",
49
+ group: "Components",
50
+ lede: "A confirm dialog and a tooltip.",
51
+ },
52
+ {
53
+ slug: "feedback",
54
+ title: "Feedback",
55
+ group: "Components",
56
+ lede: "Status bars, and the success and danger text tones.",
57
+ },
58
+ {
59
+ slug: "devbar",
60
+ title: "DevBar",
61
+ group: "Host",
62
+ lede: "The host bar a site mounts. These specimens do not call the preview API.",
63
+ },
64
+ ] as const;
65
+
66
+ export type UiKitTopicSlug = (typeof UI_KIT_TOPICS)[number]["slug"];
67
+
68
+ export function resolveUiKitTopic(slug: string | undefined): UiKitTopicSlug {
69
+ const match = UI_KIT_TOPICS.find((topic) => topic.slug === slug);
70
+ return match ? match.slug : "colors";
71
+ }
@@ -0,0 +1,55 @@
1
+ // packages/studio/src/pages/ui-kit/type.tsx
2
+ import { Text } from "../../ui/Text";
3
+ import { Specimen, Topic } from "./topic";
4
+ import { UI_KIT_TOPICS } from "./topics";
5
+
6
+ const topic = UI_KIT_TOPICS.find((item) => item.slug === "type")!;
7
+
8
+ export function TypeTopic() {
9
+ return (
10
+ <Topic title={topic.title} lede={topic.lede}>
11
+ <Specimen label="display">
12
+ <Text size="display" weight="semibold">
13
+ Display
14
+ </Text>
15
+ </Specimen>
16
+ <Specimen label="xl">
17
+ <Text size="xl" weight="semibold">
18
+ Title
19
+ </Text>
20
+ </Specimen>
21
+ <Specimen label="lg">
22
+ <Text size="lg" weight="medium">
23
+ Heading
24
+ </Text>
25
+ </Specimen>
26
+ <Specimen label="md">
27
+ <Text>Body</Text>
28
+ </Specimen>
29
+ <Specimen label="sm">
30
+ <Text size="sm">Small</Text>
31
+ </Specimen>
32
+ <Specimen label="xs">
33
+ <Text size="xs" weight="semibold">
34
+ Overline
35
+ </Text>
36
+ </Specimen>
37
+ <Specimen label="muted">
38
+ <Text tone="muted" size="sm">
39
+ Muted
40
+ </Text>
41
+ </Specimen>
42
+ <Specimen label="danger">
43
+ <Text tone="danger">Danger</Text>
44
+ </Specimen>
45
+ <Specimen label="success">
46
+ <Text tone="success">Success</Text>
47
+ </Specimen>
48
+ <Specimen label="mono">
49
+ <Text mono size="sm">
50
+ content/blog/hello.mdx
51
+ </Text>
52
+ </Specimen>
53
+ </Topic>
54
+ );
55
+ }
@@ -0,0 +1,51 @@
1
+ // packages/studio/src/preview-frame-radius.ts
2
+ // Heuristic bottom corner radius for the on-site preview ring (no web API for real window shape).
3
+
4
+ /** Chromium on macOS 26+ (Tahoe) — see chromium/brave window corner updates. */
5
+ export const PREVIEW_FRAME_RADIUS_MAC_CHROMIUM_TAHOE = 20;
6
+ /** Chromium on macOS with rounded desktop windows (approx. Sequoia era). */
7
+ export const PREVIEW_FRAME_RADIUS_MAC_CHROMIUM = 16;
8
+ /** Safari / Firefox on macOS. */
9
+ export const PREVIEW_FRAME_RADIUS_MAC = 12;
10
+ export const PREVIEW_FRAME_RADIUS_WINDOWS = 8;
11
+ export const PREVIEW_FRAME_RADIUS_DEFAULT = 12;
12
+
13
+ /** Pull macOS major from Chromium `platformVersion` (e.g. "26.1.0" → 26). */
14
+ export function macPlatformMajor(platformVersion?: string): number | null {
15
+ if (!platformVersion) return null;
16
+ const major = Number.parseInt(platformVersion.split(".")[0] ?? "", 10);
17
+ return Number.isFinite(major) ? major : null;
18
+ }
19
+
20
+ export function isMacUserAgent(userAgent: string): boolean {
21
+ return /Macintosh|Mac OS X/.test(userAgent);
22
+ }
23
+
24
+ /** Chromium desktop builds on macOS (Chrome, Brave, Edge, Opera). */
25
+ export function isMacChromiumUserAgent(userAgent: string): boolean {
26
+ if (!isMacUserAgent(userAgent)) return false;
27
+ if (/Firefox\//.test(userAgent)) return false;
28
+ return /Chrome\/|Chromium\/|Edg\/|OPR\/|Brave\//.test(userAgent);
29
+ }
30
+
31
+ /**
32
+ * Guess preview ring bottom radius (px). `platformVersion` comes from
33
+ * `navigator.userAgentData.getHighEntropyValues(['platformVersion'])` when available.
34
+ */
35
+ export function previewFrameRadiusFromUserAgent(
36
+ userAgent: string,
37
+ platformVersion?: string,
38
+ ): number {
39
+ if (/Windows/.test(userAgent)) return PREVIEW_FRAME_RADIUS_WINDOWS;
40
+ if (!isMacUserAgent(userAgent)) return PREVIEW_FRAME_RADIUS_DEFAULT;
41
+
42
+ const macMajor = macPlatformMajor(platformVersion);
43
+ if (isMacChromiumUserAgent(userAgent)) {
44
+ if (macMajor !== null && macMajor >= 26) return PREVIEW_FRAME_RADIUS_MAC_CHROMIUM_TAHOE;
45
+ if (macMajor !== null && macMajor >= 15) return PREVIEW_FRAME_RADIUS_MAC_CHROMIUM;
46
+ // UA-only: still assume modern Chromium on Mac (Brave/Chrome rarely expose OS in UA).
47
+ return PREVIEW_FRAME_RADIUS_MAC_CHROMIUM;
48
+ }
49
+
50
+ return PREVIEW_FRAME_RADIUS_MAC;
51
+ }
@@ -1,38 +1,92 @@
1
1
  // packages/studio/src/preview-frame.tsx
2
- // Full-viewport ring and a top-right preview label while the site session is active.
2
+ // Full-viewport violet frame while preview is on. No corner label.
3
+ import { useLayoutEffect, useRef, useState, type CSSProperties } from "react";
4
+ import {
5
+ DEVBAR_EASE_IN,
6
+ DEVBAR_EASE_OUT,
7
+ DEVBAR_FRAME_ENTER_MS,
8
+ DEVBAR_FRAME_EXIT_MS,
9
+ prefersReducedMotion,
10
+ } from "./devbar-state";
11
+ import { PREVIEW_FRAME_RADIUS_DEFAULT, previewFrameRadiusFromUserAgent } from "./preview-frame-radius";
12
+
3
13
  export const PREVIEW_FRAME_CLASS = "backmatter-preview-frame";
4
14
 
5
- /** Shared with the on-site dev bar while preview is active. */
6
- export const PREVIEW_CHROME_BORDER = "#d97706";
15
+ /** Bottom window corner radius when the host does not set `devBar.previewFrameRadius`. */
16
+ export function guessPreviewFrameRadius(): number {
17
+ if (typeof navigator === "undefined") return PREVIEW_FRAME_RADIUS_DEFAULT;
18
+ return previewFrameRadiusFromUserAgent(navigator.userAgent);
19
+ }
7
20
 
8
- function EyeIcon() {
9
- return (
10
- <svg width="14" height="14" viewBox="0 0 16 16" fill="none" aria-hidden>
11
- <path
12
- d="M1.5 8s2.5-4.5 6.5-4.5S14.5 8 14.5 8s-2.5 4.5-6.5 4.5S1.5 8 1.5 8Z"
13
- stroke="currentColor"
14
- strokeWidth="1.25"
15
- strokeLinejoin="round"
16
- />
17
- <circle cx="8" cy="8" r="1.75" stroke="currentColor" strokeWidth="1.25" />
18
- </svg>
19
- );
21
+ /** Host config, then `--bm-preview-frame-radius` on `<html>`, then platform guess. */
22
+ export function resolvePreviewFrameRadius(override?: number): number {
23
+ if (override !== undefined && Number.isFinite(override) && override >= 0) return override;
24
+ if (typeof document !== "undefined") {
25
+ const raw = getComputedStyle(document.documentElement).getPropertyValue("--bm-preview-frame-radius").trim();
26
+ if (raw) {
27
+ const parsed = Number.parseFloat(raw);
28
+ if (Number.isFinite(parsed) && parsed >= 0) return parsed;
29
+ }
30
+ }
31
+ return guessPreviewFrameRadius();
20
32
  }
21
33
 
22
- export function PreviewFrame() {
34
+ /** Violet used by the frame and the preview toolbar. */
35
+ export const PREVIEW_CHROME_BORDER = "oklch(0.56 0.21 295)";
36
+
37
+ export function PreviewFrame({
38
+ active,
39
+ radius,
40
+ }: {
41
+ active: boolean;
42
+ /** Bottom window corner radius (px). Top stays square under the browser chrome. */
43
+ radius?: number;
44
+ }) {
45
+ const ref = useRef<HTMLDivElement>(null);
46
+ const was = useRef<boolean | null>(null);
47
+ const [corner, setCorner] = useState(() => resolvePreviewFrameRadius(radius));
48
+
49
+ useLayoutEffect(() => {
50
+ if (radius !== undefined) {
51
+ setCorner(resolvePreviewFrameRadius(radius));
52
+ return;
53
+ }
54
+ const ua = navigator.userAgent;
55
+ setCorner(previewFrameRadiusFromUserAgent(ua));
56
+
57
+ const uad = navigator.userAgentData;
58
+ if (!uad?.getHighEntropyValues) return;
59
+ void uad.getHighEntropyValues(["platformVersion"]).then((hints) => {
60
+ setCorner(previewFrameRadiusFromUserAgent(ua, hints.platformVersion));
61
+ });
62
+ }, [radius]);
63
+
64
+ useLayoutEffect(() => {
65
+ const frame = ref.current;
66
+ const previous = was.current;
67
+ was.current = active;
68
+ if (!frame || previous === null || previous || !active || prefersReducedMotion()) return;
69
+ frame.animate(
70
+ [
71
+ { borderWidth: "3px" },
72
+ { borderWidth: "7px", offset: 0.5 },
73
+ { borderWidth: "3px" },
74
+ ],
75
+ { duration: 700, delay: 180, easing: DEVBAR_EASE_OUT },
76
+ );
77
+ }, [active]);
78
+
23
79
  return (
24
80
  <>
25
81
  <style>{FRAME_CSS}</style>
26
82
  <div
83
+ ref={ref}
27
84
  className={PREVIEW_FRAME_CLASS}
28
85
  data-backmatter-preview-frame=""
86
+ data-active={active ? "true" : "false"}
87
+ style={{ "--bm-preview-frame-radius": `${corner}px` } as CSSProperties}
29
88
  aria-hidden
30
- >
31
- <div className={`${PREVIEW_FRAME_CLASS}-badge`}>
32
- <EyeIcon />
33
- <span>Preview</span>
34
- </div>
35
- </div>
89
+ />
36
90
  </>
37
91
  );
38
92
  }
@@ -42,37 +96,27 @@ const FRAME_CSS = `
42
96
  position: fixed;
43
97
  inset: 0;
44
98
  z-index: 2147482999;
99
+ box-sizing: border-box;
45
100
  pointer-events: none;
46
- box-shadow: inset 0 0 0 2px ${PREVIEW_CHROME_BORDER};
47
- overflow: visible;
48
- }
49
- .${PREVIEW_FRAME_CLASS}-badge {
50
- position: absolute;
51
- top: env(safe-area-inset-top, 0px);
52
- right: env(safe-area-inset-right, 0px);
53
- display: inline-flex;
54
- align-items: center;
55
- gap: 6px;
56
- padding: 6px 12px;
57
- border: 2px solid ${PREVIEW_CHROME_BORDER};
58
- border-top: 0;
59
- border-radius: 0 0 14px 14px;
60
- background: #fff7ed;
61
- color: #9a3412;
62
- font: 500 12px/1 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
63
- white-space: nowrap;
64
- -webkit-font-smoothing: antialiased;
101
+ border: 3px solid ${PREVIEW_CHROME_BORDER};
102
+ border-radius: 0 0 var(--bm-preview-frame-radius, 12px) var(--bm-preview-frame-radius, 12px);
103
+ background: transparent;
104
+ clip-path: circle(0% at 100% 100%);
105
+ transition: clip-path ${DEVBAR_FRAME_EXIT_MS}ms ${DEVBAR_EASE_IN};
65
106
  }
66
- @keyframes backmatter-preview-frame-in {
67
- from { opacity: 0; }
68
- to { opacity: 1; }
107
+ .${PREVIEW_FRAME_CLASS}[data-active="true"] {
108
+ /* 150% is shorter than the viewport diagonal — the ring never reached the top. */
109
+ clip-path: circle(150vmax at 100% 100%);
110
+ transition: clip-path ${DEVBAR_FRAME_ENTER_MS}ms ${DEVBAR_EASE_OUT} 80ms;
69
111
  }
70
- @media (prefers-reduced-motion: no-preference) {
112
+ @media (prefers-reduced-motion: reduce) {
71
113
  .${PREVIEW_FRAME_CLASS} {
72
- animation: backmatter-preview-frame-in 240ms cubic-bezier(0.23, 1, 0.32, 1) both;
114
+ transition: none;
115
+ clip-path: none;
116
+ opacity: 0;
117
+ }
118
+ .${PREVIEW_FRAME_CLASS}[data-active="true"] {
119
+ opacity: 1;
73
120
  }
74
- }
75
- @media (prefers-reduced-motion: reduce) {
76
- .${PREVIEW_FRAME_CLASS} { animation: none; }
77
121
  }
78
122
  `;