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
9 changes: 9 additions & 0 deletions design-system/apps/design-lab/src/i18n/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,9 @@ export const enUSMessages = {
"patterns.navigation.description": "Compact 30-pixel rows, grouped destinations, disclosure, search, and persistent device status.",
"patterns.navigation.search": "Search navigation",
"patterns.navigation.workspace": "Workspace",
"patterns.workspace.parent": "Parent directory",
"patterns.workspace.name": "Workspace name",
"patterns.workspace.fullPath": "Full path",
"patterns.navigation.projects": "Projects",
"patterns.navigation.tools": "Development tools",
"patterns.navigation.status": "Keep primary navigation quiet; reveal secondary destinations only when they are needed.",
Expand Down Expand Up @@ -951,6 +954,9 @@ export const zhCNMessages = {
"patterns.navigation.description": "紧凑的 30 像素行、分组目标、展开区域、搜索与常驻设备状态。",
"patterns.navigation.search": "搜索导航",
"patterns.navigation.workspace": "工作区",
"patterns.workspace.parent": "父文件夹",
"patterns.workspace.name": "工作区名称",
"patterns.workspace.fullPath": "完整路径",
"patterns.navigation.projects": "项目",
"patterns.navigation.tools": "开发工具",
"patterns.navigation.status": "保持主导航安静,只在需要时展开次要目标。",
Expand Down Expand Up @@ -1485,6 +1491,9 @@ export const zhCNMessages = {

export const zhTWMessages = {
...zhCNMessages,
"patterns.workspace.parent": "父資料夾",
"patterns.workspace.name": "工作區名稱",
"patterns.workspace.fullPath": "完整路徑",
"component.Combobox.description": "支援搜尋、分組、單選或多選、自訂值、標籤及非同步狀態的選擇器。",
"patterns.provider.title": "供應商設定對話框",
"patterns.provider.description": "組合連線參數、搜尋多選、自訂模型、可展開的模型詳情和固定底部操作區。",
Expand Down
3 changes: 2 additions & 1 deletion design-system/apps/design-lab/src/pages/PatternsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import {
type TokenOverrides,
} from "@openbitfun/ui";
import { useI18n, type MessageKey } from "../i18n";
import { FormTypographyPattern, NestedMenuPattern, ProviderConfigurationPattern, SceneToolbarPattern } from "./ReferencePatterns";
import { FormTypographyPattern, NestedMenuPattern, ProviderConfigurationPattern, SceneToolbarPattern, WorkspaceConfigurationPattern } from "./ReferencePatterns";

interface PatternsPageProps {
colorScheme: ColorScheme;
Expand Down Expand Up @@ -179,6 +179,7 @@ export function PatternsPage({ colorScheme, contrast, density, tokenOverrides }:
</PatternSection>
<PatternSection description={t("patterns.provider.description")} index="05" title={t("patterns.provider.title")}>
<ProviderConfigurationPattern />
<WorkspaceConfigurationPattern />
</PatternSection>
<PatternSection description={t("patterns.toolbar.description")} index="06" title={t("patterns.toolbar.title")}>
<SceneToolbarPattern />
Expand Down
61 changes: 54 additions & 7 deletions design-system/apps/design-lab/src/pages/ReferencePatterns.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useRef, useState } from "react";
import { useId, useRef, useState } from "react";
import {
Button,
Card,
Expand Down Expand Up @@ -61,10 +61,10 @@ export function ProviderConfigurationPattern() {
const [open, setOpen] = useState(false);
const [revision, setRevision] = useState(0);
const [saved, setSaved] = useState(false);
const footer = (close: () => void) => <CardFooter align="center">
const footer = (close: () => void) => <>
<Button variant="fill" onClick={close}>{t("components.preview.modalCancel")}</Button>
<Button variant="primary" onClick={() => { setSaved(true); setOpen(false); }}>{t("components.preview.modalSave")}</Button>
</CardFooter>;
</>;

return <div className="pattern-provider" data-openbitfun-pattern="provider-configuration">
<div className="pattern-demo-actions">
Expand All @@ -74,31 +74,78 @@ export function ProviderConfigurationPattern() {
<Card appearance="raised" padding="md" gap="lg" radius="lg">
<PageHeader level={3} size="md" title={t("components.preview.modalTitle")} />
<ProviderFields key={revision} />
{footer(() => { setRevision(value => value + 1); setSaved(false); })}
<CardFooter align="center">{footer(() => { setRevision(value => value + 1); setSaved(false); })}</CardFooter>
</Card>
<Dialog
open={open}
onOpenChange={(nextOpen) => { if (!nextOpen) (() => setOpen(false))(); }}
size="md"
size="xl"
>
<DialogHeader>
<DialogHeading>
<DialogTitle>{t("components.preview.modalTitle")}</DialogTitle>
</DialogHeading>
<DialogClose aria-label={t("components.preview.close")} />
</DialogHeader>
<DialogBody inset="none">
<DialogBody>
<div className="pattern-provider-modal">
<ProviderFields />
</div>
</DialogBody>
<DialogFooter>{footer(() => setOpen(false))}</DialogFooter>
<DialogFooter appearance="floating">{footer(() => setOpen(false))}</DialogFooter>
</Dialog>
</>
<p className="pattern-feedback" role="status">{t(saved ? "patterns.provider.saved" : "patterns.provider.previewOnly")}</p>
</div>;
}

export function WorkspaceConfigurationPattern() {
const { t } = useI18n();
const formId = useId();
const [open, setOpen] = useState(false);
const [name, setName] = useState("");
const [parent, setParent] = useState("/workspaces");
const [savedPath, setSavedPath] = useState("");
const fullPath = name.trim() ? `${parent}/${name.trim()}` : "";
return <div data-openbitfun-pattern="workspace-configuration">
<Button size="sm" onClick={() => setOpen(true)}>{t("patterns.actions.newProject")}</Button>
<Dialog open={open} onOpenChange={() => setOpen(false)} size="sm">
<DialogHeader>
<DialogHeading><DialogTitle>{t("patterns.actions.newProject")}</DialogTitle></DialogHeading>
<DialogClose />
</DialogHeader>
<DialogBody>
<form id={formId} onSubmit={(event) => {
event.preventDefault();
if (!name.trim()) return;
setSavedPath(fullPath);
setOpen(false);
}}>
<FieldGroup appearance="subtle" dividers>
<FieldRow><Field label={t("patterns.workspace.parent")} controlWidth="fill">
<Select size="sm" value={parent} onValueChange={(value) => setParent(String(value))} options={[
{ value: "/workspaces", label: "/workspaces" },
{ value: "/workspaces/design-system/long-parent-directory", label: "/workspaces/design-system/long-parent-directory" },
]} />
</Field></FieldRow>
<FieldRow><Field label={t("patterns.workspace.name")} controlWidth="fill">
<Input size="sm" value={name} onChange={(event) => setName(event.target.value)} autoFocus />
</Field></FieldRow>
{fullPath && <FieldRow><Field label={t("patterns.workspace.fullPath")} controlWidth="fill">
<span className="pattern-workspace-path">{fullPath}</span>
</Field></FieldRow>}
</FieldGroup>
</form>
</DialogBody>
<DialogFooter>
<Button size="sm" variant="fill" onClick={() => setOpen(false)}>{t("components.preview.modalCancel")}</Button>
<Button size="sm" variant="primary" type="submit" form={formId} disabled={!name.trim()}>{t("patterns.actions.newProject")}</Button>
</DialogFooter>
</Dialog>
{savedPath && <p className="pattern-workspace-path" role="status">{savedPath}</p>}
</div>;
}

function ProviderFields() {
const { t } = useI18n();
const [models, setModels] = useState(["glm-5.2", "glm-4.7"]);
Expand Down
5 changes: 5 additions & 0 deletions design-system/apps/design-lab/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -2987,6 +2987,11 @@ body,
container-type: inline-size;
}

.pattern-workspace-path {
overflow-wrap: anywhere;
color: var(--openbitfun-color-content-secondary);
}

.pattern-provider-fields [data-openbitfun-component="disclosure"] [data-openbitfun-part="content-inner"] {
display: grid;
gap: var(--openbitfun-space-3);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -870,7 +870,7 @@
"maxInlineSizeSmall": { "$value": "420px" },
"maxInlineSizeMedium": { "$value": "560px" },
"maxInlineSizeLarge": { "$value": "600px" },
"maxInlineSizeXlarge": { "$value": "720px" },
"maxInlineSizeXlarge": { "$value": "800px" },
"maxInlineSizeXxlarge": { "$value": "960px" },
"maxInlineSizeWide": { "$value": "1200px" }
}
Expand Down
2 changes: 2 additions & 0 deletions design-system/packages/ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -417,3 +417,5 @@ Pattern includes a scrolling toggle for keyboard and submenu verification.
Compact tabs use `size="sm"` (30px, 14px icons, 4px icon gap); standard tabs retain 40px and 16px icons. Tabs share the outline-button surface contract and keep selection separate from pointer press. `SegmentedControl size="md"` uses a borderless 36px bar with 30px segments, 3px inset, 4px gaps and 12px segment padding. The default `sm` bar keeps its 28px outer height; separate pills retain their existing heights. Mobile controls own their touch geometry independently.

Dialog titles use 24px bold type with their own 29px line box and normal tracking. `DialogHeader` and `DialogFooter` omit separators by default; pass `separator` for a deliberate divider. A direct `DialogBody` sibling of `DialogFooter appearance="floating"` owns the trailing scroll inset automatically. The floating footer provides the 68px centered action area and a masked blur/gradient using the current theme surface; reduced transparency and forced colors use an opaque fallback. Keep scrollable form content inside `DialogBody` instead of adding a second viewport with independent footer spacing.

Extra-large (`xl`) dialogs have an 800px maximum width and continue shrinking within the viewport gutter. Provider editing uses the floating footer; small workspace creation retains its attached footer and existing button/input sizes. The Lab workspace pattern uses local sample paths and callbacks only.
2 changes: 1 addition & 1 deletion src/apps/data-migrator/ui/generated/design-system.css
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@
--openbitfun-overlay-dialog-max-inline-size-medium: 560px;
--openbitfun-overlay-dialog-max-inline-size-small: 420px;
--openbitfun-overlay-dialog-max-inline-size-wide: 1200px;
--openbitfun-overlay-dialog-max-inline-size-xlarge: 720px;
--openbitfun-overlay-dialog-max-inline-size-xlarge: 800px;
--openbitfun-overlay-dialog-max-inline-size-xxlarge: 960px;
--openbitfun-overlay-dialog-scrollbar-width: var(--openbitfun-scrollbar-width);
--openbitfun-overlay-dialog-surface-radius: 28px;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// @vitest-environment jsdom
import React, { act } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { NewProjectDialog } from './NewProjectDialog';

globalThis.IS_REACT_ACT_ENVIRONMENT = true;
const { pickDirectory } = vi.hoisted(() => ({ pickDirectory: vi.fn() }));
vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key: string) => key }) }));
vi.mock('@/shared/utils/logger', () => ({ createLogger: () => ({ error: vi.fn() }) }));
vi.mock('@/infrastructure/peer-device/pickWorkspaceDirectory', () => ({ pickWorkspaceDirectory: pickDirectory }));

describe('NewProjectDialog composition', () => {
let root: Root;
let host: HTMLDivElement;
const close = vi.fn();
const button = (label: string) => [...document.querySelectorAll('button')].find((item) => item.textContent === label)!;
const nameInput = () => document.querySelector<HTMLInputElement>('input:not([readonly])')!;
const enterName = (value: string) => act(() => {
const input = nameInput();
Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')!.set!.call(input, value);
input.dispatchEvent(new Event('input', { bubbles: true }));
});

beforeEach(() => {
vi.clearAllMocks();
host = document.createElement('div');
document.body.append(host);
root = createRoot(host);
});
afterEach(() => {
act(() => root.unmount());
host.remove();
});

it('locks actions while creating and retains the form when creation fails', async () => {
let rejectCreate!: (reason: Error) => void;
const pending = new Promise<void>((_resolve, reject) => { rejectCreate = reject; });
const confirm = vi.fn(() => pending);
await act(async () => root.render(<NewProjectDialog isOpen defaultParentPath="/srv/workspaces" onClose={close} onConfirm={confirm} />));
expect(button('newProject.cancel').dataset.openbitfunVariant).toBe('fill');
expect(button('newProject.create').dataset.openbitfunVariant).toBe('primary');
enterName(' example-project ');
await act(async () => { button('newProject.create').click(); });
expect(confirm).toHaveBeenCalledWith('/srv/workspaces', 'example-project');
expect(button('newProject.cancel').disabled).toBe(true);
expect(button('newProject.select').disabled).toBe(true);
expect(nameInput().disabled).toBe(true);
act(() => button('newProject.cancel').click());
expect(close).not.toHaveBeenCalled();
await act(async () => { rejectCreate(new Error('Directory unavailable')); });
expect(document.querySelector('[role="alert"]')?.textContent).toContain('Directory unavailable');
expect(nameInput().value).toBe(' example-project ');
expect(button('newProject.create').disabled).toBe(false);
expect(close).not.toHaveBeenCalled();
});

it('delegates directory selection to the peer-aware picker and preserves its path', async () => {
pickDirectory.mockResolvedValue('/srv/remote workspace');
const confirm = vi.fn(async () => {});
await act(async () => root.render(<NewProjectDialog isOpen defaultParentPath="/srv" onClose={close} onConfirm={confirm} />));
await act(async () => { button('newProject.select').click(); });
expect(pickDirectory).toHaveBeenCalledWith({ title: 'newProject.selectParentDirectory', defaultPath: '/srv' });
enterName('project');
expect(document.querySelector('[data-openbitfun-part="preview"]')?.textContent).toContain('/srv/remote workspace/project');
await act(async () => { button('newProject.create').click(); });
expect(confirm).toHaveBeenCalledWith('/srv/remote workspace', 'project');
expect(close).toHaveBeenCalledTimes(1);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ describe('ModelSettingsPage dialog presentation', () => {
expect(editorDialog).toContain('size="xl"');
expect(editorDialog).not.toContain('size="2xl"');
expect(editorDialog).toMatch(
/\{!reasoningPanelDraft && \(\s*<DialogFooter>/,
/\{!reasoningPanelDraft && \(\s*<DialogFooter appearance="floating">/,
);
expect(editorDialog).not.toContain('appearance="floating"');
expect(editorDialog).toContain('appearance="floating"');
expect(editorDialog).toContain(
'<Button variant="fill" size="sm" onClick={requestCloseEditingModal} disabled={isEditorSaving}>',
);
Expand All @@ -39,6 +39,8 @@ describe('ModelSettingsPage dialog presentation', () => {
expect(editorDialog).toContain('<DialogClose disabled={isEditorSaving} />');
expect(editorDialog).toContain('loading={isEditorSaving}');
expect(editingForm.match(/fieldSurface="default"/g)).toHaveLength(2);
expect(editingForm).not.toContain('<ScrollArea');
expect(editingForm).toContain('className="openbitfun-model-settings__form-content"');
expect(editorDialog).not.toContain('openbitfun-model-settings__editor-dialog-footer');
expect(editorDialog).not.toContain('openbitfun-model-settings__editor-dialog-cancel');
expect(styles).toMatch(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1415,17 +1415,11 @@
&__form--modal {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
overflow: hidden;
flex: 0 0 auto;
}

&__form-scrollable {
flex: 1 1 auto;
min-height: 0;
overflow-x: hidden;
padding: var(--openbitfun-space-5) var(--openbitfun-space-6);
scroll-padding-block: var(--openbitfun-space-5);
&__form-content {
padding-inline: var(--openbitfun-space-6);
display: flex;
flex-direction: column;
gap: var(--openbitfun-space-6);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2757,7 +2757,7 @@ const ModelSettingsPage: React.FC = () => {
return (
<>
<div className="openbitfun-model-settings__form openbitfun-model-settings__form--modal" data-openbitfun-component="model-settings" data-openbitfun-part="form">
<ScrollArea className="openbitfun-model-settings__form-scrollable" data-openbitfun-component="model-settings" data-openbitfun-part="formBody">
<div className="openbitfun-model-settings__form-content" data-openbitfun-component="model-settings" data-openbitfun-part="formBody">
<ConfigPageSection
title={isProviderScopedEditing ? t('editProviderSubtitle') : t('editSubtitle')}
className="openbitfun-model-settings__edit-section"
Expand Down Expand Up @@ -3233,7 +3233,7 @@ const ModelSettingsPage: React.FC = () => {
)}
</ConfigPageSection>
)}
</ScrollArea>
</div>

</div>
</>
Expand Down Expand Up @@ -4160,7 +4160,7 @@ const ModelSettingsPage: React.FC = () => {
) : renderEditingForm()}
</DialogBody>
{!reasoningPanelDraft && (
<DialogFooter>
<DialogFooter appearance="floating">
<Button variant="fill" size="sm" onClick={requestCloseEditingModal} disabled={isEditorSaving}>
{t('actions.cancel')}
</Button>
Expand Down
Loading