@linhey/rhine-design 0.9.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/CONTRIBUTING.md +3 -1
- package/README.md +18 -15
- package/components/AdvancedForms.tsx +22 -8
- package/components/COMMON_COMPONENTS.md +6 -3
- package/components/Forms.tsx +14 -1
- package/components/ListFilterBar.tsx +5 -3
- package/components/Overlays.tsx +3 -2
- package/components/README.md +13 -5
- package/components/SPACING.md +9 -0
- package/components/TYPOGRAPHY.md +2 -0
- package/components/Toast.tsx +10 -4
- package/components/checkbox-row.css +12 -0
- package/components/common.css +3 -1
- package/components/compact-forms.css +20 -0
- package/components/components.tsx +13 -7
- package/components/index.ts +1 -0
- package/components/media-parity.css +4 -4
- package/components/patterns.css +3 -3
- package/components/styles.css +17 -2
- package/components/theme-picker.css +2 -2
- package/docs/APPLICATION_COMPONENTS.md +2 -2
- package/docs/CHECKBOX_ROWS.md +36 -0
- package/docs/COMPACT_FILTER_CONTROLS.md +38 -0
- package/docs/COMPONENT_TYPOGRAPHY_AUDIT.md +44 -0
- package/docs/DESIGN_SYSTEM.md +10 -1
- package/docs/DESIGN_SYSTEM_AUDIT.md +4 -4
- package/docs/DISCLOSURE_VERIFICATION.md +49 -0
- package/docs/INTERACTION_PATTERNS.md +9 -1
- package/docs/LIST_FILTER_BAR.md +5 -1
- package/docs/REVIEW_TOOL.md +128 -0
- package/docs/REVIEW_VERIFICATION.md +56 -0
- package/docs/SUMMARY_TABS.md +77 -0
- package/docs/THEMING.md +1 -1
- package/docs/assets/disclosure/arknights-400.png +0 -0
- package/docs/assets/disclosure/neo-brutalism-400.png +0 -0
- package/docs/assets/disclosure/rhine-400.png +0 -0
- package/docs/assets/disclosure/rhine-native200.png +0 -0
- package/examples/review/http/App.tsx +46 -0
- package/examples/review/http/catalog.tsx +44 -0
- package/examples/review/http/details.ts +1 -0
- package/examples/review/http/vite.config.ts +8 -0
- package/examples/review/platform/Workspace.tsx +34 -0
- package/examples/review/platform/catalog.tsx +46 -0
- package/examples/review/platform/vite.config.ts +8 -0
- package/examples/review/product.css +3 -0
- package/package.json +17 -5
- package/preview/CheckboxRowGallery.tsx +26 -0
- package/preview/CommonGallery.tsx +1 -1
- package/preview/CompactFilterBarGallery.tsx +19 -0
- package/preview/ComponentTypographyGallery.tsx +23 -0
- package/preview/DenseFilterControlsGallery.tsx +21 -0
- package/preview/GalleryShell.tsx +4 -3
- package/preview/OrbitGallery.tsx +1 -1
- package/preview/SpacingGallery.tsx +9 -1
- package/preview/SummaryTabsGallery.tsx +37 -0
- package/preview/main.tsx +14 -3
- package/preview/packageInfo.ts +5 -0
- package/review/canvas.tsx +107 -0
- package/review/dev-client.mjs +17 -0
- package/review/feedback.ts +17 -0
- package/review/index.ts +2 -0
- package/review/isolation.ts +64 -0
- package/review/lifecycle.ts +59 -0
- package/review/protocol.ts +7 -0
- package/review/review.css +41 -0
- package/review/shell.tsx +165 -0
- package/review/state.ts +41 -0
- package/review/types.ts +59 -0
- package/review/vite.d.mts +10 -0
- package/review/vite.mjs +107 -0
- package/scripts/check-checkbox-rows.js +65 -0
- package/scripts/check-component-typography.js +76 -0
- package/scripts/check-dense-filters.js +60 -0
- package/scripts/check-review-package.mjs +11 -0
- package/scripts/check-review-recovery.js +47 -0
- package/scripts/check-review.js +153 -0
- package/skills/rhine-design/SKILL.md +2 -2
- package/skills/rhine-design/references/API_MAP.md +6 -0
- package/skills/rhine-design/references/TASK_MAP.md +2 -0
- package/skills/rhine-design/references/integration.md +1 -1
- package/tests/CheckboxRow.test.tsx +53 -0
- package/tests/CommonComponents.test.tsx +8 -0
- package/tests/CompactFilterBar.test.tsx +50 -0
- package/tests/DenseFilterControls.test.tsx +79 -0
- package/tests/DesignSystem.test.tsx +32 -0
- package/tests/Review.test.tsx +117 -0
- package/tests/ReviewBuild.test.tsx +59 -0
- package/tests/ReviewShell.test.tsx +63 -0
- package/tsconfig.json +5 -2
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
3
|
+
import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join, resolve } from 'node:path';
|
|
6
|
+
import { build, preview } from 'vite';
|
|
7
|
+
import react from '@vitejs/plugin-react';
|
|
8
|
+
import { reviewPlugin, reviewProductionGuard } from '../review/vite.mjs';
|
|
9
|
+
|
|
10
|
+
const directories: string[] = [];
|
|
11
|
+
async function fixture() { const path = await mkdtemp(join(tmpdir(), 'rhine-review-test-')); directories.push(path); return path; }
|
|
12
|
+
afterEach(async () => { await Promise.all(directories.splice(0).map(path => rm(path, { recursive: true, force: true }))); });
|
|
13
|
+
describe('review build integration', () => {
|
|
14
|
+
it.each(['/nested/review/', './'])('emits final CSS and usable preview for base %s', async base => {
|
|
15
|
+
const root = await fixture();
|
|
16
|
+
await build({ configFile: false, root, base, logLevel: 'silent', plugins: [react(), reviewPlugin({ catalog: resolve('examples/review/http/catalog.tsx') })] });
|
|
17
|
+
for (const name of ['index.html', 'canvas.html']) {
|
|
18
|
+
const html = await readFile(join(root, 'dist', name), 'utf8');
|
|
19
|
+
const stylesheet = html.match(/rel="stylesheet" href="([^\"]+\.css)"/)?.[1];
|
|
20
|
+
expect(stylesheet?.startsWith(base)).toBe(true);
|
|
21
|
+
const css = stylesheet?.slice(base.length);
|
|
22
|
+
expect(css).toBeTruthy();
|
|
23
|
+
expect(await readFile(join(root, 'dist', css!), 'utf8')).toContain('.rhine-review-header');
|
|
24
|
+
expect(html).toContain("connect-src 'none'");
|
|
25
|
+
expect(html).toContain(`src="${base}assets/`);
|
|
26
|
+
}
|
|
27
|
+
const server = await preview({ configFile: false, root, base, logLevel: 'silent', plugins: [reviewPlugin({ catalog: resolve('examples/review/http/catalog.tsx') })], preview: { host: '127.0.0.1', port: 0 } });
|
|
28
|
+
try {
|
|
29
|
+
const address = server.httpServer.address();
|
|
30
|
+
if (!address || typeof address === 'string') throw new Error('Missing preview port');
|
|
31
|
+
const url = `http://127.0.0.1:${address.port}${base === './' ? '/' : base}`;
|
|
32
|
+
const response = await fetch(url);
|
|
33
|
+
expect(response.status).toBe(200);
|
|
34
|
+
expect(await response.text()).toContain('Rhine Review');
|
|
35
|
+
expect((await fetch(`${url}api/missing`)).status).toBe(404);
|
|
36
|
+
} finally { await new Promise<void>((resolve, reject) => server.httpServer.close(error => error ? reject(error) : resolve())); }
|
|
37
|
+
}, 20000);
|
|
38
|
+
it('allows production theme directories but rejects explicit fixture paths', async () => {
|
|
39
|
+
const root = await fixture();
|
|
40
|
+
await mkdir(join(root, 'design')); await mkdir(join(root, 'fixtures'));
|
|
41
|
+
await writeFile(join(root, 'design/theme.js'), 'export const theme = "synthetic-theme";');
|
|
42
|
+
await writeFile(join(root, 'fixtures/data.js'), 'export const data = "synthetic-fixture";');
|
|
43
|
+
const entry = join(root, 'entry.js');
|
|
44
|
+
const options = { configFile: false as const, root, logLevel: 'silent' as const, plugins: [reviewProductionGuard({ forbidden: ['./fixtures'] })], build: { lib: { entry, formats: ['es' as const] } } };
|
|
45
|
+
await writeFile(entry, 'export {theme} from "./design/theme.js";');
|
|
46
|
+
await expect(build(options)).resolves.toBeTruthy();
|
|
47
|
+
await writeFile(entry, 'export {data} from "./fixtures/data.js";');
|
|
48
|
+
await expect(build(options)).rejects.toThrow('forbidden fixtures: fixtures/data.js');
|
|
49
|
+
}, 20000);
|
|
50
|
+
it('rejects accidental official runtime inclusion without a product denylist', async () => {
|
|
51
|
+
const root = await fixture(); const entry = join(root, 'entry.js');
|
|
52
|
+
await writeFile(entry, `export { defineReview } from ${JSON.stringify(resolve('review/index.ts'))};`);
|
|
53
|
+
await expect(build({ configFile: false, root, logLevel: 'silent', plugins: [reviewProductionGuard()], build: { lib: { entry, formats: ['es'] } } })).rejects.toThrow('Production bundle imports review runtime');
|
|
54
|
+
}, 20000);
|
|
55
|
+
it('refuses backend proxy and project env configuration', async () => {
|
|
56
|
+
const root = await fixture();
|
|
57
|
+
await expect(build({ configFile: false, root, logLevel: 'silent', plugins: [reviewPlugin({ catalog: './catalog.tsx' })], server: { proxy: { '/api': 'http://synthetic.invalid' } } })).rejects.toThrow('no server/preview proxies');
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { ReviewShell, reviewWaitMs } from '../review/shell';
|
|
4
|
+
import { appendFeedback, emptyFeedback } from '../review/feedback';
|
|
5
|
+
import { protocol } from '../review/protocol';
|
|
6
|
+
|
|
7
|
+
const catalog = { title: '故障恢复', scenes: [{ id: 'a', title: '场景 A' }, { id: 'b', title: '场景 B' }] };
|
|
8
|
+
function iframe() { return screen.getByTitle('产品场景画布') as HTMLIFrameElement; }
|
|
9
|
+
function send(data: Record<string, unknown>, source = iframe().contentWindow!) {
|
|
10
|
+
fireEvent(window, new MessageEvent('message', { origin: location.origin, source, data: { protocol, ...data } }));
|
|
11
|
+
}
|
|
12
|
+
function connect() {
|
|
13
|
+
const post = vi.spyOn(iframe().contentWindow!, 'postMessage');
|
|
14
|
+
send({ type: 'ready', catalog });
|
|
15
|
+
return { post, request: () => post.mock.calls.filter(call => call[0].type === 'select').at(-1)![0].requestId as number };
|
|
16
|
+
}
|
|
17
|
+
beforeEach(() => { vi.useFakeTimers(); history.replaceState(null, '', '#scene=a'); });
|
|
18
|
+
afterEach(() => { cleanup(); vi.useRealTimers(); });
|
|
19
|
+
describe('review shell recovery', () => {
|
|
20
|
+
it('offers recovery when the initial iframe never handshakes', () => {
|
|
21
|
+
render(<ReviewShell />); const oldFrame = iframe();
|
|
22
|
+
act(() => vi.advanceTimersByTime(reviewWaitMs));
|
|
23
|
+
expect(screen.getByRole('alert')).toHaveTextContent('连接画布超时');
|
|
24
|
+
fireEvent.click(screen.getByRole('button', { name: '重新载入画布' }));
|
|
25
|
+
expect(iframe()).not.toBe(oldFrame);
|
|
26
|
+
const { request } = connect(); send({ type: 'rendered', requestId: request() });
|
|
27
|
+
expect(screen.getByRole('status')).toHaveTextContent('已就绪');
|
|
28
|
+
expect(screen.getByRole('alert')).toHaveTextContent('连接画布超时');
|
|
29
|
+
});
|
|
30
|
+
it('replaces a stuck browsing context, keeps selection, ignores old acknowledgements', () => {
|
|
31
|
+
render(<ReviewShell />); const { request } = connect();
|
|
32
|
+
send({ type: 'rendered', requestId: request() });
|
|
33
|
+
fireEvent.click(screen.getByRole('link', { name: '场景 B' }));
|
|
34
|
+
const pending = request();
|
|
35
|
+
send({ type: 'rendered', requestId: pending - 1 });
|
|
36
|
+
act(() => vi.advanceTimersByTime(reviewWaitMs));
|
|
37
|
+
expect(screen.getByRole('alert')).toHaveTextContent('初始化或清理超过');
|
|
38
|
+
const oldFrame = iframe(), oldWindow = oldFrame.contentWindow!;
|
|
39
|
+
fireEvent.click(screen.getByRole('button', { name: '重新载入画布' }));
|
|
40
|
+
expect(oldFrame.isConnected).toBe(false); expect(location.hash).toContain('scene=b');
|
|
41
|
+
send({ type: 'feedback', kind: 'error', message: 'stale error' }, oldWindow);
|
|
42
|
+
expect(screen.queryByText(/stale error/)).not.toBeInTheDocument();
|
|
43
|
+
const next = connect(); send({ type: 'rendered', requestId: next.request() });
|
|
44
|
+
expect(screen.getByRole('status')).toHaveTextContent('已就绪');
|
|
45
|
+
});
|
|
46
|
+
it('retains scoped errors across ready and selection until explicit acknowledgement', () => {
|
|
47
|
+
render(<ReviewShell />); const { request } = connect();
|
|
48
|
+
send({ type: 'feedback', scene: 'a', kind: 'error', message: '清理失败 A' });
|
|
49
|
+
send({ type: 'rendered', requestId: request() });
|
|
50
|
+
fireEvent.click(screen.getByRole('link', { name: '场景 B' }));
|
|
51
|
+
send({ type: 'rendered', requestId: request() });
|
|
52
|
+
expect(screen.getByRole('alert')).toHaveTextContent('清理失败 A');
|
|
53
|
+
fireEvent.click(screen.getByText(/评审反馈 ·/));
|
|
54
|
+
expect(screen.getByText('场景 A', { selector: 'strong' })).toBeInTheDocument();
|
|
55
|
+
fireEvent.click(screen.getByRole('button', { name: '确认并清除反馈' }));
|
|
56
|
+
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
|
|
57
|
+
});
|
|
58
|
+
it('does not silently lose unacknowledged errors when bounded history overflows', () => {
|
|
59
|
+
let log = emptyFeedback();
|
|
60
|
+
for (let id = 0; id < 30; id++) log = appendFeedback(log, { id, kind: id < 4 ? 'error' : 'info', message: String(id), scene: 'a' });
|
|
61
|
+
expect(log.entries).toHaveLength(20); expect(log.errors).toBe(4); expect(log.omittedErrors).toBe(4);
|
|
62
|
+
});
|
|
63
|
+
});
|
package/tsconfig.json
CHANGED
|
@@ -20,13 +20,16 @@
|
|
|
20
20
|
"types": [
|
|
21
21
|
"vite/client",
|
|
22
22
|
"node"
|
|
23
|
-
]
|
|
23
|
+
],
|
|
24
|
+
"allowJs": true
|
|
24
25
|
},
|
|
25
26
|
"include": [
|
|
26
27
|
"components",
|
|
27
28
|
"preview",
|
|
28
29
|
"tests",
|
|
29
30
|
"vite.config.ts",
|
|
30
|
-
"vitest.config.ts"
|
|
31
|
+
"vitest.config.ts",
|
|
32
|
+
"review",
|
|
33
|
+
"examples/review"
|
|
31
34
|
]
|
|
32
35
|
}
|