Skip to content

Commit a6e5251

Browse files
committed
feat(web): complete node graph dark mode and i18n rollout
1 parent b7ffbec commit a6e5251

50 files changed

Lines changed: 6131 additions & 2648 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/web/src/auth/ProfilePage.tsx

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,8 @@ export const ProfilePage = () => {
142142

143143
if (!token) {
144144
return (
145-
<div className="min-h-screen bg-white text-[#101010]">
146-
<header className="flex items-center justify-between gap-4 bg-white px-5 py-4 md:px-7 md:py-[18px]">
145+
<div className="min-h-screen bg-(--color-0) text-(--color-10)">
146+
<header className="flex items-center justify-between gap-4 bg-(--color-0) px-5 py-4 md:px-7 md:py-[18px]">
147147
<MdrButton
148148
text={t('actions.backHome')}
149149
size="Small"
@@ -158,7 +158,7 @@ export const ProfilePage = () => {
158158
/>
159159
</header>
160160
<main className="mx-auto grid max-w-[980px] gap-[18px] px-5 pb-10 md:px-7 md:pb-12">
161-
<div className="grid min-h-[calc(100vh-140px)] place-content-center gap-2.5 text-center text-[#2b2b2b]">
161+
<div className="grid min-h-[calc(100vh-140px)] place-content-center gap-2.5 text-center text-(--color-8)">
162162
<MdrIcon icon={<UserRound />} size={34} />
163163
<MdrHeading level={2} className="m-0 text-[88px]">
164164
{t('empty.title')}
@@ -171,8 +171,8 @@ export const ProfilePage = () => {
171171
}
172172

173173
return (
174-
<div className="min-h-screen bg-white text-[#101010]">
175-
<header className="flex items-center justify-between gap-4 bg-white px-5 py-4 md:px-7 md:py-[18px]">
174+
<div className="min-h-screen bg-(--color-0) text-(--color-10)">
175+
<header className="flex items-center justify-between gap-4 bg-(--color-0) px-5 py-4 md:px-7 md:py-[18px]">
176176
<MdrButton
177177
text={t('actions.backHome')}
178178
size="Small"
@@ -234,12 +234,12 @@ export const ProfilePage = () => {
234234
>
235235
{displayName}
236236
</MdrHeading>
237-
<MdrParagraph className="m-0 max-w-[58ch] text-[13px] leading-[1.5] text-[#2b2b2b]">
237+
<MdrParagraph className="m-0 max-w-[58ch] text-[13px] leading-[1.5] text-(--color-7)">
238238
{displayBio}
239239
</MdrParagraph>
240240
<button
241241
type="button"
242-
className="-translate-x-2.5 inline-flex max-w-full cursor-pointer items-center gap-3 rounded-2xl border-0 bg-(--color-0) px-3 py-2.5 transition-colors duration-150 hover:bg-black/[0.02]"
242+
className="-translate-x-2.5 inline-flex max-w-full cursor-pointer items-center gap-3 rounded-2xl border-0 bg-(--color-0) px-3 py-2.5 transition-colors duration-150 hover:bg-(--color-1)"
243243
onClick={() => copyText(user?.id, t('messages.copiedId'))}
244244
aria-label={t('actions.copyId')}
245245
title={t('actions.copyId')}
@@ -257,14 +257,14 @@ export const ProfilePage = () => {
257257
{bits.map((bit, bitIndex) => (
258258
<span
259259
key={`${columnIndex}-${bitIndex}`}
260-
className={`h-0.5 w-0.5 rounded-full ${bit ? 'bg-[#101010]' : 'invisible bg-black/12 shadow-[inset_0_0_0_1px_rgba(0,0,0,0.12)]'}`}
260+
className={`h-0.5 w-0.5 rounded-full ${bit ? 'bg-(--color-9)' : 'invisible'}`}
261261
/>
262262
))}
263263
</div>
264264
))}
265265
</div>
266266
) : (
267-
<span className="[font-family:'JetBrains_Mono','SFMono-Regular','Menlo',monospace] text-xs tracking-[0.12em] break-all text-[#101010]">
267+
<span className="[font-family:'JetBrains_Mono','SFMono-Regular','Menlo',monospace] text-xs tracking-[0.12em] break-all text-(--color-9)">
268268
{user?.id}
269269
</span>
270270
)}
@@ -278,7 +278,7 @@ export const ProfilePage = () => {
278278
<section className="mt-0.5 flex flex-wrap items-center gap-2.5">
279279
<button
280280
type="button"
281-
className="inline-flex cursor-pointer items-center gap-2 rounded-full border-0 bg-black/[0.04] px-3 py-2 text-xs text-[#101010] transition-colors duration-150 hover:bg-black/[0.06]"
281+
className="inline-flex cursor-pointer items-center gap-2 rounded-full border-0 bg-(--color-1) px-3 py-2 text-xs text-(--color-9) transition-colors duration-150 hover:bg-(--color-2)"
282282
onClick={() => copyText(user?.email, t('messages.copiedEmail'))}
283283
>
284284
<MdrIcon icon={<Mail />} size={16} />
@@ -287,7 +287,7 @@ export const ProfilePage = () => {
287287
<MdrIcon icon={<Copy />} size={14} />
288288
</span>
289289
</button>
290-
<div className="inline-flex cursor-default items-center gap-2 rounded-full border-0 bg-black/[0.04] px-3 py-2 text-xs text-[#101010]">
290+
<div className="inline-flex cursor-default items-center gap-2 rounded-full border-0 bg-(--color-1) px-3 py-2 text-xs text-(--color-9)">
291291
<MdrIcon icon={<Calendar />} size={16} />
292292
<span>{formatDate(user?.createdAt)}</span>
293293
</div>
@@ -327,15 +327,15 @@ export const ProfilePage = () => {
327327
/>
328328
)}
329329
<div className="grid gap-3.5">
330-
<label className="grid gap-1.5 text-xs text-[#5c5c5c]">
330+
<label className="grid gap-1.5 text-xs text-(--color-7)">
331331
<span>{t('labels.name')}</span>
332332
<MdrInput
333333
size="Small"
334334
value={draft.name}
335335
onChange={(value) => setDraft((p) => ({ ...p, name: value }))}
336336
/>
337337
</label>
338-
<label className="grid gap-1.5 text-xs text-[#5c5c5c]">
338+
<label className="grid gap-1.5 text-xs text-(--color-7)">
339339
<span>{t('labels.description')}</span>
340340
<MdrTextarea
341341
size="Small"

apps/web/src/editor/__tests__/EditorHome.test.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@ describe('EditorHome', () => {
4040
});
4141

4242
it('opens the new resource modal when clicking the create button', async () => {
43-
listProjectsMock.mockResolvedValue({ projects: [] });
43+
listProjectsMock.mockImplementation(
44+
() => new Promise<{ projects: [] }>(() => undefined)
45+
);
4446
render(<EditorHome />);
4547

4648
fireEvent.click(

apps/web/src/editor/features/design/BlueprintEditor.autosave.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,10 +162,10 @@ export const useBlueprintAutosave = ({
162162
if (!token) return;
163163
if (hasWorkspaceTarget && !workspaceCapabilitiesLoaded) return;
164164
if (lastQueuedSaveDocRef.current === mirDoc) return;
165-
lastQueuedSaveDocRef.current = mirDoc;
166165
let disposed = false;
167166

168167
const timeoutId = window.setTimeout(() => {
168+
lastQueuedSaveDocRef.current = mirDoc;
169169
const validation = validateMirDocument(mirDoc);
170170
if (validation.hasError) {
171171
setSaveTransport(null);

apps/web/src/editor/features/design/__tests__/BlueprintEditorInspector.layout.test.tsx

Lines changed: 46 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { fireEvent, render, screen } from '@testing-library/react';
1+
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
22
import type { KeyboardEvent as ReactKeyboardEvent } from 'react';
33
import { beforeEach, describe, expect, it, vi } from 'vitest';
44
import { BlueprintEditorInspector } from '../BlueprintEditorInspector';
@@ -65,8 +65,19 @@ beforeEach(() => {
6565
resetEditorStore();
6666
});
6767

68+
const ensureLayoutGroupButton = async (groupName: 'Grid' | 'Spacing') => {
69+
if (!screen.queryByRole('button', { name: 'Layout' })) {
70+
const styleToggle = await screen.findByRole('button', { name: 'Style' });
71+
fireEvent.click(styleToggle);
72+
}
73+
if (!screen.queryByRole('button', { name: groupName })) {
74+
fireEvent.click(await screen.findByRole('button', { name: 'Layout' }));
75+
}
76+
return await screen.findByRole('button', { name: groupName });
77+
};
78+
6879
describe('BlueprintEditorInspector layout panel', () => {
69-
it('updates gap for a Flex node', () => {
80+
it('updates gap for a Flex node', async () => {
7081
resetEditorStore({
7182
mirDoc: createMirDoc([
7283
{
@@ -91,15 +102,15 @@ describe('BlueprintEditorInspector layout panel', () => {
91102
/>
92103
);
93104

94-
fireEvent.change(screen.getByPlaceholderText('8'), {
105+
fireEvent.change(await screen.findByPlaceholderText('8'), {
95106
target: { value: '24' },
96107
});
97108

98109
const child = useEditorStore.getState().mirDoc.ui.root.children?.[0];
99110
expect(child?.props?.gap).toBe(24);
100111
});
101112

102-
it('updates gridTemplateColumns for a Grid node', () => {
113+
it('updates gridTemplateColumns for a Grid node', async () => {
103114
resetEditorStore({
104115
mirDoc: createMirDoc([
105116
{
@@ -125,15 +136,25 @@ describe('BlueprintEditorInspector layout panel', () => {
125136
/>
126137
);
127138

139+
fireEvent.click(await ensureLayoutGroupButton('Grid'));
140+
await waitFor(() => {
141+
expect(screen.getAllByTestId('mdr-input').length).toBeGreaterThanOrEqual(
142+
2
143+
);
144+
});
128145
const inputs = screen.getAllByTestId('mdr-input') as HTMLInputElement[];
129146
// [0] id, [1] columns (gap uses editor-only UnitInput)
130147
fireEvent.change(inputs[1], { target: { value: '3' } });
131148

132-
const child = useEditorStore.getState().mirDoc.ui.root.children?.[0];
133-
expect(child?.style?.gridTemplateColumns).toBe('repeat(3, minmax(0, 1fr))');
149+
await waitFor(() => {
150+
const child = useEditorStore.getState().mirDoc.ui.root.children?.[0];
151+
expect(child?.style?.gridTemplateColumns).toBe(
152+
'repeat(3, minmax(0, 1fr))'
153+
);
154+
});
134155
});
135156

136-
it('keeps margin shorthand and per-side inputs in sync', () => {
157+
it('keeps margin shorthand and per-side inputs in sync', async () => {
137158
resetEditorStore({
138159
mirDoc: createMirDoc([
139160
{
@@ -157,25 +178,27 @@ describe('BlueprintEditorInspector layout panel', () => {
157178
/>
158179
);
159180

160-
const shorthandInput = screen.getByTestId(
181+
fireEvent.click(await ensureLayoutGroupButton('Spacing'));
182+
183+
const shorthandInput = (await screen.findByTestId(
161184
'inspector-margin-shorthand'
162-
) as HTMLInputElement;
185+
)) as HTMLInputElement;
163186
fireEvent.change(shorthandInput, { target: { value: '10px 20px' } });
164187

165-
fireEvent.click(screen.getByTestId('inspector-margin-toggle'));
166-
167-
const topInput = screen
168-
.getByTestId('inspector-margin-top')
169-
.querySelector('input') as HTMLInputElement;
170-
const rightInput = screen
171-
.getByTestId('inspector-margin-right')
172-
.querySelector('input') as HTMLInputElement;
173-
const bottomInput = screen
174-
.getByTestId('inspector-margin-bottom')
175-
.querySelector('input') as HTMLInputElement;
176-
const leftInput = screen
177-
.getByTestId('inspector-margin-left')
178-
.querySelector('input') as HTMLInputElement;
188+
fireEvent.click(await screen.findByTestId('inspector-margin-toggle'));
189+
190+
const topInput = (
191+
await screen.findByTestId('inspector-margin-top')
192+
).querySelector('input') as HTMLInputElement;
193+
const rightInput = (
194+
await screen.findByTestId('inspector-margin-right')
195+
).querySelector('input') as HTMLInputElement;
196+
const bottomInput = (
197+
await screen.findByTestId('inspector-margin-bottom')
198+
).querySelector('input') as HTMLInputElement;
199+
const leftInput = (
200+
await screen.findByTestId('inspector-margin-left')
201+
).querySelector('input') as HTMLInputElement;
179202

180203
expect(topInput.value).toBe('10');
181204
expect(rightInput.value).toBe('20');

apps/web/src/editor/features/design/blueprint/external/runtime/__tests__/externalState.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,12 @@ describe('external library state', () => {
5757
const diagnostics = await mod.ensureExternalLibraryById('unknown');
5858
expect(diagnostics[0]?.code).toBe('ELIB-1004');
5959
expect(mod.getExternalLibraryState('unknown').status).toBe('error');
60-
});
60+
}, 15000);
6161

6262
it('tracks configured library states for success and failure', async () => {
6363
const mod = await loadExternalModule();
6464
await mod.ensureConfiguredExternalLibraries(['antd', 'mui']);
6565
expect(mod.getExternalLibraryState('antd').status).toBe('success');
6666
expect(mod.getExternalLibraryState('mui').status).toBe('error');
67-
});
67+
}, 15000);
6868
});

apps/web/src/editor/features/design/inspector/classProtocol/MountedCssEditorModal.tsx

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useMemo, useRef } from 'react';
1+
import { useEffect, useMemo, useRef, useState } from 'react';
22
import CodeMirror from '@uiw/react-codemirror';
33
import {
44
Compartment,
@@ -92,16 +92,23 @@ export function MountedCssEditorModal({
9292
onClose,
9393
onSave,
9494
}: MountedCssEditorModalProps) {
95+
const resolveTheme = () => {
96+
if (typeof document === 'undefined') return 'light';
97+
const theme = document.documentElement.getAttribute('data-theme');
98+
return theme === 'dark' ? 'dark' : 'light';
99+
};
95100
const { t } = useTranslation('blueprint');
101+
const [editorTheme, setEditorTheme] = useState<'light' | 'dark'>(
102+
() => resolveTheme() as 'light' | 'dark'
103+
);
96104
const invalidSyntaxMessage = t(
97105
'inspector.classProtocol.mountedCss.invalidSyntax',
98106
{
99107
defaultValue: DEFAULT_INVALID_CSS_MESSAGE,
100108
}
101109
);
102-
const lintCompartmentRef = useRef(new Compartment());
110+
const [lintCompartment] = useState(() => new Compartment());
103111
const extensions = useMemo(() => {
104-
const lintCompartment = lintCompartmentRef.current;
105112
const colorGutter = gutter({
106113
class: 'MountedCssColorGutter',
107114
markers(view) {
@@ -150,18 +157,40 @@ export function MountedCssEditorModal({
150157
lintGutter(),
151158
lintTheme,
152159
];
153-
}, []);
160+
}, [lintCompartment]);
154161
const editorRef = useRef<EditorView | null>(null);
155162

163+
useEffect(() => {
164+
if (typeof document === 'undefined') return;
165+
const root = document.documentElement;
166+
const syncTheme = () => {
167+
const nextTheme = root.getAttribute('data-theme');
168+
setEditorTheme(nextTheme === 'dark' ? 'dark' : 'light');
169+
};
170+
syncTheme();
171+
const observer = new MutationObserver((mutations) => {
172+
if (
173+
mutations.some((mutation) => mutation.attributeName === 'data-theme')
174+
) {
175+
syncTheme();
176+
}
177+
});
178+
observer.observe(root, {
179+
attributes: true,
180+
attributeFilter: ['data-theme'],
181+
});
182+
return () => observer.disconnect();
183+
}, []);
184+
156185
useEffect(() => {
157186
const editor = editorRef.current;
158187
if (!editor) return;
159188
editor.dispatch({
160-
effects: lintCompartmentRef.current.reconfigure(
189+
effects: lintCompartment.reconfigure(
161190
createSyntaxLinterExtension(invalidSyntaxMessage)
162191
),
163192
});
164-
}, [invalidSyntaxMessage]);
193+
}, [invalidSyntaxMessage, lintCompartment]);
165194

166195
useEffect(() => {
167196
if (!isOpen) return;
@@ -243,12 +272,12 @@ export function MountedCssEditorModal({
243272
value={value || DEFAULT_CSS_CONTENT}
244273
height="100%"
245274
extensions={extensions}
246-
theme="light"
275+
theme={editorTheme}
247276
onChange={(next) => onChange(next)}
248277
onCreateEditor={(view) => {
249278
editorRef.current = view;
250279
view.dispatch({
251-
effects: lintCompartmentRef.current.reconfigure(
280+
effects: lintCompartment.reconfigure(
252281
createSyntaxLinterExtension(invalidSyntaxMessage)
253282
),
254283
});

apps/web/src/editor/features/design/inspector/sections/basic/InspectorDataScopeFields.tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@ const asSchemaText = (value: unknown) => {
1616

1717
export function InspectorDataScopeFields() {
1818
const { t, selectedNode, updateSelectedNode } = useInspectorSectionContext();
19-
if (!selectedNode) return null;
19+
const selectedNodeData = selectedNode?.data as
20+
| Record<string, unknown>
21+
| undefined;
2022

2123
const mountedDataModel = useMemo(() => {
22-
const data = selectedNode.data as Record<string, unknown> | undefined;
24+
const data = selectedNodeData;
2325
if (isPlainObject(data?.value)) {
2426
return data.value;
2527
}
@@ -34,24 +36,24 @@ export function InspectorDataScopeFields() {
3436
const legacy =
3537
data?.[LEGACY_DATA_MODEL_KEY] ?? data?.[LEGACY_DATA_SCHEMA_KEY];
3638
return isPlainObject(legacy) ? legacy : {};
37-
}, [selectedNode.data]);
39+
}, [selectedNodeData]);
3840
const mountedMockData = useMemo(() => {
39-
const data = selectedNode.data as Record<string, unknown> | undefined;
41+
const data = selectedNodeData;
4042
if (data?.mock !== undefined) {
4143
return data.mock;
4244
}
4345
if (Array.isArray(data?.value)) {
4446
return data.value;
4547
}
4648
return {};
47-
}, [selectedNode.data]);
49+
}, [selectedNodeData]);
4850
const [schemaDraft, setSchemaDraft] = useState(
4951
asSchemaText(mountedDataModel)
5052
);
5153
const [mockDraft, setMockDraft] = useState(asSchemaText(mountedMockData));
5254
const [schemaError, setSchemaError] = useState<string | null>(null);
5355
const [mockError, setMockError] = useState<string | null>(null);
54-
const isMounted = selectedNode.data !== undefined;
56+
const isMounted = selectedNodeData !== undefined;
5557

5658
useEffect(() => {
5759
setSchemaDraft(asSchemaText(mountedDataModel));
@@ -62,6 +64,8 @@ export function InspectorDataScopeFields() {
6264
setMockError(null);
6365
}, [mountedMockData]);
6466

67+
if (!selectedNode) return null;
68+
6569
const applySchemaDraft = () => {
6670
const raw = schemaDraft.trim();
6771
if (!raw) {

0 commit comments

Comments
 (0)