{active != null && (
-
+
{/* Keyed so switching specimens starts each gallery fresh rather
than reconciling one into the next. */}
)}
+ {active?.code != null && (
+ // Keyed so the disclosure collapses again when switching specimens.
+
+
+
+ )}
{activeMock === "mock1" && (
{
{activeMock === "mock2" && (
{
)}
{activeMock === "mock3" && (
= {
@@ -27,6 +28,9 @@ export const LibrarySurface: FC<{
/** The components to show. */
children: ReactNode;
}> = ({ colorMode, children }) => {
+ // The playground's custom theme, applied so every specimen (including the
+ // Colors gallery's own swatches) reflects the edited palette live.
+ const theme = useCustomTheme();
const host = useRef(null);
const root = useRef(null);
// Attaching the shadow root is a DOM effect, so the first render has no root
@@ -53,7 +57,7 @@ export const LibrarySurface: FC<{
@@ -61,7 +65,7 @@ export const LibrarySurface: FC<{
>,
);
- }, [attached, colorMode, children]);
+ }, [attached, colorMode, children, theme]);
useEffect(
() => () => {
diff --git a/src/playground/designSystem/specimens.tsx b/src/playground/designSystem/specimens.tsx
index b5a4134..d52712d 100644
--- a/src/playground/designSystem/specimens.tsx
+++ b/src/playground/designSystem/specimens.tsx
@@ -1,5 +1,5 @@
import clsx from "clsx";
-import { type FC, type ReactNode, useState } from "react";
+import { type FC, type ReactNode, useEffect, useState } from "react";
import { Carousel } from "../../components/ui/Carousel";
import {
CustomCard,
@@ -15,11 +15,20 @@ import * as Icons from "../../components/ui/Icons";
import { type MessageStatus } from "../../interface";
import { LaunchButton } from "../../components/ui/LaunchButton";
import { Loader } from "../../components/ui/Loader";
-import { MessageButton } from "../../components/ui/MessageButton";
+import {
+ MessageButton,
+ type MessageButtonType,
+} from "../../components/ui/MessageButton";
import { MessageStatusRow } from "../../components/ui/MessageStatusRow";
-import { Radio } from "../../components/ui/Radio";
import { TextButton } from "../../components/ui/TextButton";
import { BaseText, SmallText } from "../../components/ui/Typography";
+import { defaultTheme } from "../../components/Theme";
+import {
+ customThemeStore,
+ type EditableColorKey,
+ isEditableColorKey,
+ useCustomTheme,
+} from "../customTheme";
/*
Everything in this file renders inside the library's shadow root (see
@@ -69,7 +78,7 @@ const CARD_IMAGE = `data:image/svg+xml;utf8,${encodeURIComponent(
const TextButtons: FC = () => (
<>
-
+ (
/>
-
+ (
Icon={Icons.ArrowForward}
/>
-
+ (
const ICON_BUTTON_TYPES: IconButtonType[] = [
"main",
"ghost",
- "sound",
+ "subtle",
"coverup",
"error",
];
@@ -114,7 +123,7 @@ const ICON_BUTTON_TYPES: IconButtonType[] = [
const IconButtons: FC = () => (
<>
{ICON_BUTTON_TYPES.map((type) => (
-
+ (
>
);
+const MESSAGE_BUTTON_TYPES: MessageButtonType[] = [
+ "default",
+ "selected",
+ "unselected",
+];
+
const MessageButtons: FC = () => (
<>
-
-
-
-
-
-
-
-
+ {MESSAGE_BUTTON_TYPES.map((type) => (
+
+
+
+
+ ))}
>
);
@@ -301,32 +309,6 @@ const DateInputs: FC = () => {
);
};
-const Radios: FC = () => {
- const [cabin, setCabin] = useState("economy");
- const options = [
- { value: "economy", label: "Economy" },
- { value: "premium", label: "Premium economy" },
- { value: "business", label: "Business" },
- ];
- return (
- <>
-
- {
- setCabin(String(value));
- }}
- />
-
-
-
-
- >
- );
-};
-
const Loaders: FC = () => (
<>
@@ -407,7 +389,6 @@ const COLOR_GROUPS: ColorGroup[] = [
colors: [
{ name: "accent", className: "bg-accent" },
{ name: "accent20", className: "bg-accent-20" },
- { name: "onAccent", className: "bg-on-accent" },
{ name: "background", className: "bg-background" },
{ name: "overlay", className: "bg-overlay" },
],
@@ -439,29 +420,307 @@ const ColorSwatch: FC = ({ name, className }) => (
- {group.label}
+/*
+ The editor UI below renders in the shadow root alongside the swatches, so it
+ can't rely on the playground's Tailwind theme. Rather than depend on which
+ utilities the *library* stylesheet happens to emit, it styles itself with
+ inline styles that read the theme's own CSS custom properties (--color-*,
+ --radius-inner) — the same variables ProviderStack sets — so the popup tracks
+ light/dark and the edited palette automatically.
+*/
+
+/** A subtle text button used inside the color editor and its header. */
+const EditorButton: FC<{
+ onClick: () => void;
+ disabled?: boolean;
+ children: ReactNode;
+}> = ({ onClick, disabled = false, children }) => (
+
+);
+
+/** Matches a `#rgb`/`#rrggbb` color the native picker can display. */
+const HEX_RE = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
+
+/** Popup with a color picker and a single CSS-color input for one color. */
+const ColorEditorPopup: FC<{
+ colorKey: EditableColorKey;
+ onClose: () => void;
+}> = ({ colorKey, onClose }) => {
+ const overrides = useCustomTheme();
+ const isEdited = colorKey in overrides;
+ const value = overrides[colorKey] ?? defaultTheme[colorKey];
+ // The native picker only understands hex; when the CSS color isn't one (e.g.
+ // `rebeccapurple`, `rgb(...)`, `light-dark(...)`) it falls back to the seed
+ // so it stays usable.
+ const pickerValue = HEX_RE.test(value.trim())
+ ? value.trim()
+ : defaultTheme[colorKey];
+
+ useEffect(() => {
+ const onKey = (event: KeyboardEvent): void => {
+ if (event.key === "Escape") {
+ onClose();
+ }
+ };
+ document.addEventListener("keydown", onKey);
+ return () => {
+ document.removeEventListener("keydown", onKey);
+ };
+ }, [onClose]);
+
+ return (
+ <>
+ {/* Click-away backdrop; covers the viewport so a click anywhere closes. */}
+
+