@linhey/rhine-design 0.3.0 → 0.4.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 +33 -0
- package/README.md +53 -14
- package/components/Actions.tsx +2 -0
- package/components/COMMON_COMPONENTS.md +5 -1
- package/components/FormWorkflow.tsx +25 -0
- package/components/MOTION.md +6 -2
- package/components/Navigation.tsx +42 -0
- package/components/Overlays.tsx +9 -3
- package/components/README.md +13 -5
- package/components/SPACING.md +82 -0
- package/components/StatusTimeline.tsx +29 -0
- package/components/TYPOGRAPHY.md +34 -2
- package/components/common.css +12 -3
- package/components/index.ts +7 -1
- package/components/patterns.css +39 -0
- package/components/styles.css +12 -8
- package/components/theme-arknights.css +301 -0
- package/components/theme-neo-brutalism.css +84 -0
- package/components/theme.tsx +58 -0
- package/components/timeline.css +23 -0
- package/components/tokens.css +6 -1
- package/docs/DESIGN_SYSTEM.md +12 -2
- package/docs/DESIGN_SYSTEM_AUDIT.md +4 -4
- package/docs/INTERACTION_PATTERNS.md +129 -0
- package/docs/REFERENCE_RESEARCH.md +36 -0
- package/docs/THEME_NEO_BRUTALISM.md +37 -0
- package/docs/THEMING.md +104 -0
- package/package.json +5 -1
- package/preview/CommonGallery.tsx +8 -8
- package/preview/GalleryShell.tsx +106 -0
- package/preview/LineHeightGallery.tsx +27 -0
- package/preview/MotionGallery.tsx +6 -2
- package/preview/PatternGallery.tsx +52 -0
- package/preview/SpacingGallery.tsx +32 -0
- package/preview/SubscriptionGallery.tsx +84 -0
- package/preview/line-height.css +2 -0
- package/preview/main.tsx +39 -25
- package/preview/patterns.css +7 -0
- package/preview/preview.css +2 -8
- package/preview/shell.css +35 -0
- package/preview/spacing.css +2 -0
- package/preview/subscriptions.css +27 -0
- package/preview/theme-arknights.css +234 -0
- package/preview/theme-neo-brutalism.css +11 -0
- package/skills/rhine-design/SKILL.md +5 -3
- package/skills/rhine-design/references/API_MAP.md +7 -3
- package/skills/rhine-design/references/TASK_MAP.md +4 -1
- package/skills/rhine-design/references/integration.md +4 -2
- package/tests/GalleryShell.test.tsx +98 -0
- package/tests/Patterns.test.tsx +75 -0
- package/tests/StatusTimeline.test.tsx +15 -0
- package/tests/Theme.test.tsx +112 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { fireEvent, render, screen, within } from '@testing-library/react';
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { ThemeProvider, themes, useTheme, type ThemeName } from '@linhey/rhine-design/theme';
|
|
5
|
+
import { Button, Dialog } from '../components';
|
|
6
|
+
|
|
7
|
+
function Controls() {
|
|
8
|
+
const { theme, setTheme } = useTheme();
|
|
9
|
+
const [count, setCount] = useState(0);
|
|
10
|
+
return <>
|
|
11
|
+
<output aria-label="当前主题">{theme}</output>
|
|
12
|
+
{themes.map(item => <button key={item.name} onClick={() => setTheme(item.name)}>{item.label}</button>)}
|
|
13
|
+
<button onClick={() => setCount(count + 1)}>计数 {count}</button>
|
|
14
|
+
<input aria-label="草稿" defaultValue="" />
|
|
15
|
+
</>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('public theme API', () => {
|
|
19
|
+
it('defaults to rhine and preserves the DOM, state and input across all themes', () => {
|
|
20
|
+
const onThemeChange = vi.fn();
|
|
21
|
+
render(<ThemeProvider onThemeChange={onThemeChange} className="host" id="app" data-rhine-motion="reduced"><Controls /></ThemeProvider>);
|
|
22
|
+
const root = document.getElementById('app')!;
|
|
23
|
+
expect(root).toHaveClass('rhine-theme', 'host');
|
|
24
|
+
expect(root).toHaveAttribute('data-rhine-motion', 'reduced');
|
|
25
|
+
expect(root).toHaveAttribute('data-rhine-theme', 'rhine');
|
|
26
|
+
fireEvent.click(screen.getByRole('button', { name: '计数 0' }));
|
|
27
|
+
const input = screen.getByRole('textbox', { name: '草稿' });
|
|
28
|
+
fireEvent.change(input, { target: { value: '未保存的内容' } });
|
|
29
|
+
for (const name of ['arknights', 'neo-brutalism', 'rhine'] as const) {
|
|
30
|
+
fireEvent.click(screen.getByRole('button', { name: themes.find(item => item.name === name)!.label }));
|
|
31
|
+
expect(root).toHaveAttribute('data-rhine-theme', name);
|
|
32
|
+
expect(screen.getByLabelText('当前主题')).toHaveTextContent(name);
|
|
33
|
+
expect(screen.getByRole('textbox')).toBe(input);
|
|
34
|
+
expect(input).toHaveValue('未保存的内容');
|
|
35
|
+
expect(screen.getByRole('button', { name: '计数 1' })).toBeInTheDocument();
|
|
36
|
+
}
|
|
37
|
+
expect(onThemeChange.mock.calls).toEqual([['arknights'], ['neo-brutalism'], ['rhine']]);
|
|
38
|
+
fireEvent.click(screen.getByRole('button', { name: '莱茵 · 纸面' }));
|
|
39
|
+
expect(onThemeChange).toHaveBeenCalledTimes(3);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('uses defaultTheme only on mount', () => {
|
|
43
|
+
const { rerender } = render(<ThemeProvider defaultTheme="arknights"><Controls /></ThemeProvider>);
|
|
44
|
+
expect(screen.getByLabelText('当前主题')).toHaveTextContent('arknights');
|
|
45
|
+
rerender(<ThemeProvider defaultTheme="neo-brutalism"><Controls /></ThemeProvider>);
|
|
46
|
+
expect(screen.getByLabelText('当前主题')).toHaveTextContent('arknights');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('leaves controlled updates to the host and follows subsequent props', () => {
|
|
50
|
+
const onThemeChange = vi.fn();
|
|
51
|
+
const { rerender } = render(<ThemeProvider theme="rhine" onThemeChange={onThemeChange}><Controls /></ThemeProvider>);
|
|
52
|
+
fireEvent.click(screen.getByRole('button', { name: '明日方舟 · 战术终端' }));
|
|
53
|
+
expect(onThemeChange).toHaveBeenCalledWith('arknights');
|
|
54
|
+
expect(screen.getByLabelText('当前主题')).toHaveTextContent('rhine');
|
|
55
|
+
rerender(<ThemeProvider theme="arknights" onThemeChange={onThemeChange}><Controls /></ThemeProvider>);
|
|
56
|
+
expect(screen.getByLabelText('当前主题')).toHaveTextContent('arknights');
|
|
57
|
+
expect(onThemeChange).toHaveBeenCalledTimes(1);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('keeps sibling providers independent', () => {
|
|
61
|
+
render(<><ThemeProvider aria-label="A"><Controls /></ThemeProvider><ThemeProvider aria-label="B" defaultTheme="neo-brutalism"><Controls /></ThemeProvider></>);
|
|
62
|
+
const a = screen.getByLabelText('A'), b = screen.getByLabelText('B');
|
|
63
|
+
fireEvent.click(within(a).getByRole('button', { name: '明日方舟 · 战术终端' }));
|
|
64
|
+
expect(a).toHaveAttribute('data-rhine-theme', 'arknights');
|
|
65
|
+
expect(b).toHaveAttribute('data-rhine-theme', 'neo-brutalism');
|
|
66
|
+
expect(within(b).getByLabelText('当前主题')).toHaveTextContent('neo-brutalism');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('does not read or write host storage', () => {
|
|
70
|
+
const read = vi.spyOn(Storage.prototype, 'getItem');
|
|
71
|
+
const write = vi.spyOn(Storage.prototype, 'setItem');
|
|
72
|
+
try {
|
|
73
|
+
render(<ThemeProvider><Controls /></ThemeProvider>);
|
|
74
|
+
fireEvent.click(screen.getByRole('button', { name: '明日方舟 · 战术终端' }));
|
|
75
|
+
expect(read).not.toHaveBeenCalled();
|
|
76
|
+
expect(write).not.toHaveBeenCalled();
|
|
77
|
+
} finally { read.mockRestore(); write.mockRestore(); }
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('keeps an open dialog in the same theme root, preserving its draft and focus', () => {
|
|
81
|
+
render(<ThemeProvider><Dialog trigger={<Button>打开</Button>} title="主题测试" description="保持浮层状态"><Controls /></Dialog></ThemeProvider>);
|
|
82
|
+
const trigger = screen.getByRole('button', { name: '打开' });
|
|
83
|
+
fireEvent.click(trigger);
|
|
84
|
+
const dialog = screen.getByRole('dialog');
|
|
85
|
+
const root = trigger.closest('.rhine-theme');
|
|
86
|
+
expect(dialog.closest('.rhine-theme')).toBe(root);
|
|
87
|
+
fireEvent.change(within(dialog).getByRole('textbox'), { target: { value: '浮层草稿' } });
|
|
88
|
+
const switcher = within(dialog).getByRole('button', { name: '明日方舟 · 战术终端' });
|
|
89
|
+
switcher.focus();
|
|
90
|
+
fireEvent.click(switcher);
|
|
91
|
+
expect(root).toHaveAttribute('data-rhine-theme', 'arknights');
|
|
92
|
+
expect(screen.getByRole('dialog')).toBe(dialog);
|
|
93
|
+
expect(within(dialog).getByRole('textbox')).toHaveValue('浮层草稿');
|
|
94
|
+
expect(switcher).toHaveFocus();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('explains missing providers, nested providers and invalid theme names', () => {
|
|
98
|
+
expect(() => render(<Controls />)).toThrow('useTheme must be used within');
|
|
99
|
+
expect(() => render(<ThemeProvider><ThemeProvider /></ThemeProvider>)).toThrow('Nested Rhine ThemeProvider is not supported');
|
|
100
|
+
expect(() => render(<ThemeProvider theme={'unknown' as ThemeName} />)).toThrow('Unknown Rhine theme');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('rejects invalid imperative requests without updating state or notifying the host', () => {
|
|
104
|
+
let setTheme: (theme: ThemeName) => void = () => {};
|
|
105
|
+
function Capture() { setTheme = useTheme().setTheme; return null; }
|
|
106
|
+
const onThemeChange = vi.fn();
|
|
107
|
+
render(<ThemeProvider onThemeChange={onThemeChange}><Capture /><Controls /></ThemeProvider>);
|
|
108
|
+
expect(() => setTheme('unknown' as ThemeName)).toThrow('Unknown Rhine theme');
|
|
109
|
+
expect(screen.getByLabelText('当前主题')).toHaveTextContent('rhine');
|
|
110
|
+
expect(onThemeChange).not.toHaveBeenCalled();
|
|
111
|
+
});
|
|
112
|
+
});
|