@linhey/rhine-design 0.11.1 → 0.11.2

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.
Files changed (78) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/CONTRIBUTING.md +2 -0
  3. package/README.md +9 -4
  4. package/components/Actions.tsx +1 -0
  5. package/components/AppShell.tsx +3 -2
  6. package/components/COMMON_COMPONENTS.md +5 -1
  7. package/components/Chat.tsx +160 -0
  8. package/components/Navigation.tsx +15 -4
  9. package/components/Overlays.tsx +37 -7
  10. package/components/README.md +11 -1
  11. package/components/chat.css +32 -0
  12. package/components/components.tsx +19 -9
  13. package/components/dialog-settings.css +54 -1
  14. package/components/extensions.css +4 -0
  15. package/components/index.ts +8 -3
  16. package/components/patterns.css +4 -0
  17. package/components/styles.css +11 -2
  18. package/components/theme-arknights.css +4 -2
  19. package/docs/AGENT_CHAT.md +76 -0
  20. package/docs/AGENT_CHAT_VERIFICATION.md +42 -0
  21. package/docs/APPLICATION_COMPONENTS.md +2 -0
  22. package/docs/BUTTON_ACTIONS.md +32 -0
  23. package/docs/BUTTON_ACTIONS_VERIFICATION.md +14 -0
  24. package/docs/COMPACT_ACCOUNT_PATTERNS.md +16 -0
  25. package/docs/COMPACT_SELECTION_VERIFICATION.md +15 -0
  26. package/docs/FLOATING_OVERLAYS.md +4 -0
  27. package/docs/INTERACTION_PATTERNS.md +4 -0
  28. package/docs/SETTINGS_COMPOSITION_VERIFICATION.md +12 -0
  29. package/docs/SETTINGS_DIALOG.md +42 -4
  30. package/docs/SIDE_NAV_ACTIONS.md +9 -0
  31. package/docs/SINGLE_SEARCH.md +19 -0
  32. package/docs/SINGLE_SEARCH_VERIFICATION.md +15 -0
  33. package/docs/SUMMARY_TABS.md +7 -1
  34. package/docs/TRAY_USAGE.md +34 -0
  35. package/docs/TRAY_USAGE_VERIFICATION.md +13 -0
  36. package/docs/assets/chat/after.png +0 -0
  37. package/docs/assets/chat/arknights.png +0 -0
  38. package/docs/assets/chat/before.png +0 -0
  39. package/docs/assets/chat/neo-brutalism.png +0 -0
  40. package/package.json +1 -1
  41. package/preview/ButtonActionsGallery.tsx +25 -0
  42. package/preview/ChatGallery.tsx +98 -0
  43. package/preview/CompactSelectionGallery.tsx +76 -0
  44. package/preview/GalleryShell.tsx +2 -2
  45. package/preview/NavActionsGallery.tsx +24 -0
  46. package/preview/SettingsCompositionGallery.tsx +38 -0
  47. package/preview/SettingsDialogGallery.tsx +54 -14
  48. package/preview/SingleSearchGallery.tsx +50 -0
  49. package/preview/SummaryTabsGallery.tsx +7 -3
  50. package/preview/TrayUsageGallery.tsx +82 -0
  51. package/preview/chat-gallery.css +1 -0
  52. package/preview/compact-selection.css +9 -0
  53. package/preview/main.tsx +15 -1
  54. package/preview/packageInfo.ts +1 -1
  55. package/preview/settings-dialog-gallery.css +5 -0
  56. package/preview/tray-usage.css +4 -0
  57. package/scripts/check-button-actions.js +34 -0
  58. package/scripts/check-compact-selection.js +40 -0
  59. package/scripts/check-dialog-scroll-boundaries.js +41 -0
  60. package/scripts/check-nav-actions.js +41 -0
  61. package/scripts/check-settings-bounded.js +49 -0
  62. package/scripts/check-settings-composition.js +50 -0
  63. package/scripts/check-settings-interactions.js +93 -0
  64. package/scripts/check-settings-theme.js +1 -5
  65. package/scripts/check-single-search.js +43 -0
  66. package/scripts/check-tray-usage.js +68 -0
  67. package/scripts/prepare-settings-consumer.mjs +21 -0
  68. package/skills/rhine-design/SKILL.md +2 -0
  69. package/skills/rhine-design/references/API_MAP.md +11 -0
  70. package/skills/rhine-design/references/TASK_MAP.md +1 -0
  71. package/skills/rhine-design/references/integration.md +1 -1
  72. package/tests/Chat.test.tsx +84 -0
  73. package/tests/CompactSelection.test.tsx +47 -0
  74. package/tests/NavActions.test.tsx +29 -0
  75. package/tests/SettingsDialog.test.tsx +102 -9
  76. package/tests/SingleSearch.test.tsx +42 -0
  77. package/tests/SkillInstallation.test.tsx +77 -0
  78. package/tests/TrayUsage.test.tsx +70 -0
