@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.
Files changed (131) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/LICENSE +21 -0
  3. package/README.md +16 -0
  4. package/package.json +44 -0
  5. package/src/addons/AddonProvider.tsx +25 -0
  6. package/src/addons/g2/G2Chart.tsx +370 -0
  7. package/src/addons/g2/chartPresets.ts +43 -0
  8. package/src/addons/g2/chartThemeTokens.ts +124 -0
  9. package/src/addons/g2/index.ts +36 -0
  10. package/src/addons/g2/style.css +31 -0
  11. package/src/addons/insight/Insight.tsx +10 -0
  12. package/src/addons/insight/InsightAddonProvider.tsx +20 -0
  13. package/src/addons/insight/SpotlightLayout.tsx +11 -0
  14. package/src/addons/insight/index.ts +17 -0
  15. package/src/addons/insight/style.css +34 -0
  16. package/src/addons/mermaid/MermaidDiagram.tsx +379 -0
  17. package/src/addons/mermaid/index.ts +10 -0
  18. package/src/addons/registry.test.ts +28 -0
  19. package/src/addons/registry.ts +61 -0
  20. package/src/addons/types.ts +6 -0
  21. package/src/app/App.tsx +125 -0
  22. package/src/app/README.md +18 -0
  23. package/src/app/providers/SlidesNavigationProvider.tsx +82 -0
  24. package/src/app/usePresentationBootstrap.ts +85 -0
  25. package/src/features/presentation/PresentationStatus.tsx +514 -0
  26. package/src/features/presentation/PrintSlidesView.tsx +350 -0
  27. package/src/features/presentation/browser.ts +5 -0
  28. package/src/features/presentation/draw/DrawOverlay.tsx +170 -0
  29. package/src/features/presentation/draw/DrawProvider.tsx +394 -0
  30. package/src/features/presentation/draw/persistence.test.ts +80 -0
  31. package/src/features/presentation/draw/persistence.ts +54 -0
  32. package/src/features/presentation/exportArtifacts.test.ts +48 -0
  33. package/src/features/presentation/exportArtifacts.ts +6 -0
  34. package/src/features/presentation/location.test.ts +73 -0
  35. package/src/features/presentation/location.ts +113 -0
  36. package/src/features/presentation/navigation/KeyboardController.tsx +73 -0
  37. package/src/features/presentation/navigation/PresentationNavbar.tsx +162 -0
  38. package/src/features/presentation/navigation/ShortcutsHelpOverlay.test.tsx +24 -0
  39. package/src/features/presentation/navigation/ShortcutsHelpOverlay.tsx +111 -0
  40. package/src/features/presentation/navigation/keyboardShortcuts.test.ts +74 -0
  41. package/src/features/presentation/navigation/keyboardShortcuts.ts +221 -0
  42. package/src/features/presentation/navigation/useSlidesNavigation.ts +15 -0
  43. package/src/features/presentation/overview/NotesOverview.tsx +200 -0
  44. package/src/features/presentation/overview/QuickOverview.tsx +126 -0
  45. package/src/features/presentation/path.ts +137 -0
  46. package/src/features/presentation/presenter/FlowTimelinePreview.test.tsx +54 -0
  47. package/src/features/presentation/presenter/FlowTimelinePreview.tsx +274 -0
  48. package/src/features/presentation/presenter/PresenterModeView.tsx +93 -0
  49. package/src/features/presentation/presenter/PresenterShell.tsx +286 -0
  50. package/src/features/presentation/presenter/PresenterSidePreview.tsx +68 -0
  51. package/src/features/presentation/presenter/PresenterTopProgress.tsx +28 -0
  52. package/src/features/presentation/presenter/SpeakerNotesPanel.tsx +51 -0
  53. package/src/features/presentation/presenter/StandaloneModeView.tsx +36 -0
  54. package/src/features/presentation/presenter/persistence.test.ts +26 -0
  55. package/src/features/presentation/presenter/persistence.ts +31 -0
  56. package/src/features/presentation/presenter/presentationSyncBridge.test.ts +87 -0
  57. package/src/features/presentation/presenter/presentationSyncBridge.ts +82 -0
  58. package/src/features/presentation/presenter/stage.ts +15 -0
  59. package/src/features/presentation/presenter/types.ts +30 -0
  60. package/src/features/presentation/presenter/useFullscreen.ts +58 -0
  61. package/src/features/presentation/presenter/useIdleCursor.ts +37 -0
  62. package/src/features/presentation/presenter/usePresentationFlowRuntime.ts +238 -0
  63. package/src/features/presentation/presenter/usePresenterChromeRuntime.ts +358 -0
  64. package/src/features/presentation/presenter/usePresenterSessionState.ts +226 -0
  65. package/src/features/presentation/presenter/useWakeLock.ts +110 -0
  66. package/src/features/presentation/recordingFilename.test.ts +46 -0
  67. package/src/features/presentation/recordingFilename.ts +56 -0
  68. package/src/features/presentation/reveal/Reveal.tsx +119 -0
  69. package/src/features/presentation/reveal/RevealContext.tsx +29 -0
  70. package/src/features/presentation/reveal/useRevealStep.ts +35 -0
  71. package/src/features/presentation/session.test.ts +122 -0
  72. package/src/features/presentation/session.ts +124 -0
  73. package/src/features/presentation/stage/SlidePreviewSurface.tsx +92 -0
  74. package/src/features/presentation/stage/SlideStage.tsx +159 -0
  75. package/src/features/presentation/stage/slideSurface.ts +71 -0
  76. package/src/features/presentation/stage/slideViewport.tsx +47 -0
  77. package/src/features/presentation/sync/adapters/broadcastChannelTransport.ts +40 -0
  78. package/src/features/presentation/sync/adapters/websocketTransport.ts +128 -0
  79. package/src/features/presentation/sync/model/presence.test.ts +42 -0
  80. package/src/features/presentation/sync/model/presence.ts +33 -0
  81. package/src/features/presentation/sync/model/replication.test.ts +72 -0
  82. package/src/features/presentation/sync/model/replication.ts +113 -0
  83. package/src/features/presentation/sync/model/status.test.ts +52 -0
  84. package/src/features/presentation/sync/model/status.ts +33 -0
  85. package/src/features/presentation/types.ts +1 -0
  86. package/src/features/presentation/usePresentationRecorder.ts +194 -0
  87. package/src/features/presentation/usePresentationSync.ts +423 -0
  88. package/src/index.ts +7 -0
  89. package/src/main.tsx +12 -0
  90. package/src/theme/ThemeProvider.test.ts +36 -0
  91. package/src/theme/ThemeProvider.tsx +79 -0
  92. package/src/theme/__mocks__/active-theme.ts +3 -0
  93. package/src/theme/base.css +14 -0
  94. package/src/theme/components.css +231 -0
  95. package/src/theme/index.css +11 -0
  96. package/src/theme/layouts/center.tsx +9 -0
  97. package/src/theme/layouts/cover.tsx +9 -0
  98. package/src/theme/layouts/default.tsx +5 -0
  99. package/src/theme/layouts/defaultLayouts.ts +20 -0
  100. package/src/theme/layouts/helpers.tsx +12 -0
  101. package/src/theme/layouts/image-right.tsx +21 -0
  102. package/src/theme/layouts/immersive.tsx +9 -0
  103. package/src/theme/layouts/resolveLayout.ts +9 -0
  104. package/src/theme/layouts/section.tsx +9 -0
  105. package/src/theme/layouts/statement.tsx +9 -0
  106. package/src/theme/layouts/two-cols.tsx +21 -0
  107. package/src/theme/layouts/types.ts +1 -0
  108. package/src/theme/layouts.css +133 -0
  109. package/src/theme/mark.css +379 -0
  110. package/src/theme/print.css +106 -0
  111. package/src/theme/prose.css +263 -0
  112. package/src/theme/registry.test.ts +21 -0
  113. package/src/theme/registry.ts +40 -0
  114. package/src/theme/tokens.css +148 -0
  115. package/src/theme/transitions.css +141 -0
  116. package/src/theme/types.ts +9 -0
  117. package/src/theme/useResolvedLayout.ts +24 -0
  118. package/src/types/generated-slides.d.ts +7 -0
  119. package/src/types/mdx-components.ts +7 -0
  120. package/src/types/plantuml-encoder.d.ts +7 -0
  121. package/src/ui/diagrams/PlantUmlDiagram.tsx +33 -0
  122. package/src/ui/mdx/MagicMoveDemo.tsx +114 -0
  123. package/src/ui/mdx/index.ts +21 -0
  124. package/src/ui/primitives/Annotate.test.tsx +64 -0
  125. package/src/ui/primitives/Annotate.tsx +82 -0
  126. package/src/ui/primitives/Badge.tsx +5 -0
  127. package/src/ui/primitives/Callout.tsx +24 -0
  128. package/src/ui/primitives/ChromeIconButton.tsx +58 -0
  129. package/src/ui/primitives/ChromePanel.tsx +79 -0
  130. package/src/ui/primitives/ChromeTag.tsx +70 -0
  131. 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,5 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ export function Badge({ children }: { children: ReactNode }) {
4
+ return <span className="slide-badge">{children}</span>;
5
+ }
@@ -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
+ }