@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.
- package/dist/chunks/UiKitApp-b8qFsmA-.js +2401 -0
- package/dist/chunks/WorkspaceRoutes-BKKEkAq7.js +90126 -0
- package/dist/chunks/{dashboard-DCHRI0WD.js → dashboard-BNUcbYU0.js} +380 -382
- package/dist/chunks/{index-BS0-Bvbk.js → index-laWAz5_p.js} +111 -112
- package/dist/dashboard.js +2 -2
- package/dist/index.js +287 -146
- package/dist/site-widget.css +1615 -0
- package/dist/site-widget.js +30896 -630
- package/dist/studio.css +303 -98
- package/package.json +4 -4
- package/src/App.tsx +12 -29
- package/src/api.ts +55 -0
- package/src/chat/ChangeCard.tsx +2 -0
- package/src/chat/ChatMessage.tsx +73 -62
- package/src/chat/ChatPanel.tsx +110 -15
- package/src/chat/DocListCard.tsx +2 -0
- package/src/chat/LiveStatus.tsx +78 -8
- package/src/chat/Reveal.tsx +34 -0
- package/src/chat/Shimmer.tsx +65 -23
- package/src/chat/chat-helpers.ts +35 -0
- package/src/chat/motion.ts +73 -0
- package/src/chat/useStickToBottom.ts +129 -14
- package/src/chrome/AppShell.tsx +47 -10
- package/src/chrome/AppSidebar.tsx +9 -34
- package/src/chrome/ContentExplorerSidebar.tsx +149 -96
- package/src/chrome/UserMenu.tsx +3 -1
- package/src/chrome/content-tree-visibility.ts +55 -0
- package/src/chrome/explorer-icons.tsx +2 -4
- package/src/devbar-state.ts +125 -0
- package/src/index.css +3 -131
- package/src/pages/CliAuthorizePage.tsx +61 -28
- package/src/pages/MediaGalleryPage.tsx +144 -85
- package/src/pages/MediaLightbox.tsx +195 -96
- package/src/pages/SelectRepositoriesLink.tsx +28 -4
- package/src/pages/SettingsGeneralPage.tsx +25 -28
- package/src/pages/SettingsGithubPage.tsx +413 -124
- package/src/pages/SettingsUsagePage.tsx +23 -1
- package/src/pages/media-grid.ts +14 -1
- package/src/pages/ui-kit/UiKitApp.tsx +164 -0
- package/src/pages/ui-kit/buttons.tsx +30 -0
- package/src/pages/ui-kit/catalog.tsx +41 -0
- package/src/pages/ui-kit/colors.tsx +95 -0
- package/src/pages/ui-kit/devbar.tsx +160 -0
- package/src/pages/ui-kit/feedback.tsx +39 -0
- package/src/pages/ui-kit/fields.tsx +47 -0
- package/src/pages/ui-kit/lists.tsx +19 -0
- package/src/pages/ui-kit/overlays.tsx +39 -0
- package/src/pages/ui-kit/path.ts +9 -0
- package/src/pages/ui-kit/sections.tsx +32 -0
- package/src/pages/ui-kit/surfaces.tsx +121 -0
- package/src/pages/ui-kit/topic.tsx +112 -0
- package/src/pages/ui-kit/topics.ts +71 -0
- package/src/pages/ui-kit/type.tsx +55 -0
- package/src/preview-frame-radius.ts +51 -0
- package/src/preview-frame.tsx +93 -49
- package/src/shell-resets.css +155 -0
- package/src/site-bar.tsx +1791 -425
- package/src/site-dashboard-href.ts +21 -0
- package/src/site-devbar-overlay-inject.ts +15 -0
- package/src/site-devbar-overlay.css +217 -0
- package/src/site-pointer.ts +1 -1
- package/src/site-publish-menu.tsx +33 -0
- package/src/site-widget-styles.ts +7 -0
- package/src/site-widget.tsx +61 -21
- package/src/studio-prose-ui.css +39 -0
- package/src/theme/ThemeProvider.tsx +4 -1
- package/src/workspace-path.ts +18 -1
- package/dist/chunks/EmbedPublishPage-88VsmOhi.js +0 -149
- package/dist/chunks/UiKitPage-X4hkFL2d.js +0 -173
- package/dist/chunks/WorkspaceRoutes-DXzdWqN3.js +0 -59523
- package/dist/chunks/status-bar-action-wj1KZ51m.js +0 -30612
- 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
|
+
}
|
package/src/preview-frame.tsx
CHANGED
|
@@ -1,38 +1,92 @@
|
|
|
1
1
|
// packages/studio/src/preview-frame.tsx
|
|
2
|
-
// Full-viewport
|
|
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
|
-
/**
|
|
6
|
-
export
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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:
|
|
112
|
+
@media (prefers-reduced-motion: reduce) {
|
|
71
113
|
.${PREVIEW_FRAME_CLASS} {
|
|
72
|
-
|
|
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
|
`;
|