@@ -0,0 +1,77 @@
1
+ import { cpSync, existsSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
4
+ import { afterEach, describe, expect, it } from 'vitest';
5
+
6
+ const source = resolve(process.env.RHINE_SKILL_SOURCE ?? 'skills/rhine-design');
7
+ const sandboxes: string[] = [];
8
+
9
+ afterEach(() => {
10
+ for (const sandbox of sandboxes.splice(0)) rmSync(sandbox, { recursive: true, force: true });
11
+ });
12
+
13
+ function install(fromPackage: boolean) {
14
+ const sandbox = mkdtempSync(join(tmpdir(), 'rhine-skill-'));
15
+ sandboxes.push(sandbox);
16
+ const target = join(sandbox, fromPackage ? '.agents/skills/rhine-design' : 'skills/rhine-design');
17
+ if (fromPackage) {
18
+ const packaged = join(sandbox, 'node_modules/@linhey/rhine-design/skills/rhine-design');
19
+ cpSync(source, packaged, { recursive: true });
20
+ // Same source and destination layout as the README's cp -R command.
21
+ cpSync(packaged, target, { recursive: true });
22
+ } else {
23
+ // Only the skill directory is available: no adjacent repository docs.
24
+ cpSync(source, target, { recursive: true });
25
+ }
26
+ return target;
27
+ }
28
+
29
+ function checkLinks(root: string) {
30
+ let localLinks = 0;
31
+ for (const file of readdirSync(root, { recursive: true, encoding: 'utf8' }).filter(file => file.endsWith('.md'))) {
32
+ const document = join(root, file);
33
+ const markdown = readFileSync(document, 'utf8');
34
+ // The skill uses inline Markdown links; code-formatted package paths are not links.
35
+ for (const [, href] of markdown.matchAll(/\[[^\]]*\]\(([^\s)]+)\)/g)) {
36
+ if (/^https?:\/\//.test(href)) {
37
+ const url = new URL(href);
38
+ if (url.hostname === 'github.com' && /^\/linhay\/rhine-design\/(blob|tree)\//.test(url.pathname)) {
39
+ const revision = url.pathname.split('/')[4];
40
+ if (!/^(?:[a-f0-9]{40}|v?\d+\.\d+\.\d+)$/.test(revision)) throw new Error(`Floating source link: ${href}`);
41
+ }
42
+ continue;
43
+ }
44
+ const [path, fragment] = href.split('#');
45
+ const target = path ? resolve(dirname(document), decodeURIComponent(path)) : document;
46
+ const within = relative(root, target);
47
+ if (isAbsolute(within) || within === '..' || within.startsWith('../')) throw new Error(`Link escapes skill: ${href}`);
48
+ if (!existsSync(target)) throw new Error(`Missing link target: ${href}`);
49
+ const realWithin = relative(realpathSync(root), realpathSync(target));
50
+ if (isAbsolute(realWithin) || realWithin === '..' || realWithin.startsWith('../')) throw new Error(`Link escapes skill: ${href}`);
51
+ if (fragment) {
52
+ const headings = [...readFileSync(target, 'utf8').matchAll(/^#{1,6}\s+(.+)$/gm)]
53
+ .map(([, heading]) => heading.toLowerCase().replace(/[^\p{L}\p{N}_\-\s]/gu, '').replace(/\s/g, '-'));
54
+ if (!headings.includes(decodeURIComponent(fragment))) throw new Error(`Missing heading: ${href}`);
55
+ }
56
+ localLinks++;
57
+ }
58
+ }
59
+ return localLinks;
60
+ }
61
+
62
+ describe('portable skill installation', () => {
63
+ it.each([true, false])('keeps links reachable outside the repository (package copy: %s)', fromPackage => {
64
+ expect(checkLinks(install(fromPackage))).toBeGreaterThan(0);
65
+ });
66
+
67
+ it.each([
68
+ ['../../../docs/THEMING.md', 'Link escapes skill'],
69
+ ['missing.md', 'Missing link target'],
70
+ ['../SKILL.md#missing-heading', 'Missing heading'],
71
+ ['https://github.com/linhay/rhine-design/blob/main/docs/THEMING.md', 'Floating source link'],
72
+ ])('rejects an invalid installed reference: %s', (href, error) => {
73
+ const root = install(false);
74
+ writeFileSync(join(root, 'references/regression.md'), `[contract](${href})\n`);
75
+ expect(() => checkLinks(root)).toThrow(error);
76
+ });
77
+ });
@@ -0,0 +1,70 @@
1
+ import { act, fireEvent, render, screen, within } from '@testing-library/react';
2
+ import { afterEach, describe, expect, it, vi } from 'vitest';
3
+ import { Button, Popover } from '../components';
4
+ import { ThemeProvider } from '../components/theme';
5
+ import { TrayUsageGallery } from '../preview/TrayUsageGallery';
6
+
7
+ afterEach(() => vi.useRealTimers());
8
+ function demo() { return render(<ThemeProvider><TrayUsageGallery /></ThemeProvider>); }
9
+ const account = () => screen.getByRole('combobox', { name: 'Agent A 当前账号' });
10
+ const panel = () => screen.getByRole('dialog', { name: '账号与用量概览' });
11
+
12
+ describe('headerless Popover and synthetic usage host', () => {
13
+ it('preserves the accessible name and default header compatibility', () => {
14
+ const { rerender } = render(<ThemeProvider><Popover title="详情名称" trigger={<Button>详情</Button>} open>内容</Popover></ThemeProvider>);
15
+ expect(screen.getByRole('button', { name: '关闭详情' })).toBeVisible();
16
+ rerender(<ThemeProvider><Popover title="详情名称" showHeader={false} trigger={<Button>详情</Button>} open>内容</Popover></ThemeProvider>);
17
+ expect(screen.getByRole('dialog', { name: '详情名称' })).toBeVisible();
18
+ expect(screen.queryByRole('button', { name: '关闭详情' })).toBeNull();
19
+ expect(screen.queryByText('详情名称')).toBeNull();
20
+ });
21
+ it('keeps committed account and unrelated data while pending, locks duplicates, and retries a failure', () => {
22
+ demo(); vi.useFakeTimers();
23
+ fireEvent.click(screen.getByLabelText('首次切换失败'));
24
+ const other = screen.getByTestId('other-agent').textContent, summary = screen.getByTestId('usage-summary').textContent;
25
+ fireEvent.change(account(), { target: { value: 'b' } });
26
+ fireEvent.change(account(), { target: { value: 'b' } });
27
+ expect(account()).toBeDisabled(); expect(account()).toHaveValue('a');
28
+ expect(screen.getByTestId('current-account')).toHaveTextContent('研究账号 A');
29
+ expect(screen.getByTestId('request-count')).toHaveTextContent('1');
30
+ act(() => vi.advanceTimersByTime(1200));
31
+ expect(account()).toHaveValue('a'); expect(account()).not.toBeDisabled();
32
+ expect(within(panel()).getByRole('alert')).toHaveTextContent('切换失败');
33
+ fireEvent.click(screen.getByRole('button', { name: '重试切换' }));
34
+ act(() => vi.advanceTimersByTime(1200));
35
+ expect(account()).toHaveValue('b');
36
+ expect(screen.getByTestId('request-count')).toHaveTextContent('2');
37
+ expect(screen.getByTestId('other-agent').textContent).toBe(other);
38
+ expect(screen.getByTestId('usage-summary').textContent).toBe(summary);
39
+ });
40
+ it('blocks unavailable directories and cancels a late result on logout/reset', () => {
41
+ demo(); vi.useFakeTimers();
42
+ for (const state of ['loading', 'stale', 'connecting', 'unmatched']) {
43
+ fireEvent.change(screen.getByLabelText('用量场景'), { target: { value: state } });
44
+ expect(account()).toBeDisabled();
45
+ fireEvent.change(account(), { target: { value: 'b' } });
46
+ expect(screen.getByTestId('request-count')).toHaveTextContent('0');
47
+ }
48
+ fireEvent.change(screen.getByLabelText('用量场景'), { target: { value: 'normal' } });
49
+ fireEvent.change(account(), { target: { value: 'b' } });
50
+ fireEvent.change(screen.getByLabelText('用量场景'), { target: { value: 'signed-out' } });
51
+ act(() => vi.advanceTimersByTime(1200));
52
+ expect(within(panel()).queryByText(/研究账号|协作账号|1.28|128/)).toBeNull();
53
+ expect(screen.queryByTestId('usage-summary')).toBeNull();
54
+ fireEvent.click(screen.getByRole('button', { name: '重置托盘示例' }));
55
+ expect(account()).toHaveValue('a');
56
+ fireEvent.change(account(), { target: { value: 'b' } });
57
+ fireEvent.click(screen.getByRole('button', { name: '重置托盘示例' }));
58
+ act(() => vi.advanceTimersByTime(1200));
59
+ expect(account()).toHaveValue('a'); expect(screen.getByTestId('request-count')).toHaveTextContent('0');
60
+ });
61
+ it('keeps unknown quota distinct from zero and stale cache explicitly marked', () => {
62
+ demo();
63
+ fireEvent.change(screen.getByLabelText('用量场景'), { target: { value: 'unknown' } });
64
+ expect(within(panel()).getByText('未知')).toBeVisible();
65
+ expect(within(panel()).queryByRole('progressbar')).toBeNull();
66
+ fireEvent.change(screen.getByLabelText('用量场景'), { target: { value: 'stale' } });
67
+ expect(within(panel()).getByText('上次数据 · 目录已过期')).toBeVisible();
68
+ expect(screen.getByTestId('usage-summary')).toHaveTextContent('上次更新');
69
+ });
70
+ });