@@ -74,12 +74,12 @@ export function ProviderConfigurationPattern() {
- {footer(() => { setRevision(value => value + 1); setSaved(false); })}
+ {footer(() => { setRevision(value => value + 1); setSaved(false); })}
>
{t(saved ? "patterns.provider.saved" : "patterns.provider.previewOnly")}
;
}
+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
+
+
+ {savedPath &&
{savedPath}
}
+
;
+}
+
function ProviderFields() {
const { t } = useI18n();
const [models, setModels] = useState(["glm-5.2", "glm-4.7"]);
diff --git a/design-system/apps/design-lab/src/styles.css b/design-system/apps/design-lab/src/styles.css
index 3c2b6ae4e8..b7a4bada7a 100644
--- a/design-system/apps/design-lab/src/styles.css
+++ b/design-system/apps/design-lab/src/styles.css
@@ -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);
diff --git a/design-system/packages/design-tokens/src/system.tokens.json b/design-system/packages/design-tokens/src/system.tokens.json
index 079aff2ca9..cfed08c856 100644
--- a/design-system/packages/design-tokens/src/system.tokens.json
+++ b/design-system/packages/design-tokens/src/system.tokens.json
@@ -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" }
}
diff --git a/design-system/packages/ui/README.md b/design-system/packages/ui/README.md
index 862fea78c6..ef599ff9a8 100644
--- a/design-system/packages/ui/README.md
+++ b/design-system/packages/ui/README.md
@@ -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.
diff --git a/src/apps/data-migrator/ui/generated/design-system.css b/src/apps/data-migrator/ui/generated/design-system.css
index 7289d3927d..f54a291fd0 100644
--- a/src/apps/data-migrator/ui/generated/design-system.css
+++ b/src/apps/data-migrator/ui/generated/design-system.css
@@ -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;
diff --git a/src/web-ui/src/app/components/NewProjectDialog/NewProjectDialog.test.tsx b/src/web-ui/src/app/components/NewProjectDialog/NewProjectDialog.test.tsx
new file mode 100644
index 0000000000..dc6b082535
--- /dev/null
+++ b/src/web-ui/src/app/components/NewProjectDialog/NewProjectDialog.test.tsx
@@ -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
('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((_resolve, reject) => { rejectCreate = reject; });
+ const confirm = vi.fn(() => pending);
+ await act(async () => root.render());
+ 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());
+ 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);
+ });
+});
diff --git a/src/web-ui/src/infrastructure/config/components/ModelSettingsDialog.presentation.test.ts b/src/web-ui/src/infrastructure/config/components/ModelSettingsDialog.presentation.test.ts
index 7135e3715c..572b8ba948 100644
--- a/src/web-ui/src/infrastructure/config/components/ModelSettingsDialog.presentation.test.ts
+++ b/src/web-ui/src/infrastructure/config/components/ModelSettingsDialog.presentation.test.ts
@@ -27,9 +27,9 @@ describe('ModelSettingsPage dialog presentation', () => {
expect(editorDialog).toContain('size="xl"');
expect(editorDialog).not.toContain('size="2xl"');
expect(editorDialog).toMatch(
- /\{!reasoningPanelDraft && \(\s*/,
+ /\{!reasoningPanelDraft && \(\s*/,
);
- expect(editorDialog).not.toContain('appearance="floating"');
+ expect(editorDialog).toContain('appearance="floating"');
expect(editorDialog).toContain(
'