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
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const categoryKeys: Readonly<Record<string, MessageKey>> = {
};

const descriptionKeys: Readonly<Record<string, MessageKey>> = {
SplitView: "component.SplitView.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 @@ -5,6 +5,7 @@ import { designEn, designZhCN, designZhTW } from "./designMessages";
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 type { DesignLabLocale } from "./core.mjs";

export const enUSMessages = {
Expand All @@ -14,6 +15,7 @@ export const enUSMessages = {
...brandAssetsEn,
...subagentMotionEn,
...thinkingIndicatorEn,
...splitViewEn,
...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 @@ -818,6 +820,7 @@ export const zhCNMessages = {
...brandAssetsZhCN,
...subagentMotionZhCN,
...thinkingIndicatorZhCN,
...splitViewZhCN,
...flowChatZhCN,
"component.OpenBitFunSolidMark.description": "透明底的银色实体标志,保留原稿的材质、光泽与立体明暗。",
"component.OpenBitFunAppIcon.description": "实际应用使用的图标:银色实体标志、黑色圆角底板,以及适配不同显示尺寸的资源。",
Expand Down Expand Up @@ -1620,6 +1623,7 @@ export const zhTWMessages = {
...brandAssetsZhTW,
...subagentMotionZhTW,
...thinkingIndicatorZhTW,
...splitViewZhTW,
...flowChatZhTW,
"patterns.description": "查看元件在常見介面中的組合與互動。",
"patterns.title": "組合範例",
Expand Down
41 changes: 41 additions & 0 deletions design-system/apps/design-lab/src/i18n/splitViewMessages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
export const splitViewEn = {
'component.SplitView.description': 'A resizable pair of panes. Content swaps without moving the divider or remounting either pane.',
'splitView.split': 'Show both panes',
'splitView.hide': 'Hide content',
'splitView.maximize': 'Maximize content',
'splitView.resize': 'Resize panes',
'splitView.swap': 'Swap panes',
'splitView.primary': 'Conversation',
'splitView.secondary': 'Content',
'splitView.draft': 'Write a draft',
'splitView.edit': 'Edit content',
'splitView.help': 'Hover over the divider to swap panes. Drag or use arrow keys to resize; F6 moves focus between panes. Your text survives every layout change.',
} as const;

export const splitViewZhCN = {
'component.SplitView.description': '可调整大小的双窗格。交换内容时保留分隔线位置,两个窗格持续挂载。',
'splitView.split': '显示双窗格',
'splitView.hide': '隐藏内容',
'splitView.maximize': '内容全屏',
'splitView.resize': '调整窗格宽度',
'splitView.swap': '交换左右窗格',
'splitView.primary': '聊天',
'splitView.secondary': '内容',
'splitView.draft': '输入草稿',
'splitView.edit': '编辑内容',
'splitView.help': '悬停分隔线可交换窗格,拖动或使用方向键调整宽度,F6 切换窗格焦点。布局切换会保留已输入的文字。',
} satisfies Record<keyof typeof splitViewEn, string>;

export const splitViewZhTW = {
'component.SplitView.description': '可調整大小的雙窗格。交換內容時保留分隔線位置,兩個窗格持續掛載。',
'splitView.split': '顯示雙窗格',
'splitView.hide': '隱藏內容',
'splitView.maximize': '內容全螢幕',
'splitView.resize': '調整窗格寬度',
'splitView.swap': '交換左右窗格',
'splitView.primary': '聊天',
'splitView.secondary': '內容',
'splitView.draft': '輸入草稿',
'splitView.edit': '編輯內容',
'splitView.help': '懸停分隔線可交換窗格,拖動或使用方向鍵調整寬度,F6 切換窗格焦點。佈局切換會保留已輸入的文字。',
} satisfies Record<keyof typeof splitViewEn, string>;
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ import previewImage from "../assets/design-system-hero.webp";
import { IconCompositionPreview } from "../preview/IconCompositionPreview";
import { RollingTextPreview } from "../preview/RollingTextPreview";
import { ThinkingIndicatorPreview } from "../preview/ThinkingIndicatorPreview";
import { SplitViewPreview } from "../preview/SplitViewPreview";
import { NestedMenuPattern } from "./ReferencePatterns";
import { useI18n, type MessageKey } from "../i18n";
import {
Expand Down Expand Up @@ -443,6 +444,9 @@ export function ComponentDetailPage({
const inspectorStates = states;

const codeSample = useMemo(() => {
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/>`;
}
if (component.name === "ThinkingIndicator") {
return `import { ThinkingIndicator } from "@openbitfun/ui";\n\n<ThinkingIndicator active={thinking} size="sm" label="${t("thinkingIndicator.label")}" />\n`;
}
Expand Down Expand Up @@ -903,6 +907,7 @@ export function ComponentDetailPage({
if (component.name === "ThinkingIndicator") {
return <ThinkingIndicatorPreview key={state} state={state} />;
}
if (component.name === "SplitView") return <SplitViewPreview 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
30 changes: 30 additions & 0 deletions design-system/apps/design-lab/src/preview/SplitViewPreview.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
.split-view-preview { display: grid; inline-size: 100%; min-inline-size: 0; gap: var(--openbitfun-space-4); }
.split-view-preview__controls { display: flex; flex-wrap: wrap; gap: var(--openbitfun-space-2); }
.split-view-preview__viewport {
block-size: 20rem;
border: var(--openbitfun-border-width-default) solid var(--openbitfun-color-border-subtle);
border-radius: var(--openbitfun-radius-md);
overflow: hidden;
}
.split-view-preview__pane {
display: flex;
flex-direction: column;
gap: var(--openbitfun-space-4);
padding: var(--openbitfun-space-4);
min-inline-size: 0;
color: var(--openbitfun-color-content-primary);
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);
}
.split-view-preview__note {
margin: 0;
color: var(--openbitfun-color-content-secondary);
font-family: var(--openbitfun-type-support-font-family);
font-size: var(--openbitfun-type-support-font-size);
font-weight: var(--openbitfun-type-support-font-weight);
line-height: var(--openbitfun-type-support-line-height);
letter-spacing: var(--openbitfun-type-support-letter-spacing);
}
31 changes: 31 additions & 0 deletions design-system/apps/design-lab/src/preview/SplitViewPreview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useEffect, useState } from 'react';
import { ArrowLeftRight } from 'lucide-react';
import { Button, Icon, IconButton, Input, SplitView, Tooltip, type SplitViewMode } from '@openbitfun/ui';
import { useI18n } from '../i18n';
import './SplitViewPreview.css';

export function SplitViewPreview({ state }: { state: string }) {
const { t } = useI18n();
const [mode, setMode] = useState<SplitViewMode>('split');
const [side, setSide] = useState<'left' | 'right'>('right');
const [rightSize, setRightSize] = useState(280);
useEffect(() => { setMode(state === 'primary' || state === 'secondary' ? state : 'split'); }, [state]);

return <div className="split-view-preview">
<div className="split-view-preview__controls">
<Button size="sm" onClick={() => setMode('split')}>{t('splitView.split')}</Button>
<Button size="sm" onClick={() => setMode('primary')}>{t('splitView.hide')}</Button>
<Button size="sm" onClick={() => setMode('secondary')}>{t('splitView.maximize')}</Button>
</div>
<div className="split-view-preview__viewport">
<SplitView mode={mode} secondarySide={side} rightSize={rightSize} onRightSizeChange={setRightSize}
minLeftSize={180} minRightSize={180} defaultRightSize={280} dividerLabel={t('splitView.resize')}
dividerActions={<Tooltip content={t('splitView.swap')}><IconButton size="sm" aria-label={t('splitView.swap')}
icon={<Icon glyph={ArrowLeftRight} size="sm" />} onClick={() => setSide(value => value === 'right' ? 'left' : 'right')} /></Tooltip>}
primary={<div className="split-view-preview__pane"><strong>{t('splitView.primary')}</strong><Input aria-label={t('splitView.draft')} placeholder={t('splitView.draft')} /></div>}
secondary={<div className="split-view-preview__pane"><strong>{t('splitView.secondary')}</strong><Input aria-label={t('splitView.edit')} placeholder={t('splitView.edit')} /></div>}
/>
</div>
<p className="split-view-preview__note">{t('splitView.help')}</p>
</div>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const presentations: Record<string, ComponentPresentation> = {
Dialog: "interactive", ConfirmDialog: "interactive", Sheet: "interactive",
Combobox: "interactive", MultiSelect: "interactive", Select: "interactive", Tooltip: "interactive",
Disclosure: "composition", Composer: "composition", Listbox: "composition", Menu: "composition",
NavigationPanel: "composition", PageHeader: "composition", ScrollArea: "composition",
NavigationPanel: "composition", PageHeader: "composition", ScrollArea: "composition", SplitView: "composition",
SegmentedControl: "composition", TabGroup: "composition",
Icon: "icons", RollingText: "motion", ShimmerText: "motion", VoiceCallPanel: "motion",
ThinkingIndicator: "motion",
Expand Down
4 changes: 2 additions & 2 deletions design-system/packages/theme-openbitfun/src/dark.tokens.json
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,8 @@
}
},
"link": {
"default": { "$type": "color", "$value": "{ref.color.blue.400}" },
"hover": { "$type": "color", "$value": "{ref.color.blue.300}" }
"default": { "$type": "color", "$value": "{ref.color.cyan.500}" },
"hover": { "$type": "color", "$value": "{ref.color.cyan.400}" }
},
"border": {
"subtle": { "$type": "color", "$value": "rgba(255, 255, 255, 0.12)" },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@
"secondary": { "$type": "color", "$value": "{ref.color.gray.50}" },
"muted": { "$type": "color", "$value": "{ref.color.gray.150}" }
},
"link": {
"default": { "$type": "color", "$value": "{ref.color.cyan.300}" },
"hover": { "$type": "color", "$value": "{ref.color.cyan.200}" }
},
"border": {
"subtle": { "$type": "color", "$value": "{ref.color.gray.400}" },
"default": { "$type": "color", "$value": "{ref.color.gray.250}" },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@
"secondary": { "$type": "color", "$value": "{ref.color.gray.800}" },
"muted": { "$type": "color", "$value": "{ref.color.gray.700}" }
},
"link": {
"default": { "$type": "color", "$value": "{ref.color.cyan.700}" },
"hover": { "$type": "color", "$value": "{ref.color.cyan.800}" }
},
"border": {
"subtle": { "$type": "color", "$value": "{ref.color.gray.500}" },
"default": { "$type": "color", "$value": "{ref.color.gray.650}" },
Expand Down
4 changes: 2 additions & 2 deletions design-system/packages/theme-openbitfun/src/light.tokens.json
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,8 @@
}
},
"link": {
"default": { "$type": "color", "$value": "{ref.color.blue.600}" },
"hover": { "$type": "color", "$value": "{ref.color.blue.700}" }
"default": { "$type": "color", "$value": "{ref.color.cyan.500}" },
"hover": { "$type": "color", "$value": "{ref.color.cyan.600}" }
},
"border": {
"subtle": { "$type": "color", "$value": "rgba(16, 26, 39, 0.08)" },
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { ComponentMeta } from '../../registry.types';

export const splitViewMeta = {
category: 'primitive',
description: 'A controlled two-pane layout with stable content containers, reversible placement, and accessible resizing.',
maturity: 'stable',
name: 'SplitView',
props: [
{ name: 'primary', type: 'ReactNode' },
{ name: 'secondary', type: 'ReactNode' },
{ name: 'mode', type: 'split | primary | secondary', defaultValue: 'split' },
{ name: 'secondarySide', type: 'left | right', defaultValue: 'right' },
{ name: 'rightSize', type: 'number' },
{ name: 'onRightSizeChange', type: '(size: number) => void' },
{ name: 'dividerActions', type: 'ReactNode' },
{ name: 'dividerLabel', type: 'string' },
],
states: ['split', 'primary', 'secondary'],
tokens: ['color.border.subtle', 'color.surface.raised', 'color.accent.default', 'color.focus.ring', 'space.1', 'space.2', 'space.3', 'radius.lg'],
} as const satisfies ComponentMeta;
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
@layer openbitfun.components {
.root {
display: grid;
grid-template-columns: minmax(0, 1fr) var(--openbitfun-border-width-default) minmax(0, var(--_split-view-right-size));
grid-template-areas: "primary divider secondary";
min-inline-size: 0;
min-block-size: 0;
inline-size: 100%;
block-size: 100%;
isolation: isolate;
}

.root[data-secondary-side="left"] {
grid-template-areas: "secondary divider primary";
}

.root[data-mode="primary"] { grid-template-columns: minmax(0, 1fr); grid-template-areas: "primary"; }
.root[data-mode="secondary"] { grid-template-columns: minmax(0, 1fr); grid-template-areas: "secondary"; }

.pane {
display: flex;
flex-direction: column;
min-inline-size: 0;
min-block-size: 0;
overflow: hidden;
}

.primary { grid-area: primary; }
.secondary { grid-area: secondary; }
.pane[hidden], .divider[hidden] { display: none; }

.divider {
grid-area: divider;
position: relative;
background: var(--openbitfun-color-border-subtle);
z-index: var(--openbitfun-layer-sticky);
}

.resizeHandle {
position: absolute;
inset-block: 0;
inset-inline: calc(-1 * var(--openbitfun-space-2));
cursor: col-resize;
touch-action: none;
outline: none;
}

.resizeHandle::after {
content: "";
position: absolute;
inset-block: 0;
inset-inline-start: 50%;
inline-size: var(--openbitfun-border-width-default);
transform: translateX(-50%);
background: var(--openbitfun-color-accent-default);
opacity: 0;
transition: opacity var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard);
}

.resizeHandle:hover::after,
.resizeHandle:focus-visible::after,
.root[data-resizing] .resizeHandle::after { opacity: 1; }

.resizeHandle:focus-visible::after { background: var(--openbitfun-color-focus-ring); }

.actions {
position: absolute;
inset-block-start: 50%;
inset-inline-start: 50%;
transform: translate(-50%, -50%);
display: flex;
align-items: center;
gap: var(--openbitfun-space-1);
padding: var(--openbitfun-space-1);
background: var(--openbitfun-color-surface-raised);
border: var(--openbitfun-border-width-default) solid var(--openbitfun-color-border-subtle);
border-radius: var(--openbitfun-radius-lg);
opacity: 0;
pointer-events: none;
transition: opacity var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard);
}

.divider:hover .actions, .divider:has(:focus-visible) .actions { opacity: 1; pointer-events: auto; }
.root[data-resizing] .pane, .root[data-resizing] .actions { pointer-events: none; }

@media (prefers-reduced-motion: reduce) {
.resizeHandle::after, .actions { transition: none; }
}

@media (hover: none) {
.actions { opacity: 1; pointer-events: auto; }
.resizeHandle { inset-inline: calc(-1 * var(--openbitfun-space-3)); }
}
}
Loading
Loading