@slidev-react/client 0.2.5
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/CHANGELOG.md +42 -0
- package/LICENSE +21 -0
- package/README.md +16 -0
- package/package.json +44 -0
- package/src/addons/AddonProvider.tsx +25 -0
- package/src/addons/g2/G2Chart.tsx +370 -0
- package/src/addons/g2/chartPresets.ts +43 -0
- package/src/addons/g2/chartThemeTokens.ts +124 -0
- package/src/addons/g2/index.ts +36 -0
- package/src/addons/g2/style.css +31 -0
- package/src/addons/insight/Insight.tsx +10 -0
- package/src/addons/insight/InsightAddonProvider.tsx +20 -0
- package/src/addons/insight/SpotlightLayout.tsx +11 -0
- package/src/addons/insight/index.ts +17 -0
- package/src/addons/insight/style.css +34 -0
- package/src/addons/mermaid/MermaidDiagram.tsx +379 -0
- package/src/addons/mermaid/index.ts +10 -0
- package/src/addons/registry.test.ts +28 -0
- package/src/addons/registry.ts +61 -0
- package/src/addons/types.ts +6 -0
- package/src/app/App.tsx +125 -0
- package/src/app/README.md +18 -0
- package/src/app/providers/SlidesNavigationProvider.tsx +82 -0
- package/src/app/usePresentationBootstrap.ts +85 -0
- package/src/features/presentation/PresentationStatus.tsx +514 -0
- package/src/features/presentation/PrintSlidesView.tsx +350 -0
- package/src/features/presentation/browser.ts +5 -0
- package/src/features/presentation/draw/DrawOverlay.tsx +170 -0
- package/src/features/presentation/draw/DrawProvider.tsx +394 -0
- package/src/features/presentation/draw/persistence.test.ts +80 -0
- package/src/features/presentation/draw/persistence.ts +54 -0
- package/src/features/presentation/exportArtifacts.test.ts +48 -0
- package/src/features/presentation/exportArtifacts.ts +6 -0
- package/src/features/presentation/location.test.ts +73 -0
- package/src/features/presentation/location.ts +113 -0
- package/src/features/presentation/navigation/KeyboardController.tsx +73 -0
- package/src/features/presentation/navigation/PresentationNavbar.tsx +162 -0
- package/src/features/presentation/navigation/ShortcutsHelpOverlay.test.tsx +24 -0
- package/src/features/presentation/navigation/ShortcutsHelpOverlay.tsx +111 -0
- package/src/features/presentation/navigation/keyboardShortcuts.test.ts +74 -0
- package/src/features/presentation/navigation/keyboardShortcuts.ts +221 -0
- package/src/features/presentation/navigation/useSlidesNavigation.ts +15 -0
- package/src/features/presentation/overview/NotesOverview.tsx +200 -0
- package/src/features/presentation/overview/QuickOverview.tsx +126 -0
- package/src/features/presentation/path.ts +137 -0
- package/src/features/presentation/presenter/FlowTimelinePreview.test.tsx +54 -0
- package/src/features/presentation/presenter/FlowTimelinePreview.tsx +274 -0
- package/src/features/presentation/presenter/PresenterModeView.tsx +93 -0
- package/src/features/presentation/presenter/PresenterShell.tsx +286 -0
- package/src/features/presentation/presenter/PresenterSidePreview.tsx +68 -0
- package/src/features/presentation/presenter/PresenterTopProgress.tsx +28 -0
- package/src/features/presentation/presenter/SpeakerNotesPanel.tsx +51 -0
- package/src/features/presentation/presenter/StandaloneModeView.tsx +36 -0
- package/src/features/presentation/presenter/persistence.test.ts +26 -0
- package/src/features/presentation/presenter/persistence.ts +31 -0
- package/src/features/presentation/presenter/presentationSyncBridge.test.ts +87 -0
- package/src/features/presentation/presenter/presentationSyncBridge.ts +82 -0
- package/src/features/presentation/presenter/stage.ts +15 -0
- package/src/features/presentation/presenter/types.ts +30 -0
- package/src/features/presentation/presenter/useFullscreen.ts +58 -0
- package/src/features/presentation/presenter/useIdleCursor.ts +37 -0
- package/src/features/presentation/presenter/usePresentationFlowRuntime.ts +238 -0
- package/src/features/presentation/presenter/usePresenterChromeRuntime.ts +358 -0
- package/src/features/presentation/presenter/usePresenterSessionState.ts +226 -0
- package/src/features/presentation/presenter/useWakeLock.ts +110 -0
- package/src/features/presentation/recordingFilename.test.ts +46 -0
- package/src/features/presentation/recordingFilename.ts +56 -0
- package/src/features/presentation/reveal/Reveal.tsx +119 -0
- package/src/features/presentation/reveal/RevealContext.tsx +29 -0
- package/src/features/presentation/reveal/useRevealStep.ts +35 -0
- package/src/features/presentation/session.test.ts +122 -0
- package/src/features/presentation/session.ts +124 -0
- package/src/features/presentation/stage/SlidePreviewSurface.tsx +92 -0
- package/src/features/presentation/stage/SlideStage.tsx +159 -0
- package/src/features/presentation/stage/slideSurface.ts +71 -0
- package/src/features/presentation/stage/slideViewport.tsx +47 -0
- package/src/features/presentation/sync/adapters/broadcastChannelTransport.ts +40 -0
- package/src/features/presentation/sync/adapters/websocketTransport.ts +128 -0
- package/src/features/presentation/sync/model/presence.test.ts +42 -0
- package/src/features/presentation/sync/model/presence.ts +33 -0
- package/src/features/presentation/sync/model/replication.test.ts +72 -0
- package/src/features/presentation/sync/model/replication.ts +113 -0
- package/src/features/presentation/sync/model/status.test.ts +52 -0
- package/src/features/presentation/sync/model/status.ts +33 -0
- package/src/features/presentation/types.ts +1 -0
- package/src/features/presentation/usePresentationRecorder.ts +194 -0
- package/src/features/presentation/usePresentationSync.ts +423 -0
- package/src/index.ts +7 -0
- package/src/main.tsx +12 -0
- package/src/theme/ThemeProvider.test.ts +36 -0
- package/src/theme/ThemeProvider.tsx +79 -0
- package/src/theme/__mocks__/active-theme.ts +3 -0
- package/src/theme/base.css +14 -0
- package/src/theme/components.css +231 -0
- package/src/theme/index.css +11 -0
- package/src/theme/layouts/center.tsx +9 -0
- package/src/theme/layouts/cover.tsx +9 -0
- package/src/theme/layouts/default.tsx +5 -0
- package/src/theme/layouts/defaultLayouts.ts +20 -0
- package/src/theme/layouts/helpers.tsx +12 -0
- package/src/theme/layouts/image-right.tsx +21 -0
- package/src/theme/layouts/immersive.tsx +9 -0
- package/src/theme/layouts/resolveLayout.ts +9 -0
- package/src/theme/layouts/section.tsx +9 -0
- package/src/theme/layouts/statement.tsx +9 -0
- package/src/theme/layouts/two-cols.tsx +21 -0
- package/src/theme/layouts/types.ts +1 -0
- package/src/theme/layouts.css +133 -0
- package/src/theme/mark.css +379 -0
- package/src/theme/print.css +106 -0
- package/src/theme/prose.css +263 -0
- package/src/theme/registry.test.ts +21 -0
- package/src/theme/registry.ts +40 -0
- package/src/theme/tokens.css +148 -0
- package/src/theme/transitions.css +141 -0
- package/src/theme/types.ts +9 -0
- package/src/theme/useResolvedLayout.ts +24 -0
- package/src/types/generated-slides.d.ts +7 -0
- package/src/types/mdx-components.ts +7 -0
- package/src/types/plantuml-encoder.d.ts +7 -0
- package/src/ui/diagrams/PlantUmlDiagram.tsx +33 -0
- package/src/ui/mdx/MagicMoveDemo.tsx +114 -0
- package/src/ui/mdx/index.ts +21 -0
- package/src/ui/primitives/Annotate.test.tsx +64 -0
- package/src/ui/primitives/Annotate.tsx +82 -0
- package/src/ui/primitives/Badge.tsx +5 -0
- package/src/ui/primitives/Callout.tsx +24 -0
- package/src/ui/primitives/ChromeIconButton.tsx +58 -0
- package/src/ui/primitives/ChromePanel.tsx +79 -0
- package/src/ui/primitives/ChromeTag.tsx +70 -0
- package/src/ui/primitives/FormSelect.tsx +51 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { HighlighterCore } from "shiki";
|
|
2
|
+
import { useEffect, useMemo, useState } from "react";
|
|
3
|
+
import { createHighlighter } from "shiki";
|
|
4
|
+
import { ShikiMagicMove } from "shiki-magic-move/react";
|
|
5
|
+
|
|
6
|
+
const STEPS = [
|
|
7
|
+
`const message = 'Hello'
|
|
8
|
+
const target = 'world'
|
|
9
|
+
|
|
10
|
+
console.log(message, target)`,
|
|
11
|
+
`const message = 'Hi'
|
|
12
|
+
const target = user.name
|
|
13
|
+
|
|
14
|
+
console.log(\`${"${message}"}, ${"${target}"}!\`)`,
|
|
15
|
+
`function greet(target: string) {
|
|
16
|
+
const message = 'Hi'
|
|
17
|
+
|
|
18
|
+
return \`${"${message}"}, ${"${target}"}!\`
|
|
19
|
+
}`,
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
let highlighterPromise: Promise<HighlighterCore> | null = null;
|
|
23
|
+
|
|
24
|
+
function getHighlighter() {
|
|
25
|
+
if (!highlighterPromise) {
|
|
26
|
+
highlighterPromise = createHighlighter({
|
|
27
|
+
themes: ["vitesse-light"],
|
|
28
|
+
langs: ["javascript", "typescript"],
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return highlighterPromise;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function MagicMoveDemo() {
|
|
36
|
+
const [stepIndex, setStepIndex] = useState(0);
|
|
37
|
+
const [highlighter, setHighlighter] = useState<HighlighterCore>();
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
let cancelled = false;
|
|
41
|
+
|
|
42
|
+
const initializeHighlighter = async () => {
|
|
43
|
+
const instance = await getHighlighter();
|
|
44
|
+
if (!cancelled) setHighlighter(instance);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
void initializeHighlighter();
|
|
48
|
+
|
|
49
|
+
return () => {
|
|
50
|
+
cancelled = true;
|
|
51
|
+
};
|
|
52
|
+
}, []);
|
|
53
|
+
|
|
54
|
+
const code = useMemo(() => STEPS[stepIndex], [stepIndex]);
|
|
55
|
+
|
|
56
|
+
if (!highlighter) {
|
|
57
|
+
return (
|
|
58
|
+
<div className="rounded-xl border border-slate-300/70 bg-white/70 p-3 text-sm text-slate-700">
|
|
59
|
+
Preparing highlighter...
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<div className="magic-move-demo grid gap-4">
|
|
66
|
+
<div className="magic-move-demo-shell overflow-hidden rounded-xl border border-slate-200/80 bg-white/85 px-4 py-3 shadow-[inset_0_1px_0_rgba(255,255,255,0.7)]">
|
|
67
|
+
<ShikiMagicMove
|
|
68
|
+
lang="ts"
|
|
69
|
+
theme="vitesse-light"
|
|
70
|
+
highlighter={highlighter}
|
|
71
|
+
code={code}
|
|
72
|
+
className="magic-move-demo-pre"
|
|
73
|
+
options={{
|
|
74
|
+
duration: 750,
|
|
75
|
+
stagger: 3,
|
|
76
|
+
delayMove: 0,
|
|
77
|
+
delayEnter: 0,
|
|
78
|
+
delayLeave: 0,
|
|
79
|
+
lineNumbers: false,
|
|
80
|
+
splitTokens: false,
|
|
81
|
+
enhanceMatching: true,
|
|
82
|
+
animateContainer: false,
|
|
83
|
+
containerStyle: false,
|
|
84
|
+
}}
|
|
85
|
+
/>
|
|
86
|
+
</div>
|
|
87
|
+
<div className="flex flex-wrap gap-2">
|
|
88
|
+
<button
|
|
89
|
+
type="button"
|
|
90
|
+
className="rounded-lg bg-blue-600 px-3 py-1.5 text-sm text-white disabled:opacity-45"
|
|
91
|
+
onClick={() => setStepIndex((index) => Math.max(index - 1, 0))}
|
|
92
|
+
disabled={stepIndex === 0}
|
|
93
|
+
>
|
|
94
|
+
Prev Step
|
|
95
|
+
</button>
|
|
96
|
+
<button
|
|
97
|
+
type="button"
|
|
98
|
+
className="rounded-lg bg-blue-600 px-3 py-1.5 text-sm text-white disabled:opacity-45"
|
|
99
|
+
onClick={() => setStepIndex((index) => Math.min(index + 1, STEPS.length - 1))}
|
|
100
|
+
disabled={stepIndex >= STEPS.length - 1}
|
|
101
|
+
>
|
|
102
|
+
Next Step
|
|
103
|
+
</button>
|
|
104
|
+
<button
|
|
105
|
+
type="button"
|
|
106
|
+
className="rounded-lg bg-slate-600 px-3 py-1.5 text-sm text-white"
|
|
107
|
+
onClick={() => setStepIndex(0)}
|
|
108
|
+
>
|
|
109
|
+
Reset
|
|
110
|
+
</button>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { PlantUmlDiagram } from "../diagrams/PlantUmlDiagram"
|
|
2
|
+
import { Annotate } from "../primitives/Annotate"
|
|
3
|
+
import { Badge } from "../primitives/Badge"
|
|
4
|
+
import { Callout } from "../primitives/Callout"
|
|
5
|
+
import { MagicMoveDemo } from "./MagicMoveDemo"
|
|
6
|
+
import { Reveal, RevealGroup } from "../../features/presentation/reveal/Reveal"
|
|
7
|
+
import { CourseCover } from "../../../../../components/CourseCover"
|
|
8
|
+
import { MinimaxReactVisualizer } from "../../../../../components/MinimaxReactVisualizer"
|
|
9
|
+
|
|
10
|
+
export const mdxComponents = {
|
|
11
|
+
Badge,
|
|
12
|
+
Callout,
|
|
13
|
+
MagicMoveDemo,
|
|
14
|
+
Annotate,
|
|
15
|
+
PlantUmlDiagram,
|
|
16
|
+
Reveal,
|
|
17
|
+
RevealGroup,
|
|
18
|
+
CourseCover,
|
|
19
|
+
MinimaxReactVisualizer,
|
|
20
|
+
}
|
|
21
|
+
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from "react-dom/server";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import {
|
|
4
|
+
RevealProvider,
|
|
5
|
+
type RevealContextValue,
|
|
6
|
+
} from "../../features/presentation/reveal/RevealContext";
|
|
7
|
+
import { Annotate } from "./Annotate";
|
|
8
|
+
|
|
9
|
+
function createRevealValue(clicks: number): RevealContextValue {
|
|
10
|
+
return {
|
|
11
|
+
slideId: "slide-annotation",
|
|
12
|
+
clicks,
|
|
13
|
+
clicksTotal: 2,
|
|
14
|
+
setClicks: vi.fn(),
|
|
15
|
+
registerStep: vi.fn(() => () => {}),
|
|
16
|
+
advance: vi.fn(),
|
|
17
|
+
retreat: vi.fn(),
|
|
18
|
+
canAdvance: true,
|
|
19
|
+
canRetreat: clicks > 0,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("Annotate", () => {
|
|
24
|
+
it("keeps text visible before its reveal step", () => {
|
|
25
|
+
const html = renderToStaticMarkup(
|
|
26
|
+
<RevealProvider value={createRevealValue(0)}>
|
|
27
|
+
<Annotate type="underline" step={1}>
|
|
28
|
+
reveal copy
|
|
29
|
+
</Annotate>
|
|
30
|
+
</RevealProvider>,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
expect(html).toContain("reveal copy");
|
|
34
|
+
expect(html).not.toContain("slide-mark-overlay");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("renders an animated mark after the reveal step is reached", () => {
|
|
38
|
+
const html = renderToStaticMarkup(
|
|
39
|
+
<RevealProvider value={createRevealValue(1)}>
|
|
40
|
+
<Annotate type="underline" step={1}>
|
|
41
|
+
reveal copy
|
|
42
|
+
</Annotate>
|
|
43
|
+
</RevealProvider>,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
expect(html).toContain("slide-mark--underline");
|
|
47
|
+
expect(html).toContain("slide-mark--animate");
|
|
48
|
+
expect(html).toContain("slide-mark-overlay");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("can reveal instantly without playback animation", () => {
|
|
52
|
+
const html = renderToStaticMarkup(
|
|
53
|
+
<RevealProvider value={createRevealValue(1)}>
|
|
54
|
+
<Annotate type="box" step={1} animate={false}>
|
|
55
|
+
instant reveal
|
|
56
|
+
</Annotate>
|
|
57
|
+
</RevealProvider>,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
expect(html).toContain("slide-mark--box");
|
|
61
|
+
expect(html).not.toContain("slide-mark--animate");
|
|
62
|
+
expect(html).toContain("slide-mark-overlay");
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { type CSSProperties, type ReactNode } from "react";
|
|
2
|
+
import { useRevealStep } from "../../features/presentation/reveal/useRevealStep";
|
|
3
|
+
|
|
4
|
+
type AnnotateType = "underline" | "box" | "circle" | "highlight" | "strike-through" | "crossed-off";
|
|
5
|
+
|
|
6
|
+
export type AnnotateProps = {
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
type?: AnnotateType;
|
|
9
|
+
step?: number;
|
|
10
|
+
animate?: boolean;
|
|
11
|
+
color?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const DEFAULT_ANIMATION_DURATION_MS = 520;
|
|
15
|
+
|
|
16
|
+
const defaultColorByType: Record<AnnotateType, string> = {
|
|
17
|
+
underline: "#16a34a",
|
|
18
|
+
box: "#16a34a",
|
|
19
|
+
circle: "#16a34a",
|
|
20
|
+
highlight: "rgba(250, 204, 21, 0.78)",
|
|
21
|
+
"strike-through": "#ef4444",
|
|
22
|
+
"crossed-off": "#ef4444",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const defaultPaddingByType: Record<AnnotateType, [number, number, number, number]> = {
|
|
26
|
+
underline: [0, 2, 2, 2],
|
|
27
|
+
box: [2, 5, 2, 5],
|
|
28
|
+
circle: [3, 7, 3, 7],
|
|
29
|
+
highlight: [1, 3, 1, 3],
|
|
30
|
+
"strike-through": [0, 2, 0, 2],
|
|
31
|
+
"crossed-off": [1, 3, 1, 3],
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const defaultStrokeWidthByType: Record<AnnotateType, number> = {
|
|
35
|
+
underline: 2.4,
|
|
36
|
+
box: 2.2,
|
|
37
|
+
circle: 2.2,
|
|
38
|
+
highlight: 0,
|
|
39
|
+
"strike-through": 2.2,
|
|
40
|
+
"crossed-off": 2.2,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const joinClassNames = (...names: Array<string | false>) => {
|
|
44
|
+
return names.filter(Boolean).join(" ");
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export function Annotate({
|
|
48
|
+
children,
|
|
49
|
+
type = "highlight",
|
|
50
|
+
step,
|
|
51
|
+
animate = step !== undefined,
|
|
52
|
+
color,
|
|
53
|
+
}: AnnotateProps) {
|
|
54
|
+
const { isVisible } = useRevealStep(step);
|
|
55
|
+
|
|
56
|
+
const [padTop, padRight, padBottom, padLeft] = defaultPaddingByType[type];
|
|
57
|
+
const shouldRenderMark = isVisible;
|
|
58
|
+
const style = {
|
|
59
|
+
"--mark-color": color ?? defaultColorByType[type],
|
|
60
|
+
"--mark-stroke-width": `${defaultStrokeWidthByType[type]}px`,
|
|
61
|
+
"--mark-pad-top": `${padTop}px`,
|
|
62
|
+
"--mark-pad-right": `${padRight}px`,
|
|
63
|
+
"--mark-pad-bottom": `${padBottom}px`,
|
|
64
|
+
"--mark-pad-left": `${padLeft}px`,
|
|
65
|
+
"--mark-animation-duration": `${DEFAULT_ANIMATION_DURATION_MS}ms`,
|
|
66
|
+
"--mark-animation-iterations": "1",
|
|
67
|
+
} as CSSProperties;
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<span
|
|
71
|
+
className={joinClassNames(
|
|
72
|
+
"slide-mark",
|
|
73
|
+
`slide-mark--${type}`,
|
|
74
|
+
shouldRenderMark && animate && "slide-mark--animate",
|
|
75
|
+
)}
|
|
76
|
+
style={style}
|
|
77
|
+
>
|
|
78
|
+
<span className="slide-mark-target">{children}</span>
|
|
79
|
+
{shouldRenderMark ? <span aria-hidden className="slide-mark-overlay" /> : null}
|
|
80
|
+
</span>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
const stylesByType = {
|
|
4
|
+
info: "border-green-600/80 bg-green-50 text-green-950",
|
|
5
|
+
warn: "border-amber-600/80 bg-amber-50 text-amber-950",
|
|
6
|
+
success: "border-emerald-600/80 bg-emerald-50 text-emerald-950",
|
|
7
|
+
} as const;
|
|
8
|
+
|
|
9
|
+
export function Callout({
|
|
10
|
+
type = "info",
|
|
11
|
+
title,
|
|
12
|
+
children,
|
|
13
|
+
}: {
|
|
14
|
+
type?: "info" | "warn" | "success";
|
|
15
|
+
title?: string;
|
|
16
|
+
children: ReactNode;
|
|
17
|
+
}) {
|
|
18
|
+
return (
|
|
19
|
+
<aside className={`my-4 rounded-xl border-l-4 px-3.5 py-3 ${stylesByType[type]}`}>
|
|
20
|
+
{title ? <strong className="mb-1 block">{title}</strong> : null}
|
|
21
|
+
<div>{children}</div>
|
|
22
|
+
</aside>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { ButtonHTMLAttributes, ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
function joinClassNames(...classNames: Array<string | false | null | undefined>) {
|
|
4
|
+
return classNames.filter(Boolean).join(" ");
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const toneClassNames = {
|
|
8
|
+
default:
|
|
9
|
+
"border-slate-200/80 bg-white/88 text-slate-700 hover:bg-white disabled:cursor-not-allowed disabled:opacity-45",
|
|
10
|
+
active: "border-emerald-200/80 bg-emerald-50 text-emerald-700",
|
|
11
|
+
danger:
|
|
12
|
+
"border-rose-300/80 bg-rose-50 text-rose-700 hover:bg-rose-100 disabled:cursor-not-allowed disabled:opacity-45",
|
|
13
|
+
violet:
|
|
14
|
+
"border-violet-300/80 bg-violet-50 text-violet-700 hover:bg-violet-100 disabled:cursor-not-allowed disabled:opacity-45",
|
|
15
|
+
success:
|
|
16
|
+
"border-emerald-300/80 bg-emerald-50 text-emerald-700 hover:bg-emerald-100 disabled:cursor-not-allowed disabled:opacity-45",
|
|
17
|
+
info: "border-green-300/80 bg-green-50 text-green-700 hover:bg-green-100 disabled:cursor-not-allowed disabled:opacity-45",
|
|
18
|
+
} as const;
|
|
19
|
+
|
|
20
|
+
const sizeClassNames = {
|
|
21
|
+
sm: "size-8",
|
|
22
|
+
md: "size-9",
|
|
23
|
+
} as const;
|
|
24
|
+
|
|
25
|
+
const radiusClassNames = {
|
|
26
|
+
soft: "rounded-md",
|
|
27
|
+
chrome: "rounded-md",
|
|
28
|
+
} as const;
|
|
29
|
+
|
|
30
|
+
export function ChromeIconButton({
|
|
31
|
+
children,
|
|
32
|
+
className,
|
|
33
|
+
tone = "default",
|
|
34
|
+
size = "md",
|
|
35
|
+
radius = "chrome",
|
|
36
|
+
...props
|
|
37
|
+
}: ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
38
|
+
children: ReactNode;
|
|
39
|
+
tone?: keyof typeof toneClassNames;
|
|
40
|
+
size?: keyof typeof sizeClassNames;
|
|
41
|
+
radius?: keyof typeof radiusClassNames;
|
|
42
|
+
}) {
|
|
43
|
+
return (
|
|
44
|
+
<button
|
|
45
|
+
{...props}
|
|
46
|
+
type={props.type ?? "button"}
|
|
47
|
+
className={joinClassNames(
|
|
48
|
+
"inline-flex shrink-0 items-center justify-center border transition",
|
|
49
|
+
toneClassNames[tone],
|
|
50
|
+
sizeClassNames[size],
|
|
51
|
+
radiusClassNames[radius],
|
|
52
|
+
className,
|
|
53
|
+
)}
|
|
54
|
+
>
|
|
55
|
+
{children}
|
|
56
|
+
</button>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef, ElementType, ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
function joinClassNames(...classNames: Array<string | false | null | undefined>) {
|
|
4
|
+
return classNames.filter(Boolean).join(" ");
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const toneClassNames = {
|
|
8
|
+
glass:
|
|
9
|
+
"border-slate-200/80 bg-white/88 text-slate-900",
|
|
10
|
+
solid:
|
|
11
|
+
"border-slate-200/80 bg-white/92 text-slate-900",
|
|
12
|
+
inset: "border-slate-200/80 bg-slate-50/78 text-slate-600",
|
|
13
|
+
frame: "border-slate-200/80 bg-white",
|
|
14
|
+
dashed: "border-slate-200/80 bg-slate-50/75 text-slate-500",
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
const radiusClassNames = {
|
|
18
|
+
panel: "rounded-md",
|
|
19
|
+
section: "rounded-lg",
|
|
20
|
+
inset: "rounded-md",
|
|
21
|
+
frame: "rounded-md",
|
|
22
|
+
} as const;
|
|
23
|
+
|
|
24
|
+
const paddingClassNames = {
|
|
25
|
+
none: "",
|
|
26
|
+
sm: "p-3",
|
|
27
|
+
md: "p-4",
|
|
28
|
+
lg: "p-5",
|
|
29
|
+
} as const;
|
|
30
|
+
|
|
31
|
+
export function chromePanelClassName({
|
|
32
|
+
className,
|
|
33
|
+
tone = "glass",
|
|
34
|
+
radius = "panel",
|
|
35
|
+
padding = "md",
|
|
36
|
+
}: {
|
|
37
|
+
className?: string;
|
|
38
|
+
tone?: keyof typeof toneClassNames;
|
|
39
|
+
radius?: keyof typeof radiusClassNames;
|
|
40
|
+
padding?: keyof typeof paddingClassNames;
|
|
41
|
+
}) {
|
|
42
|
+
return joinClassNames(
|
|
43
|
+
"min-h-0 min-w-0",
|
|
44
|
+
toneClassNames[tone],
|
|
45
|
+
radiusClassNames[radius],
|
|
46
|
+
paddingClassNames[padding],
|
|
47
|
+
className,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type ChromePanelProps<T extends ElementType> = {
|
|
52
|
+
as?: T;
|
|
53
|
+
children: ReactNode;
|
|
54
|
+
className?: string;
|
|
55
|
+
tone?: keyof typeof toneClassNames;
|
|
56
|
+
radius?: keyof typeof radiusClassNames;
|
|
57
|
+
padding?: keyof typeof paddingClassNames;
|
|
58
|
+
} & Omit<ComponentPropsWithoutRef<T>, "as" | "children" | "className">;
|
|
59
|
+
|
|
60
|
+
export function ChromePanel<T extends ElementType = "section">({
|
|
61
|
+
as,
|
|
62
|
+
children,
|
|
63
|
+
className,
|
|
64
|
+
tone = "glass",
|
|
65
|
+
radius = "panel",
|
|
66
|
+
padding = "md",
|
|
67
|
+
...props
|
|
68
|
+
}: ChromePanelProps<T>) {
|
|
69
|
+
const Component = as ?? "section";
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<Component
|
|
73
|
+
{...props}
|
|
74
|
+
className={chromePanelClassName({ className, tone, radius, padding })}
|
|
75
|
+
>
|
|
76
|
+
{children}
|
|
77
|
+
</Component>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { ElementType, ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
function joinClassNames(...classNames: Array<string | false | null | undefined>) {
|
|
4
|
+
return classNames.filter(Boolean).join(" ");
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const toneClassNames = {
|
|
8
|
+
default: "border-slate-200/80 bg-white/88 text-slate-500",
|
|
9
|
+
defaultStrong: "border-slate-200/80 bg-white/88 text-slate-800",
|
|
10
|
+
muted: "border-slate-200/80 bg-white/82 text-slate-600",
|
|
11
|
+
active: "border-emerald-200/80 bg-emerald-50 text-emerald-700",
|
|
12
|
+
success: "border-emerald-200/80 bg-emerald-50 text-emerald-700",
|
|
13
|
+
warning: "border-amber-200/80 bg-amber-50 text-amber-700",
|
|
14
|
+
danger: "border-rose-300/80 bg-rose-50 text-rose-700",
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
const sizeClassNames = {
|
|
18
|
+
xs: "px-2 py-0.5 text-[10px]",
|
|
19
|
+
sm: "px-2.5 py-1 text-[11px]",
|
|
20
|
+
md: "px-3 py-1.5 text-xs",
|
|
21
|
+
} as const;
|
|
22
|
+
|
|
23
|
+
const weightClassNames = {
|
|
24
|
+
medium: "font-medium",
|
|
25
|
+
semibold: "font-semibold",
|
|
26
|
+
} as const;
|
|
27
|
+
|
|
28
|
+
export function chromeTagClassName({
|
|
29
|
+
className,
|
|
30
|
+
tone = "default",
|
|
31
|
+
size = "sm",
|
|
32
|
+
weight = "medium",
|
|
33
|
+
}: {
|
|
34
|
+
className?: string;
|
|
35
|
+
tone?: keyof typeof toneClassNames;
|
|
36
|
+
size?: keyof typeof sizeClassNames;
|
|
37
|
+
weight?: keyof typeof weightClassNames;
|
|
38
|
+
}) {
|
|
39
|
+
return joinClassNames(
|
|
40
|
+
"inline-flex items-center gap-2 rounded-md border",
|
|
41
|
+
toneClassNames[tone],
|
|
42
|
+
sizeClassNames[size],
|
|
43
|
+
weightClassNames[weight],
|
|
44
|
+
className,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function ChromeTag({
|
|
49
|
+
as,
|
|
50
|
+
children,
|
|
51
|
+
className,
|
|
52
|
+
tone = "default",
|
|
53
|
+
size = "sm",
|
|
54
|
+
weight = "medium",
|
|
55
|
+
}: {
|
|
56
|
+
as?: ElementType;
|
|
57
|
+
children: ReactNode;
|
|
58
|
+
className?: string;
|
|
59
|
+
tone?: keyof typeof toneClassNames;
|
|
60
|
+
size?: keyof typeof sizeClassNames;
|
|
61
|
+
weight?: keyof typeof weightClassNames;
|
|
62
|
+
}) {
|
|
63
|
+
const Component = as ?? "span";
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<Component className={chromeTagClassName({ className, tone, size, weight })}>
|
|
67
|
+
{children}
|
|
68
|
+
</Component>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ReactNode, SelectHTMLAttributes } from "react";
|
|
2
|
+
|
|
3
|
+
function joinClassNames(...classNames: Array<string | false | null | undefined>) {
|
|
4
|
+
return classNames.filter(Boolean).join(" ");
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
interface FormSelectProps extends Omit<SelectHTMLAttributes<HTMLSelectElement>, "children" | "size"> {
|
|
8
|
+
label: ReactNode;
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
size?: "sm" | "md";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const sizeStyles = {
|
|
14
|
+
sm: {
|
|
15
|
+
container: "px-3 py-1.5 gap-2",
|
|
16
|
+
select: "px-2.5 py-1",
|
|
17
|
+
},
|
|
18
|
+
md: {
|
|
19
|
+
container: "px-3 py-2 gap-2",
|
|
20
|
+
select: "px-2.5 py-1",
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function FormSelect({
|
|
25
|
+
label,
|
|
26
|
+
children,
|
|
27
|
+
size = "sm",
|
|
28
|
+
className,
|
|
29
|
+
...props
|
|
30
|
+
}: FormSelectProps) {
|
|
31
|
+
return (
|
|
32
|
+
<label
|
|
33
|
+
className={joinClassNames(
|
|
34
|
+
"inline-flex items-center rounded-md border border-slate-200/80 bg-white/88 text-xs font-medium text-slate-700",
|
|
35
|
+
sizeStyles[size].container,
|
|
36
|
+
className,
|
|
37
|
+
)}
|
|
38
|
+
>
|
|
39
|
+
{label}
|
|
40
|
+
<select
|
|
41
|
+
{...props}
|
|
42
|
+
className={joinClassNames(
|
|
43
|
+
"rounded-md border border-slate-200/80 bg-white text-xs text-slate-700 outline-none",
|
|
44
|
+
sizeStyles[size].select,
|
|
45
|
+
)}
|
|
46
|
+
>
|
|
47
|
+
{children}
|
|
48
|
+
</select>
|
|
49
|
+
</label>
|
|
50
|
+
);
|
|
51
|
+
}
|