Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions design-system/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ The same entry publishes `ChatComposer`, the shared context-band and
compact/expanded input anatomy used by product composers without importing
their stores, localized menus, editor, model, voice, or host actions.

`SetupWizard` publishes the guided configuration pattern used by agent creation:
a split header, tinted explanatory pane, one current-step indicator, form/editor
viewport and footer actions. It supports any consumer-controlled sequence and
stacks by available container width. Design Lab includes a working three-stage
example; the [UI package guide](packages/ui/README.md#guided-configuration)
documents validation, localized guidance, dialog labelling and scroll ownership.

FlowChat tool-card frameworks are published from `@openbitfun/ui/flow-chat` as
`AmbientToolCard` and `ProminentToolCard`. The names describe whether a tool
result should stay in the conversational background or receive deliberate user
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const descriptionKeys: Readonly<Record<string, MessageKey>> = {
MobileTag: "component.MobileTag.description",
MobileToolbar: "component.MobileToolbar.description",
SplitView: "component.SplitView.description",
SetupWizard: "component.SetupWizard.description",
ListModelsToolCard: "component.ListModelsToolCard.description",
ControlHubToolCard: "component.ControlHubToolCard.description",
ThinkingBlock: "component.ThinkingBlock.description",
Expand Down
4 changes: 4 additions & 0 deletions design-system/apps/design-lab/src/i18n/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { flowChatEn, flowChatZhCN, flowChatZhTW } from "./flowChatMessages";
import { subagentMotionEn, subagentMotionZhCN, subagentMotionZhTW } from "./subagentMotionMessages";
import { thinkingIndicatorEn, thinkingIndicatorZhCN, thinkingIndicatorZhTW } from "./thinkingIndicatorMessages";
import { splitViewEn, splitViewZhCN, splitViewZhTW } from "./splitViewMessages";
import { setupWizardEn, setupWizardZhCN, setupWizardZhTW } from "./setupWizardMessages";
import type { DesignLabLocale } from "./core.mjs";

export const enUSMessages = {
Expand All @@ -16,6 +17,7 @@ export const enUSMessages = {
...subagentMotionEn,
...thinkingIndicatorEn,
...splitViewEn,
...setupWizardEn,
...flowChatEn,
"component.OpenBitFunSolidMark.description": "The original silver solid mark on a transparent canvas, preserving its material and shading.",
"component.OpenBitFunAppIcon.description": "The application icon used by the installed product: silver mark, black rounded-square tile, and assets for different display sizes.",
Expand Down Expand Up @@ -838,6 +840,7 @@ export const zhCNMessages = {
...subagentMotionZhCN,
...thinkingIndicatorZhCN,
...splitViewZhCN,
...setupWizardZhCN,
...flowChatZhCN,
"component.OpenBitFunSolidMark.description": "透明底的银色实体标志,保留原稿的材质、光泽与立体明暗。",
"component.OpenBitFunAppIcon.description": "实际应用使用的图标:银色实体标志、黑色圆角底板,以及适配不同显示尺寸的资源。",
Expand Down Expand Up @@ -1658,6 +1661,7 @@ export const zhTWMessages = {
...subagentMotionZhTW,
...thinkingIndicatorZhTW,
...splitViewZhTW,
...setupWizardZhTW,
...flowChatZhTW,
"patterns.description": "查看元件在常見介面中的組合與互動。",
"patterns.title": "組合範例",
Expand Down
83 changes: 83 additions & 0 deletions design-system/apps/design-lab/src/i18n/setupWizardMessages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
export const setupWizardEn = {
"component.SetupWizard.description": "Guided configuration with a tinted explanation pane, one current-step indicator, a form or editor viewport, and independent actions.",
"setupWizard.title": "Configure workflow",
"setupWizard.guide": "Configuration guidance",
"setupWizard.progress": "{current} / {total}",
"setupWizard.basics": "Basic information",
"setupWizard.basicsTitle": "Make the workflow easy to find",
"setupWizard.basicsDescription": "A recognizable name helps collaborators choose the right workflow when starting a task.",
"setupWizard.rules": "Working rules",
"setupWizard.rulesTitle": "Keep results consistent",
"setupWizard.rulesDescription": "These rules apply to every run. Define completion criteria so repeated tasks produce comparable results.",
"setupWizard.access": "Available capabilities",
"setupWizard.accessTitle": "Set the scope of execution",
"setupWizard.accessDescription": "Enable only the capabilities this workflow needs. Every operation remains subject to the current permission policy.",
"setupWizard.name": "Workflow name",
"setupWizard.namePlaceholder": "For example, weekly research",
"setupWizard.rulesPlaceholder": "Enter the workflow's working rules",
"setupWizard.network": "Allow network access",
"setupWizard.back": "Back",
"setupWizard.next": "Next",
"setupWizard.finish": "Finish preview",
"setupWizard.reset": "Reset",
"setupWizard.complete": "Preview complete. No configuration was saved.",
"setupWizard.help": "Try all three stages. Drafts survive navigation. The contained state delegates scrolling to the editor; the narrow state stacks the guide above the form.",
"setupWizard.state.form": "Form",
"setupWizard.state.contained": "Contained editor",
} as const;

export const setupWizardZhCN = {
"component.SetupWizard.description": "分步配置布局,包含带底色的引导区、单个当前步骤指示、表单或编辑器区域,以及独立操作区。",
"setupWizard.title": "配置工作流",
"setupWizard.guide": "配置引导",
"setupWizard.progress": "{current} / {total}",
"setupWizard.basics": "基础信息",
"setupWizard.basicsTitle": "方便发现与选择",
"setupWizard.basicsDescription": "清晰的名称能帮助协作者在开始任务时,找到适合的工作流。",
"setupWizard.rules": "工作规则",
"setupWizard.rulesTitle": "让结果保持一致",
"setupWizard.rulesDescription": "规则会应用于每次执行。明确完成条件,让重复任务的结果更容易比较。",
"setupWizard.access": "可用能力",
"setupWizard.accessTitle": "界定执行范围",
"setupWizard.accessDescription": "仅开放工作流需要的能力。每次操作仍受当前权限策略约束。",
"setupWizard.name": "工作流名称",
"setupWizard.namePlaceholder": "例如:每周调研",
"setupWizard.rulesPlaceholder": "请输入工作流的执行规则",
"setupWizard.network": "允许访问网络",
"setupWizard.back": "上一步",
"setupWizard.next": "下一步",
"setupWizard.finish": "完成预览",
"setupWizard.reset": "重置",
"setupWizard.complete": "预览已完成,未保存配置。",
"setupWizard.help": "可体验三个阶段,返回时保留输入。编辑器模式由内部编辑区域负责滚动;窄屏模式将引导区放到表单上方。",
"setupWizard.state.form": "表单",
"setupWizard.state.contained": "沉浸式编辑器",
} satisfies Record<keyof typeof setupWizardEn, string>;

export const setupWizardZhTW = {
"component.SetupWizard.description": "分步設定版面,包含帶底色的引導區、單個目前步驟指示、表單或編輯器區域,以及獨立操作區。",
"setupWizard.title": "設定工作流程",
"setupWizard.guide": "設定引導",
"setupWizard.progress": "{current} / {total}",
"setupWizard.basics": "基本資訊",
"setupWizard.basicsTitle": "方便發現與選擇",
"setupWizard.basicsDescription": "清楚的名稱能協助協作者在開始任務時,找到適合的工作流程。",
"setupWizard.rules": "工作規則",
"setupWizard.rulesTitle": "讓結果保持一致",
"setupWizard.rulesDescription": "規則會套用至每次執行。明確的完成條件,讓重複任務的結果更容易比較。",
"setupWizard.access": "可用能力",
"setupWizard.accessTitle": "界定執行範圍",
"setupWizard.accessDescription": "僅開放工作流程需要的能力。每次操作仍受目前權限策略約束。",
"setupWizard.name": "工作流程名稱",
"setupWizard.namePlaceholder": "例如:每週調研",
"setupWizard.rulesPlaceholder": "請輸入工作流程的執行規則",
"setupWizard.network": "允許存取網路",
"setupWizard.back": "上一步",
"setupWizard.next": "下一步",
"setupWizard.finish": "完成預覽",
"setupWizard.reset": "重設",
"setupWizard.complete": "預覽已完成,未儲存設定。",
"setupWizard.help": "可體驗三個階段,返回時保留輸入。編輯器模式由內部編輯區域負責捲動;窄螢幕模式將引導區放到表單上方。",
"setupWizard.state.form": "表單",
"setupWizard.state.contained": "沉浸式編輯器",
} satisfies Record<keyof typeof setupWizardEn, string>;
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ import { IconCompositionPreview } from "../preview/IconCompositionPreview";
import { RollingTextPreview } from "../preview/RollingTextPreview";
import { ThinkingIndicatorPreview } from "../preview/ThinkingIndicatorPreview";
import { SplitViewPreview } from "../preview/SplitViewPreview";
import { SetupWizardPreview } from "../preview/SetupWizardPreview";
import { NestedMenuPattern } from "./ReferencePatterns";
import { useI18n, type MessageKey } from "../i18n";
import {
Expand Down Expand Up @@ -224,6 +225,8 @@ const iconSizes = ["2xs", "xs", "sm", "md", "lg"] as const;
const iconTones = ["inherit", "primary", "secondary", "muted", "disabled", "info", "success", "warning", "danger"] as const;

const optionLabelKeys: Readonly<Record<string, MessageKey>> = {
form: "setupWizard.state.form",
contained: "setupWizard.state.contained",
...Object.fromEntries(Object.keys(previewEn).filter(key => key.startsWith("preview.state.")).map(key => [key.slice("preview.state.".length), key as MessageKey])),
collapsed: "detail.option.collapsed",
streaming: "detail.option.streaming",
Expand Down Expand Up @@ -493,6 +496,9 @@ export function ComponentDetailPage({
const inspectorStates = states;

const codeSample = useMemo(() => {
if (component.name === "SetupWizard") {
return 'import { SetupWizard } from "@openbitfun/ui";\n\n// The host owns step state, drafts, validation, and saving.\n<SetupWizard\n title={title}\n titleId={titleId}\n headerActions={<CloseButton />}\n guideLabel={guideLabel}\n step={{ ...steps[current], progress: formatProgress(current + 1, steps.length) }}\n contentMode={steps[current].hasEditor ? "contained" : "scroll"}\n footer={<StepActions />}\n>\n <StepForm />\n</SetupWizard>\n\n// For a modal, connect Dialog aria-labelledby to the same titleId.\n';
}
if (component.name === "SplitView") {
return `import { SplitView } from "@openbitfun/ui";\n\n<SplitView\n primary={<Chat />}\n secondary={<Content />}\n mode={mode}\n secondarySide={contentSide}\n rightSize={rightSize}\n onRightSizeChange={setRightSize}\n dividerLabel="${t('splitView.resize')}"\n dividerActions={<SwapButton onClick={swapPanes} />}\n/>`;
}
Expand Down Expand Up @@ -972,6 +978,7 @@ export function ComponentDetailPage({
return <ThinkingIndicatorPreview key={state} state={state} />;
}
if (component.name === "SplitView") return <SplitViewPreview state={state} />;
if (component.name === "SetupWizard") return <SetupWizardPreview state={state} />;

if (component.name === "MobileButton") {
return <MobileButton data-openbitfun-preview-state={state} disabled={state === "disabled"} loading={state === "loading"}>{t("components.preview.actionCardTitle")}</MobileButton>;
Expand Down
35 changes: 35 additions & 0 deletions design-system/apps/design-lab/src/preview/SetupWizardPreview.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
.setup-wizard-preview { inline-size: 100%; min-inline-size: 0; }
.setup-wizard-preview[data-narrow="true"] { max-inline-size: 420px; }
.setup-wizard-preview__surface {
block-size: 600px;
border-radius: var(--openbitfun-overlay-dialog-surface-radius);
box-shadow: var(--openbitfun-shadow-sm);
}
.setup-wizard-preview__form {
display: flex;
min-inline-size: 0;
flex-direction: column;
gap: var(--openbitfun-space-5);
}
.setup-wizard-preview__form[data-contained="true"] { min-block-size: 0; flex: 1; }
.setup-wizard-preview__editor {
min-block-size: 0;
flex: 1;
inline-size: 100%;
border: 0;
padding: 0;
resize: none;
color: var(--openbitfun-color-content-primary);
background: transparent;
font: inherit;
}
.setup-wizard-preview__editor:focus-visible { outline: var(--openbitfun-border-width-default) solid var(--openbitfun-color-focus-ring); }
.setup-wizard-preview__note {
margin-block: var(--openbitfun-space-4) 0;
color: var(--openbitfun-color-content-muted);
font-family: var(--openbitfun-type-body-sm-font-family);
font-size: var(--openbitfun-type-body-sm-font-size);
font-weight: var(--openbitfun-type-body-sm-font-weight);
line-height: var(--openbitfun-type-body-sm-line-height);
letter-spacing: var(--openbitfun-type-body-sm-letter-spacing);
}
51 changes: 51 additions & 0 deletions design-system/apps/design-lab/src/preview/SetupWizardPreview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { useEffect, useId, useState } from "react";
import { Button, Field, Input, SetupWizard, Switch } from "@openbitfun/ui";
import { useI18n } from "../i18n";
import "./SetupWizardPreview.css";

export function SetupWizardPreview({ state }: { state: string }) {
const { t } = useI18n();
const formId = useId();
const [step, setStep] = useState<0 | 1 | 2>(state === "contained" ? 1 : 0);
const [name, setName] = useState("");
const [rules, setRules] = useState("");
const [network, setNetwork] = useState(false);
const [complete, setComplete] = useState(false);
useEffect(() => { setStep(state === "contained" ? 1 : 0); setComplete(false); }, [state]);
const steps = [
{ id: "basics", label: t("setupWizard.basics"), title: t("setupWizard.basicsTitle"), description: t("setupWizard.basicsDescription") },
{ id: "rules", label: t("setupWizard.rules"), title: t("setupWizard.rulesTitle"), description: t("setupWizard.rulesDescription") },
{ id: "access", label: t("setupWizard.access"), title: t("setupWizard.accessTitle"), description: t("setupWizard.accessDescription") },
] as const;
const reset = () => { setStep(0); setName(""); setRules(""); setNetwork(false); setComplete(false); };

return <div className="setup-wizard-preview" data-narrow={state === "narrow"}>
<SetupWizard
className="setup-wizard-preview__surface"
title={t("setupWizard.title")}
headerActions={<Button size="sm" variant="text" onClick={reset}>{t("setupWizard.reset")}</Button>}
guideLabel={t("setupWizard.guide")}
step={{ ...steps[step], progress: t("setupWizard.progress", { current: step + 1, total: steps.length }) }}
contentMode={step === 1 ? "contained" : "scroll"}
footer={<>
{step > 0 && <Button size="sm" variant="outline" onClick={() => { setStep(step === 2 ? 1 : 0); setComplete(false); }}>{t("setupWizard.back")}</Button>}
<Button type="submit" form={formId} size="sm" variant="primary">{t(step === 2 ? "setupWizard.finish" : "setupWizard.next")}</Button>
</>}
>
<form id={formId} className="setup-wizard-preview__form" data-contained={step === 1} onSubmit={event => {
event.preventDefault();
if (step < 2) setStep(step === 0 ? 1 : 2);
else setComplete(true);
}}>
{step === 0 && <Field label={t("setupWizard.name")}><Input placeholder={t("setupWizard.namePlaceholder")} value={name} onChange={event => setName(event.target.value)} /></Field>}
{step === 1 && <>
<label htmlFor={`${formId}-rules`}>{t("setupWizard.rules")}</label>
<textarea id={`${formId}-rules`} className="setup-wizard-preview__editor" value={rules} onChange={event => setRules(event.target.value)} placeholder={t("setupWizard.rulesPlaceholder")} />
</>}
{step === 2 && <Field label={t("setupWizard.network")}><Switch checked={network} onCheckedChange={setNetwork} /></Field>}
{complete && <p role="status">{t("setupWizard.complete")}</p>}
</form>
</SetupWizard>
<p className="setup-wizard-preview__note">{t("setupWizard.help")}</p>
</div>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const presentations: Record<string, ComponentPresentation> = {
Disclosure: "composition", Composer: "composition", Listbox: "composition", Menu: "composition",
NavigationPanel: "composition", PageHeader: "composition", ScrollArea: "composition", SplitView: "composition",
SegmentedControl: "composition", TabGroup: "composition",
SetupWizard: "composition",
Icon: "icons", RollingText: "motion", ShimmerText: "motion", VoiceCallPanel: "motion",
ThinkingIndicator: "motion",
MobileBadge: "compact", MobileButton: "compact", MobileIconButton: "compact",
Expand Down
58 changes: 58 additions & 0 deletions design-system/packages/ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,64 @@ the native scrollbar while preserving scrolling. `Menu`, `Listbox`, and
`scrollbar-gutter`, not local scrollbar colors or show/hide handlers. Monaco
and terminal renderers keep their own scrollbar APIs.

## Guided configuration

`SetupWizard` provides the split-pane layout for complex, staged configuration.
It includes the split header, a tinted guide pane, one compact current-step
indicator, a content viewport, and an independent footer. It has no divider lines.
The same component can sit inside a `Dialog` or an inline card. Its container
query stacks the guide above the form when the available width is narrow.

```tsx
import { Button, Dialog, DialogClose, SetupWizard } from "@openbitfun/ui";
import { useId } from "react";

const titleId = useId();

<Dialog open={open} onOpenChange={close} size="2xl" aria-labelledby={titleId}>
<SetupWizard
title="Configure workflow"
titleId={titleId}
headerActions={<DialogClose />}
guideLabel="Configuration guidance"
step={{
id: step.id,
label: step.label,
title: step.guideTitle,
description: step.guideDescription,
progress: formatProgress(current + 1, steps.length),
}}
contentMode={step.hasEditor ? "contained" : "scroll"}
footer={<>
<Button onClick={close}>Cancel</Button>
{current > 0 && <Button onClick={back}>Back</Button>}
<Button type="submit" form={formId} loading={saving}>Next</Button>
</>}
>
<form id={formId} onSubmit={validateAndAdvance}>{stepContent}</form>
</SetupWizard>
</Dialog>
```

The consumer controls the current step, any number of stages, field drafts,
validation, focus after navigation, asynchronous work, and submission. A stable
`step.id` resets only the content scroll position when the stage changes;
ordinary field updates do not reset scrolling or remount the content. Supply
localized copy and progress; the package imports no locale catalog or product
state. The guide announces the new explanation and current step together.

Write the left-hand guide about consequences, scope, or decision criteria.
Keep field instructions beside their controls on the right. Avoid a second
step list or repeating the right-hand heading on the left.

Use `contentMode="scroll"` for ordinary forms. `contained` bounds the content
viewport and delegates scrolling to an editor or other child. In that mode,
intermediate wrappers must use a flex column with `flex: 1` and `min-height: 0`.
Size the root through `className` or `style` when its default viewport height
does not suit the host. Keep validation and loading states in the content and
action slots. `Dialog` continues to own focus trapping, dismissal, and portals;
connect its `aria-labelledby` to `SetupWizard.titleId` explicitly.

## Text overflow


Expand Down
Loading
Loading