From 6cc27b891d65d9ec3a11ecd2928ef099c54439ce Mon Sep 17 00:00:00 2001 From: kev1n77 Date: Sun, 30 Aug 2026 12:01:27 +0800 Subject: [PATCH 1/3] fix(design-lab): separate wide previews and constrain canvas overflow --- .../src/pages/ComponentDetailPage.tsx | 15 +++++ design-system/apps/design-lab/src/styles.css | 60 +++++++++++++++---- .../vite/component-detail-contract.test.mjs | 20 +++++-- 3 files changed, 81 insertions(+), 14 deletions(-) diff --git a/design-system/apps/design-lab/src/pages/ComponentDetailPage.tsx b/design-system/apps/design-lab/src/pages/ComponentDetailPage.tsx index 860c92d9dc..ce18043390 100644 --- a/design-system/apps/design-lab/src/pages/ComponentDetailPage.tsx +++ b/design-system/apps/design-lab/src/pages/ComponentDetailPage.tsx @@ -1531,6 +1531,9 @@ export function ComponentDetailPage({ contrast={contrast} density={density} tokenOverrides={tokenOverrides} + tabIndex={0} + role="region" + aria-label={t("detail.preview")} > {component.name === "ConfirmDialog" ? (
@@ -1582,6 +1585,18 @@ export function ComponentDetailPage({ {t(optionLabelKeys[previewState] ?? "detail.option.default")} {renderPreview(previewState)}
+ ) : component.name === "Menu" || component.name === "NavigationPanel" ? ( +
+ {states.map((state) => ( +
+
+ {t(optionLabelKeys[state] ?? "detail.option.default")} + {state} +
+
{renderPreview(state)}
+
+ ))} +
) : isFlowChatComponent ? (
[data-bf-component="combobox"] { + inline-size: min(100%, 380px); +} + +.component-combobox-preview > span { + color: var(--bf-color-content-muted); + font-size: var(--bf-font-size-small); +} + +.component-menu-interaction { padding: var(--bf-space-6); } + +.component-surface-state-list { + display: grid; + gap: var(--bf-space-6); + padding: var(--bf-space-6); + min-inline-size: 0; +} + +.component-surface-state-list__item { + display: grid; + gap: var(--bf-space-4); + min-inline-size: 0; + padding-block-end: var(--bf-space-6); + border-block-end: var(--bf-border-width-default) solid var(--bf-color-border-subtle); +} + +.component-surface-state-list__preview { + min-inline-size: 0; + overflow: auto; + padding: var(--bf-space-4); +} + +.component-surface-state-list [data-bf-component="menu"] { + inline-size: var(--bf-overlay-menu-inline-size); + max-block-size: var(--bf-overlay-menu-max-block-size); +} /* Component reference page */ .lab-page--component-detail { @@ -3485,7 +3520,7 @@ input.lab-force-focus { display: grid; grid-template-columns: minmax(0, 1fr) 380px; align-items: start; - gap: 70px; + gap: 24px; } .component-preview-main { @@ -3917,6 +3952,7 @@ input.lab-force-focus { align-items: stretch; min-height: 0; padding: 14px 24px 24px; + column-gap: var(--bf-space-4); } .component-preview-matrix[data-state-count="1"] { @@ -3926,25 +3962,25 @@ input.lab-force-focus { } .component-preview-matrix[data-state-count="3"] { - grid-template-columns: 96px repeat(3, minmax(244px, 1fr)); + grid-template-columns: 96px repeat(3, minmax(280px, max-content)); grid-template-rows: 50px minmax(190px, 1fr); min-width: 828px; } .component-preview-matrix[data-state-count="4"] { - grid-template-columns: 96px repeat(4, minmax(124px, 1fr)); + grid-template-columns: 96px repeat(4, minmax(280px, max-content)); grid-template-rows: 50px minmax(190px, 1fr); min-width: 640px; } .component-preview-matrix[data-state-count="5"] { - grid-template-columns: 96px repeat(5, minmax(144px, 1fr)); + grid-template-columns: 96px repeat(5, minmax(280px, max-content)); grid-template-rows: 50px minmax(190px, 1fr); min-width: 816px; } .component-preview-matrix[data-state-count="6"] { - grid-template-columns: 96px repeat(6, minmax(260px, 1fr)); + grid-template-columns: 96px repeat(6, minmax(280px, max-content)); grid-template-rows: 50px minmax(190px, 1fr); min-width: 1656px; } @@ -4072,7 +4108,7 @@ input.lab-force-focus { .component-preview-matrix__cell { display: grid; - min-width: 0; + min-width: max-content; place-items: center; padding: var(--bf-space-3); background: transparent; @@ -4250,6 +4286,10 @@ input.lab-force-focus { border-radius: 0; color: var(--bf-color-content-primary); background: transparent; + min-inline-size: 0; + max-block-size: 480px; + overflow: auto; + padding: var(--bf-space-3); } .component-inspector-preview [data-bf-component="button"] { @@ -4377,7 +4417,7 @@ input.lab-force-focus { .component-preview-layout { grid-template-columns: minmax(0, 1fr) 300px; - gap: 52px; + gap: 24px; } .component-preview-matrix[data-component="button"] [data-bf-component="button"] { diff --git a/design-system/apps/design-lab/vite/component-detail-contract.test.mjs b/design-system/apps/design-lab/vite/component-detail-contract.test.mjs index c8623c3462..470be504ee 100644 --- a/design-system/apps/design-lab/vite/component-detail-contract.test.mjs +++ b/design-system/apps/design-lab/vite/component-detail-contract.test.mjs @@ -27,19 +27,19 @@ test("preview matrices define horizontal columns for every registered state coun ); assert.match( source, - /\.component-preview-matrix\[data-state-count="3"\]\s*\{[^}]*grid-template-columns:\s*96px\s+repeat\(3, minmax\(244px, 1fr\)\)/s, + /\.component-preview-matrix\[data-state-count="3"\]\s*\{[^}]*grid-template-columns:\s*96px\s+repeat\(3, minmax\(280px, max-content\)\)/s, ); assert.match( source, - /\.component-preview-matrix\[data-state-count="4"\]\s*\{[^}]*grid-template-columns:\s*96px\s+repeat\(4, minmax\(124px, 1fr\)\)/s, + /\.component-preview-matrix\[data-state-count="4"\]\s*\{[^}]*grid-template-columns:\s*96px\s+repeat\(4, minmax\(280px, max-content\)\)/s, ); assert.match( source, - /\.component-preview-matrix\[data-state-count="5"\]\s*\{[^}]*grid-template-columns:\s*96px\s+repeat\(5, minmax\(144px, 1fr\)\)/s, + /\.component-preview-matrix\[data-state-count="5"\]\s*\{[^}]*grid-template-columns:\s*96px\s+repeat\(5, minmax\(280px, max-content\)\)/s, ); assert.match( source, - /\.component-preview-matrix\[data-state-count="6"\]\s*\{[^}]*grid-template-columns:\s*96px\s+repeat\(6, minmax\(260px, 1fr\)\)/s, + /\.component-preview-matrix\[data-state-count="6"\]\s*\{[^}]*grid-template-columns:\s*96px\s+repeat\(6, minmax\(280px, max-content\)\)/s, ); }); @@ -561,3 +561,15 @@ test("Combobox details render their own live state and menus include nested inte assert.match(detail, /defaultSearchValue=\{state === "searching"/); assert.match(detail, / { + const [detail, styles] = await Promise.all([readFile(detailSource, "utf8"), readFile(stylesSource, "utf8")]); + assert.match(detail, /component.name === "Menu" \|\| component.name === "NavigationPanel" \? \(/); + assert.match(detail, /className="component-surface-state-list__preview"/); + assert.match(styles, /\.component-surface-state-list\s*\{[^}]*display: grid/); + assert.match(styles, /\.component-surface-state-list__preview\s*\{[^}]*overflow: auto/); + const picker = styles.match(/\.component-combobox-preview\s*\{[^}]*\}/)?.[0] ?? ""; + assert.match(picker, /padding:/); + assert.doesNotMatch(picker, /min-block-size: 280px/); + assert.match(styles, /\.component-inspector-preview\s*\{[^}]*overflow: auto/); +}); From 8ab436978bce281278239eabc4e8e1449eb0b4e4 Mon Sep 17 00:00:00 2001 From: kev1n77 Date: Sun, 30 Aug 2026 14:41:57 +0800 Subject: [PATCH 2/3] fix(design-lab): render actual component states and isolate form previews --- .../apps/design-lab/src/i18n/messages.ts | 12 ++ .../src/pages/ComponentDetailPage.tsx | 136 +++++++++++++++--- design-system/apps/design-lab/src/styles.css | 27 ++++ .../vite/component-detail-contract.test.mjs | 11 +- .../vite/component-preview-render.test.mjs | 85 +++++++++++ 5 files changed, 249 insertions(+), 22 deletions(-) create mode 100644 design-system/apps/design-lab/vite/component-preview-render.test.mjs diff --git a/design-system/apps/design-lab/src/i18n/messages.ts b/design-system/apps/design-lab/src/i18n/messages.ts index fe5e0679bd..d2a322d87b 100644 --- a/design-system/apps/design-lab/src/i18n/messages.ts +++ b/design-system/apps/design-lab/src/i18n/messages.ts @@ -410,6 +410,8 @@ export const enUSMessages = { "detail.livePlayground": "Live playground", "detail.allStates": "Preview states", "detail.preview": "Preview", + "detail.previewUnavailable": "Preview not implemented", + "components.preview.inputError": "Please check this value.", "detail.code": "Code", "detail.inspector.label": "Component inspector", "detail.inspector.properties": "Properties", @@ -473,6 +475,8 @@ export const enUSMessages = { "detail.option.with-context": "With context bar", "detail.option.raised": "Raised surface", "detail.option.subtle": "Subtle surface", + "detail.option.plain": "Plain surface", + "detail.option.divided": "With dividers", "detail.option.media": "Media composition", "detail.option.with-center": "With center region", "detail.option.overflow": "Scrollable overflow", @@ -1101,6 +1105,8 @@ export const zhCNMessages = { "detail.livePlayground": "实时工作台", "detail.allStates": "状态预览", "detail.preview": "预览", + "detail.previewUnavailable": "此组件尚未实现预览", + "components.preview.inputError": "请检查输入内容。", "detail.code": "代码", "detail.inspector.label": "组件检查器", "detail.inspector.properties": "属性", @@ -1164,6 +1170,8 @@ export const zhCNMessages = { "detail.option.with-context": "包含上下文栏", "detail.option.raised": "浮起表面", "detail.option.subtle": "弱化表面", + "detail.option.plain": "无背景表面", + "detail.option.divided": "带分隔线", "detail.option.media": "媒体组合", "detail.option.with-center": "包含居中区域", "detail.option.overflow": "可滚动溢出", @@ -1737,6 +1745,8 @@ export const zhTWMessages = { "detail.livePlayground": "即時工作台", "detail.allStates": "狀態預覽", "detail.preview": "預覽", + "detail.previewUnavailable": "此元件尚未實作預覽", + "components.preview.inputError": "請檢查輸入內容。", "detail.code": "程式碼", "detail.inspector.label": "元件檢查器", "detail.inspector.properties": "屬性", @@ -1794,6 +1804,8 @@ export const zhTWMessages = { "detail.option.with-context": "包含上下文列", "detail.option.raised": "浮起表面", "detail.option.subtle": "弱化表面", + "detail.option.plain": "無背景表面", + "detail.option.divided": "帶分隔線", "detail.option.media": "媒體組合", "detail.option.with-center": "包含置中區域", "detail.option.overflow": "可捲動溢出", diff --git a/design-system/apps/design-lab/src/pages/ComponentDetailPage.tsx b/design-system/apps/design-lab/src/pages/ComponentDetailPage.tsx index ce18043390..584d01a3c4 100644 --- a/design-system/apps/design-lab/src/pages/ComponentDetailPage.tsx +++ b/design-system/apps/design-lab/src/pages/ComponentDetailPage.tsx @@ -26,6 +26,9 @@ import { ActionCard, ActionItem, ActivityItem, + Alert, + Avatar, + AvatarGroup, Button, Card, CardBody, @@ -33,6 +36,7 @@ import { CardHeader, CardMedia, ChangeCount, + Checkbox, Composer, Combobox, ComposerContextBar, @@ -40,6 +44,7 @@ import { ComposerToolbar, ConfirmDialog, Disclosure, + Empty, Field, FieldGroup, FieldRow, @@ -58,7 +63,9 @@ import { NavigationPanelItem, NavigationPanelSection, NavigationPanelSeparator, + NumberInput, PageHeader, + Radio, ScrollArea, SearchField, SegmentedControl, @@ -66,6 +73,7 @@ import { StatusPill, Switch, TabGroup, + Textarea, ThemeRoot, Toolbar, ToolbarBadge, @@ -89,6 +97,7 @@ import { type TokenOverrides, } from "@bitfun/ui"; import type { ComponentMeta } from "@bitfun/ui/registry"; +import previewImage from "../assets/design-system-hero.webp"; import { NestedMenuPattern } from "./ReferencePatterns"; import { useI18n, type MessageKey } from "../i18n"; import { @@ -169,6 +178,8 @@ const optionLabelKeys: Readonly> = { custom: "detail.option.custom", empty: "detail.option.empty", pending: "detail.option.pending", + plain: "detail.option.plain", + divided: "detail.option.divided", md: "detail.option.md", none: "detail.option.none", off: "detail.option.off", @@ -215,7 +226,7 @@ function InspectorSelect({ @@ -244,6 +255,20 @@ function InspectorToggle({ ); } +function NumberInputPreview({ state }: { state: string }) { + const { t } = useI18n(); + const [value, setValue] = useState(8); + return ( + + ); +} + export function ComponentDetailPage({ colorScheme, component, @@ -254,6 +279,7 @@ export function ComponentDetailPage({ tokenOverrides, }: ComponentDetailPageProps) { const { t } = useI18n(); + const stateLabel = (state: string) => optionLabelKeys[state] ? t(optionLabelKeys[state]) : state; const [variant, setVariant] = useState<(typeof buttonVariants)[number]>("fill"); const [iconButtonVariant, setIconButtonVariant] = useState<(typeof iconButtonVariants)[number]>("quiet"); const [iconName, setIconName] = useState("search"); @@ -294,7 +320,7 @@ export function ComponentDetailPage({ ? "auto" : component.name === "Tooltip" ? "top" - : "default", + : component.states[0] ?? "default", ); const [inspectorDisabled, setInspectorDisabled] = useState(false); const [inspectorLoading, setInspectorLoading] = useState(false); @@ -363,8 +389,10 @@ export function ComponentDetailPage({ return ["top", "bottom", "left", "right"] as const; case "Combobox": return component.states; - default: + case "Switch": return ["off", "on", "focus-visible", "disabled"] as const; + default: + return component.states; } }, [component.name, component.states, flowChatPreview]); const inspectorStates = component.name === "Button" || component.name === "IconButton" @@ -372,6 +400,12 @@ export function ComponentDetailPage({ : states; const codeSample = useMemo(() => { + if (component.name === "Textarea") return `import { Textarea } from "@bitfun/ui";\n\n`; + if (component.name === "Alert") return `import { Alert } from "@bitfun/ui";\n\n`; + if (component.name === "Avatar") return 'import { Avatar } from "@bitfun/ui";\n\nBF'; + if (component.name === "Checkbox" || component.name === "Radio") return `import { ${component.name} } from "@bitfun/ui";\n\n<${component.name} label="${t("components.preview.notifications")}" defaultChecked />`; + if (component.name === "NumberInput") return 'import { useState } from "react";\nimport { NumberInput } from "@bitfun/ui";\n\nfunction Example() {\n const [value, setValue] = useState(8);\n return ;\n}'; + if (component.name === "Empty") return `import { Empty } from "@bitfun/ui";\n\n`; if (component.name === "Combobox") return 'import { Combobox } from "@bitfun/ui";\n\n'; if (flowChatPreview) { return flowChatPreview.codeSample(t); @@ -503,6 +537,7 @@ export function ComponentDetailPage({ if (component.name === "Toolbar") { return `import { ChangeCount, IconButton, TabGroup, Toolbar, ToolbarBadge, ToolbarGroup, ToolbarSeparator } from "@bitfun/ui";\nimport { MoreHorizontal, Search } from "lucide-react";\n\nconst items = [\n { label: "${t("components.preview.welcome")}", value: "welcome" },\n { label: "${t("components.preview.settings")}", value: "settings" },\n];\n\n\n 18\n ${t("components.preview.session")}\n }\n leading={}\n size="${toolbarSize}"\n trailing={\n \n \n } size="xs" />\n } size="xs" />\n }\n/>`; } + if (component.name !== "Switch") return `// ${t("detail.previewUnavailable")}: ${component.name}`; const stateProps = previewState === "on" ? " defaultChecked" : previewState === "disabled" @@ -736,6 +771,64 @@ export function ComponentDetailPage({ return ; } + if (component.name === "Textarea") { + return ( +