cursedbelt 3.0.2 → 4.1.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/dist/react/components/ListToolbar.d.ts +94 -0
- package/dist/react/components/ListToolbar.d.ts.map +1 -0
- package/dist/react/components/ListToolbar.js +66 -0
- package/dist/react/components/ListToolbar.js.map +1 -0
- package/dist/react/file-tree/FileTree.d.ts +282 -3
- package/dist/react/file-tree/FileTree.d.ts.map +1 -1
- package/dist/react/file-tree/FileTree.js +799 -131
- package/dist/react/file-tree/FileTree.js.map +1 -1
- package/dist/react/file-tree/fileTree.css +151 -0
- package/dist/react/file-tree/fileTreeSelection.d.ts +187 -0
- package/dist/react/file-tree/fileTreeSelection.d.ts.map +1 -0
- package/dist/react/file-tree/fileTreeSelection.js +264 -0
- package/dist/react/file-tree/fileTreeSelection.js.map +1 -0
- package/dist/react/file-tree/index.d.ts +1 -0
- package/dist/react/file-tree/index.d.ts.map +1 -1
- package/dist/react/file-tree/index.js +8 -0
- package/dist/react/file-tree/index.js.map +1 -1
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +1 -0
- package/dist/react/index.js.map +1 -1
- package/dist/react/lib/form.d.ts +3 -0
- package/dist/react/lib/form.d.ts.map +1 -1
- package/dist/react/lib/form.js +43 -5
- package/dist/react/lib/form.js.map +1 -1
- package/dist/react/media/DenseGalleryShell.d.ts +19 -3
- package/dist/react/media/DenseGalleryShell.d.ts.map +1 -1
- package/dist/react/media/DenseGalleryShell.js +55 -15
- package/dist/react/media/DenseGalleryShell.js.map +1 -1
- package/dist/react/media/FocusViewer.d.ts.map +1 -1
- package/dist/react/media/FocusViewer.js +14 -14
- package/dist/react/media/FocusViewer.js.map +1 -1
- package/dist/react/media/FrameGrabber.d.ts +23 -0
- package/dist/react/media/FrameGrabber.d.ts.map +1 -1
- package/dist/react/media/FrameGrabber.js +67 -1
- package/dist/react/media/FrameGrabber.js.map +1 -1
- package/dist/react/media/MediaResize.d.ts +51 -19
- package/dist/react/media/MediaResize.d.ts.map +1 -1
- package/dist/react/media/MediaResize.js +20 -13
- package/dist/react/media/MediaResize.js.map +1 -1
- package/dist/react/media/VideoChapterEditor.d.ts +67 -0
- package/dist/react/media/VideoChapterEditor.d.ts.map +1 -0
- package/dist/react/media/VideoChapterEditor.js +148 -0
- package/dist/react/media/VideoChapterEditor.js.map +1 -0
- package/dist/react/media/VideoPlayer.d.ts +46 -4
- package/dist/react/media/VideoPlayer.d.ts.map +1 -1
- package/dist/react/media/VideoPlayer.js +124 -11
- package/dist/react/media/VideoPlayer.js.map +1 -1
- package/dist/react/media/VideoTurner.d.ts +15 -2
- package/dist/react/media/VideoTurner.d.ts.map +1 -1
- package/dist/react/media/VideoTurner.js +29 -2
- package/dist/react/media/VideoTurner.js.map +1 -1
- package/dist/react/media/denseGallery.css +104 -46
- package/dist/react/media/hlsSource.d.ts +57 -0
- package/dist/react/media/hlsSource.d.ts.map +1 -1
- package/dist/react/media/hlsSource.js +177 -1
- package/dist/react/media/hlsSource.js.map +1 -1
- package/dist/react/media/index.d.ts +2 -0
- package/dist/react/media/index.d.ts.map +1 -1
- package/dist/react/media/index.js +6 -0
- package/dist/react/media/index.js.map +1 -1
- package/dist/react/media/mediaResize.css +127 -54
- package/dist/react/media/pictureEditor.css +52 -6
- package/dist/react/media/videoChapterEditor.css +203 -0
- package/dist/react/media/videoChapters.d.ts +117 -0
- package/dist/react/media/videoChapters.d.ts.map +1 -0
- package/dist/react/media/videoChapters.js +95 -0
- package/dist/react/media/videoChapters.js.map +1 -0
- package/dist/react/media-gallery/GalleryTable.d.ts +52 -1
- package/dist/react/media-gallery/GalleryTable.d.ts.map +1 -1
- package/dist/react/media-gallery/GalleryTable.js +40 -9
- package/dist/react/media-gallery/GalleryTable.js.map +1 -1
- package/dist/react/media-gallery/MediaGallery.d.ts +107 -4
- package/dist/react/media-gallery/MediaGallery.d.ts.map +1 -1
- package/dist/react/media-gallery/MediaGallery.js +484 -104
- package/dist/react/media-gallery/MediaGallery.js.map +1 -1
- package/dist/react/media-gallery/MediaMetaEditor.d.ts +48 -0
- package/dist/react/media-gallery/MediaMetaEditor.d.ts.map +1 -0
- package/dist/react/media-gallery/MediaMetaEditor.js +66 -0
- package/dist/react/media-gallery/MediaMetaEditor.js.map +1 -0
- package/dist/react/media-gallery/index.d.ts +2 -0
- package/dist/react/media-gallery/index.d.ts.map +1 -1
- package/dist/react/media-gallery/index.js +5 -0
- package/dist/react/media-gallery/index.js.map +1 -1
- package/dist/react/media-gallery/mediaGallery.css +180 -4
- package/dist/react/media-gallery/mediaMeta.d.ts +62 -0
- package/dist/react/media-gallery/mediaMeta.d.ts.map +1 -0
- package/dist/react/media-gallery/mediaMeta.js +56 -0
- package/dist/react/media-gallery/mediaMeta.js.map +1 -0
- package/dist/react/media-gallery/playbackPreferences.d.ts +77 -0
- package/dist/react/media-gallery/playbackPreferences.d.ts.map +1 -1
- package/dist/react/media-gallery/playbackPreferences.js +42 -0
- package/dist/react/media-gallery/playbackPreferences.js.map +1 -1
- package/dist/react/media-gallery/types.d.ts +34 -0
- package/dist/react/media-gallery/types.d.ts.map +1 -1
- package/dist/scripts/guardrailsEnforce.d.ts +29 -0
- package/dist/scripts/guardrailsEnforce.d.ts.map +1 -0
- package/dist/styles-areas/activity-bar.css +30 -0
- package/dist/styles-areas/analytics.css +672 -0
- package/dist/styles-areas/auth.css +154 -0
- package/dist/styles-areas/calendar.css +416 -0
- package/dist/styles-areas/chart-table.css +41 -0
- package/dist/styles-areas/charts.css +89 -0
- package/dist/styles-areas/chat.css +364 -0
- package/dist/styles-areas/clipboard.css +26 -0
- package/dist/styles-areas/code-editor.css +130 -0
- package/dist/styles-areas/companion-link.css +70 -0
- package/dist/styles-areas/core.css +2503 -0
- package/dist/styles-areas/dashboard-grid.css +84 -0
- package/dist/styles-areas/data-table.css +657 -0
- package/dist/styles-areas/deep-link.css +26 -0
- package/dist/styles-areas/diff-viewer.css +30 -0
- package/dist/styles-areas/disk-usage.css +262 -0
- package/dist/styles-areas/emoji.css +154 -0
- package/dist/styles-areas/fields.css +582 -0
- package/dist/styles-areas/file-tree.css +33 -0
- package/dist/styles-areas/filter-rail.css +235 -0
- package/dist/styles-areas/folder-tree.css +219 -0
- package/dist/styles-areas/keep-awake.css +26 -0
- package/dist/styles-areas/layout-engine.css +239 -0
- package/dist/styles-areas/markdown.css +87 -0
- package/dist/styles-areas/master-detail.css +239 -0
- package/dist/styles-areas/master-lock.css +26 -0
- package/dist/styles-areas/media-gallery.css +351 -0
- package/dist/styles-areas/media.css +658 -0
- package/dist/styles-areas/nav.css +488 -0
- package/dist/styles-areas/notifications.css +245 -0
- package/dist/styles-areas/overlays.css +133 -0
- package/dist/styles-areas/palette.css +140 -0
- package/dist/styles-areas/pdf-viewer.css +47 -0
- package/dist/styles-areas/problem-detail.css +97 -0
- package/dist/styles-areas/rich-text.css +167 -0
- package/dist/styles-areas/sharing.css +153 -0
- package/dist/styles-areas/spreadsheet.css +37 -0
- package/dist/styles-areas/stats.css +281 -0
- package/dist/styles-areas/test-report.css +133 -0
- package/dist/styles-areas/virtual.css +30 -0
- package/dist/styles-areas/wizard.css +381 -0
- package/dist/styles-areas/workbench.css +543 -0
- package/dist/styles-areas/workbook-viewer.css +44 -0
- package/dist/styles-static.css +34 -23
- package/dist/styles.css +34 -23
- package/package.json +130 -32
- package/scripts/checkDistExports.ts +67 -0
- package/scripts/cssRules.ts +125 -0
- package/scripts/fixtureAppCss.ts +344 -0
- package/scripts/generateAreaStyles.ts +253 -0
- package/scripts/guardrailsEnforce.spec.ts +62 -0
- package/scripts/guardrailsEnforce.ts +67 -3
- package/scripts/paths.ts +106 -0
- package/scripts/styleAreas.ts +585 -0
- package/scripts/verify.ts +5 -0
- package/src/barrelsReachNoOptionalPeer.spec.ts +117 -9
- package/src/docsMatchTheSplit.spec.ts +98 -0
- package/src/fixtureAppCss.spec.ts +177 -0
- package/src/namedSubpathsResolve.spec.ts +35 -0
- package/src/publishShape.spec.ts +67 -0
- package/src/react/components/ListToolbar.spec.tsx +172 -0
- package/src/react/components/ListToolbar.tsx +151 -0
- package/src/react/file-tree/FileTree.spec.tsx +1009 -0
- package/src/react/file-tree/FileTree.tsx +1459 -309
- package/src/react/file-tree/fileTree.css +151 -0
- package/src/react/file-tree/fileTreeSelection.spec.ts +327 -0
- package/src/react/file-tree/fileTreeSelection.ts +321 -0
- package/src/react/file-tree/index.ts +21 -0
- package/src/react/index.ts +1 -0
- package/src/react/lib/form.ts +51 -8
- package/src/react/markdown/MarkdownDoc.spec.tsx +1 -1
- package/src/react/media/DenseGalleryShell.tsx +108 -36
- package/src/react/media/FocusViewer.tsx +15 -8
- package/src/react/media/FrameGrabber.spec.tsx +175 -0
- package/src/react/media/FrameGrabber.tsx +67 -0
- package/src/react/media/MediaResize.spec.tsx +84 -14
- package/src/react/media/MediaResize.tsx +51 -19
- package/src/react/media/VideoChapterEditor.tsx +428 -0
- package/src/react/media/VideoPlayer.spec.tsx +111 -1
- package/src/react/media/VideoPlayer.tsx +158 -11
- package/src/react/media/VideoTurner.spec.tsx +67 -0
- package/src/react/media/VideoTurner.tsx +70 -15
- package/src/react/media/denseGallery.css +104 -46
- package/src/react/media/hlsSource.recovery.spec.ts +213 -0
- package/src/react/media/hlsSource.spec.ts +8 -0
- package/src/react/media/hlsSource.ts +189 -1
- package/src/react/media/index.ts +14 -0
- package/src/react/media/mediaResize.css +127 -54
- package/src/react/media/pictureEditor.css +52 -6
- package/src/react/media/videoChapterEditor.css +203 -0
- package/src/react/media/videoChapters.spec.ts +176 -0
- package/src/react/media/videoChapters.ts +181 -0
- package/src/react/media-gallery/GalleryTable.tsx +93 -3
- package/src/react/media-gallery/MediaGallery.spec.tsx +1156 -10
- package/src/react/media-gallery/MediaGallery.tsx +813 -95
- package/src/react/media-gallery/MediaMetaEditor.tsx +237 -0
- package/src/react/media-gallery/galleryTable.spec.ts +41 -1
- package/src/react/media-gallery/index.ts +7 -0
- package/src/react/media-gallery/mediaGallery.css +180 -4
- package/src/react/media-gallery/mediaMeta.spec.ts +95 -0
- package/src/react/media-gallery/mediaMeta.ts +101 -0
- package/src/react/media-gallery/playbackPreferences.spec.ts +172 -0
- package/src/react/media-gallery/playbackPreferences.ts +100 -0
- package/src/react/media-gallery/types.ts +34 -0
- package/src/shippedFilesAreTracked.spec.ts +69 -0
- package/src/styles-areas/activity-bar.css +30 -0
- package/src/styles-areas/analytics.css +672 -0
- package/src/styles-areas/auth.css +154 -0
- package/src/styles-areas/calendar.css +416 -0
- package/src/styles-areas/chart-table.css +41 -0
- package/src/styles-areas/charts.css +89 -0
- package/src/styles-areas/chat.css +364 -0
- package/src/styles-areas/clipboard.css +26 -0
- package/src/styles-areas/code-editor.css +130 -0
- package/src/styles-areas/companion-link.css +70 -0
- package/src/styles-areas/core.css +2503 -0
- package/src/styles-areas/dashboard-grid.css +84 -0
- package/src/styles-areas/data-table.css +657 -0
- package/src/styles-areas/deep-link.css +26 -0
- package/src/styles-areas/diff-viewer.css +30 -0
- package/src/styles-areas/disk-usage.css +262 -0
- package/src/styles-areas/emoji.css +154 -0
- package/src/styles-areas/fields.css +582 -0
- package/src/styles-areas/file-tree.css +33 -0
- package/src/styles-areas/filter-rail.css +235 -0
- package/src/styles-areas/folder-tree.css +219 -0
- package/src/styles-areas/keep-awake.css +26 -0
- package/src/styles-areas/layout-engine.css +239 -0
- package/src/styles-areas/markdown.css +87 -0
- package/src/styles-areas/master-detail.css +239 -0
- package/src/styles-areas/master-lock.css +26 -0
- package/src/styles-areas/media-gallery.css +351 -0
- package/src/styles-areas/media.css +658 -0
- package/src/styles-areas/nav.css +488 -0
- package/src/styles-areas/notifications.css +245 -0
- package/src/styles-areas/overlays.css +133 -0
- package/src/styles-areas/palette.css +140 -0
- package/src/styles-areas/pdf-viewer.css +47 -0
- package/src/styles-areas/problem-detail.css +97 -0
- package/src/styles-areas/rich-text.css +167 -0
- package/src/styles-areas/sharing.css +153 -0
- package/src/styles-areas/spreadsheet.css +37 -0
- package/src/styles-areas/stats.css +281 -0
- package/src/styles-areas/test-report.css +133 -0
- package/src/styles-areas/virtual.css +30 -0
- package/src/styles-areas/wizard.css +381 -0
- package/src/styles-areas/workbench.css +543 -0
- package/src/styles-areas/workbook-viewer.css +44 -0
- package/src/styles-static.css +34 -23
- package/src/styles.css +34 -23
- package/src/stylesAreas.spec.ts +247 -0
- package/src/stylesUtilitiesMatches.spec.ts +6 -77
- package/src/testFilesRunInParallel.spec.ts +79 -0
- package/src/typecheckCachesAreSeparate.spec.ts +128 -0
- package/src/verifyGraph.spec.ts +6 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from 'bun:test';
|
|
2
|
+
import { cleanup, fireEvent, render } from '@testing-library/react';
|
|
3
|
+
import { ListToolbar } from './ListToolbar';
|
|
4
|
+
|
|
5
|
+
afterEach(cleanup);
|
|
6
|
+
|
|
7
|
+
const CHIPS = [
|
|
8
|
+
{ value: 'waiting', label: 'Waiting', count: 2 },
|
|
9
|
+
{ value: 'taken', label: 'Taken' },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
describe('ListToolbar', () => {
|
|
13
|
+
test('renders the search box, named by searchLabel', () => {
|
|
14
|
+
const { getByRole } = render(
|
|
15
|
+
<ListToolbar
|
|
16
|
+
search='draft'
|
|
17
|
+
onSearchChange={() => {}}
|
|
18
|
+
searchPlaceholder='anything in a draft'
|
|
19
|
+
searchLabel='Search the drafts'
|
|
20
|
+
/>,
|
|
21
|
+
);
|
|
22
|
+
const box = getByRole('searchbox', { name: 'Search the drafts' });
|
|
23
|
+
expect((box as HTMLInputElement).value).toBe('draft');
|
|
24
|
+
expect(box.getAttribute('placeholder')).toBe('anything in a draft');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('the accessible name falls back to the placeholder, then to "Search"', () => {
|
|
28
|
+
const { getByRole } = render(
|
|
29
|
+
<ListToolbar search='' onSearchChange={() => {}} searchPlaceholder='file name or repo' />,
|
|
30
|
+
);
|
|
31
|
+
expect(getByRole('searchbox', { name: 'file name or repo' })).toBeDefined();
|
|
32
|
+
cleanup();
|
|
33
|
+
|
|
34
|
+
const bare = render(<ListToolbar search='' onSearchChange={() => {}} />);
|
|
35
|
+
expect(bare.getByRole('searchbox', { name: 'Search' })).toBeDefined();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('typing fires onSearchChange with the new text', () => {
|
|
39
|
+
let seen = '';
|
|
40
|
+
const { getByRole } = render(
|
|
41
|
+
<ListToolbar
|
|
42
|
+
search=''
|
|
43
|
+
onSearchChange={(v) => {
|
|
44
|
+
seen = v;
|
|
45
|
+
}}
|
|
46
|
+
/>,
|
|
47
|
+
);
|
|
48
|
+
fireEvent.change(getByRole('searchbox'), { target: { value: 'queue' } });
|
|
49
|
+
expect(seen).toBe('queue');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('no search box without BOTH value and handler — a box nobody listens to is worse than none', () => {
|
|
53
|
+
const { queryByRole } = render(<ListToolbar search='orphan' />);
|
|
54
|
+
expect(queryByRole('searchbox')).toBeNull();
|
|
55
|
+
cleanup();
|
|
56
|
+
|
|
57
|
+
const noValue = render(<ListToolbar onSearchChange={() => {}} />);
|
|
58
|
+
expect(noValue.queryByRole('searchbox')).toBeNull();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('renders one chip per option plus the "All" pill, and reports a click', () => {
|
|
62
|
+
let picked: string | string[] | null = null;
|
|
63
|
+
const read = (): string | string[] | null => picked;
|
|
64
|
+
const { getByRole, getAllByRole } = render(
|
|
65
|
+
<ListToolbar
|
|
66
|
+
chips={CHIPS}
|
|
67
|
+
chipValue={null}
|
|
68
|
+
onChipChange={(v) => {
|
|
69
|
+
picked = v;
|
|
70
|
+
}}
|
|
71
|
+
/>,
|
|
72
|
+
);
|
|
73
|
+
expect(getAllByRole('button')).toHaveLength(3); // All + 2
|
|
74
|
+
expect(getByRole('button', { name: 'All' }).getAttribute('aria-pressed')).toBe('true');
|
|
75
|
+
fireEvent.click(getByRole('button', { name: /Waiting/ }));
|
|
76
|
+
expect(read()).toBe('waiting');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('chipValue defaults to the "All" state', () => {
|
|
80
|
+
const { getByRole } = render(<ListToolbar chips={CHIPS} onChipChange={() => {}} />);
|
|
81
|
+
expect(getByRole('button', { name: 'All' }).getAttribute('aria-pressed')).toBe('true');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('multiChips accumulates instead of replacing', () => {
|
|
85
|
+
let picked: string | string[] | null = null;
|
|
86
|
+
const read = (): string | string[] | null => picked;
|
|
87
|
+
const { getByRole } = render(
|
|
88
|
+
<ListToolbar
|
|
89
|
+
chips={CHIPS}
|
|
90
|
+
chipValue={['waiting']}
|
|
91
|
+
multiChips
|
|
92
|
+
onChipChange={(v) => {
|
|
93
|
+
picked = v;
|
|
94
|
+
}}
|
|
95
|
+
/>,
|
|
96
|
+
);
|
|
97
|
+
fireEvent.click(getByRole('button', { name: /Taken/ }));
|
|
98
|
+
expect(read()).toEqual(['waiting', 'taken']);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('chipsAllLabel renames the clear-all pill', () => {
|
|
102
|
+
const { getByRole } = render(
|
|
103
|
+
<ListToolbar chips={CHIPS} onChipChange={() => {}} chipsAllLabel='Everything' />,
|
|
104
|
+
);
|
|
105
|
+
expect(getByRole('button', { name: 'Everything' })).toBeDefined();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('an EMPTY chip list renders nothing — not a lone "All" pill filtering nothing', () => {
|
|
109
|
+
const { queryByRole } = render(<ListToolbar chips={[]} onChipChange={() => {}} />);
|
|
110
|
+
expect(queryByRole('button', { name: 'All' })).toBeNull();
|
|
111
|
+
expect(queryByRole('group', { name: 'Filter options' })).toBeNull();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('sort, view and actions land in one right-hand cluster, in that order', () => {
|
|
115
|
+
const { container, getByTestId } = render(
|
|
116
|
+
<ListToolbar
|
|
117
|
+
search=''
|
|
118
|
+
onSearchChange={() => {}}
|
|
119
|
+
sort={<span data-testid='sort'>sort</span>}
|
|
120
|
+
view={<span data-testid='view'>view</span>}
|
|
121
|
+
actions={<span data-testid='actions'>actions</span>}
|
|
122
|
+
/>,
|
|
123
|
+
);
|
|
124
|
+
const cluster = getByTestId('sort').parentElement;
|
|
125
|
+
expect(cluster).not.toBeNull();
|
|
126
|
+
// One cluster, not three loose children: all three share the same parent…
|
|
127
|
+
expect(getByTestId('view').parentElement).toBe(cluster as HTMLElement);
|
|
128
|
+
expect(getByTestId('actions').parentElement).toBe(cluster as HTMLElement);
|
|
129
|
+
expect([...(cluster as HTMLElement).children].map((c) => c.textContent)).toEqual([
|
|
130
|
+
'sort',
|
|
131
|
+
'view',
|
|
132
|
+
'actions',
|
|
133
|
+
]);
|
|
134
|
+
// …and that cluster is pushed right, per DESIGN.md §2.
|
|
135
|
+
expect((cluster as HTMLElement).className).toContain('ml-auto');
|
|
136
|
+
expect((cluster as HTMLElement).parentElement).toBe(container.firstChild as HTMLElement);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('no right-hand cluster when there is nothing to put in it', () => {
|
|
140
|
+
const { container } = render(<ListToolbar search='' onSearchChange={() => {}} />);
|
|
141
|
+
expect((container.firstChild as HTMLElement).querySelector('.ml-auto')).toBeNull();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('extra filters sit on the left, outside the right-hand cluster', () => {
|
|
145
|
+
const { getByTestId, container } = render(
|
|
146
|
+
<ListToolbar
|
|
147
|
+
filters={<span data-testid='extra'>extra</span>}
|
|
148
|
+
view={<span data-testid='view'>view</span>}
|
|
149
|
+
/>,
|
|
150
|
+
);
|
|
151
|
+
expect(getByTestId('extra').parentElement).toBe(container.firstChild as HTMLElement);
|
|
152
|
+
expect(getByTestId('view').parentElement).not.toBe(container.firstChild as HTMLElement);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('ONE wrapping line (DESIGN.md §2 + §5), never a second row', () => {
|
|
156
|
+
const { container } = render(
|
|
157
|
+
<ListToolbar search='' onSearchChange={() => {}} chips={CHIPS} onChipChange={() => {}} />,
|
|
158
|
+
);
|
|
159
|
+
const row = container.firstChild as HTMLElement;
|
|
160
|
+
// A single flex container: `flex-wrap` is what keeps 44px targets at 390px without
|
|
161
|
+
// a second layout, so it is asserted rather than left to a reviewer's eye.
|
|
162
|
+
expect(row.className).toContain('flex');
|
|
163
|
+
expect(row.className).toContain('flex-wrap');
|
|
164
|
+
expect(container.childElementCount).toBe(1);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('className is merged onto the row, not dropped', () => {
|
|
168
|
+
const { container } = render(<ListToolbar className='mb-4' view={<span>v</span>} />);
|
|
169
|
+
expect((container.firstChild as HTMLElement).className).toContain('mb-4');
|
|
170
|
+
expect((container.firstChild as HTMLElement).className).toContain('flex-wrap');
|
|
171
|
+
});
|
|
172
|
+
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { cn } from '../lib/cn';
|
|
3
|
+
import { ChipFilterStrip, type ChipOption } from './ChipFilterStrip';
|
|
4
|
+
import { Input } from './Input';
|
|
5
|
+
|
|
6
|
+
export interface ListToolbarProps {
|
|
7
|
+
/**
|
|
8
|
+
* The search text. The box renders only when BOTH this and `onSearchChange`
|
|
9
|
+
* are given — a search input nobody is listening to is worse than none.
|
|
10
|
+
*/
|
|
11
|
+
search?: string;
|
|
12
|
+
onSearchChange?: (value: string) => void;
|
|
13
|
+
/** What the reader can type, in the reader's terms — `'file name, repo or title'`. */
|
|
14
|
+
searchPlaceholder?: string;
|
|
15
|
+
/**
|
|
16
|
+
* The box's accessible name. Say WHAT is being searched (*"Search the queue"*),
|
|
17
|
+
* because a page with two lists gives a screen reader two boxes called "Search".
|
|
18
|
+
* Falls back to the placeholder, then to `'Search'`.
|
|
19
|
+
*/
|
|
20
|
+
searchLabel?: string;
|
|
21
|
+
/**
|
|
22
|
+
* The filter chips — one dimension of the list, as data. An EMPTY array renders
|
|
23
|
+
* nothing at all: {@link ChipFilterStrip} always draws its "All" pill, and a lone
|
|
24
|
+
* "All" chip filtering nothing is noise on the line (`ConsoleView` filters its
|
|
25
|
+
* bucket chips down to the buckets that have rows, and can reach zero).
|
|
26
|
+
*/
|
|
27
|
+
chips?: readonly ChipOption[];
|
|
28
|
+
/** Selected chip(s). `null` / `[]` is the "All" state. Default `null`. */
|
|
29
|
+
chipValue?: string | string[] | null;
|
|
30
|
+
onChipChange?: (value: string | string[] | null) => void;
|
|
31
|
+
/** Let more than one chip be active at once. Default `false`. */
|
|
32
|
+
multiChips?: boolean;
|
|
33
|
+
/** Label for the chips' clear-all pill. Default `'All'`. */
|
|
34
|
+
chipsAllLabel?: string;
|
|
35
|
+
/** Any further FILTER controls — they sit left, after the chips. */
|
|
36
|
+
filters?: ReactNode;
|
|
37
|
+
/** The sort control (a `NativeSelect`, a `ChoiceMenu`, …). Right-hand cluster. */
|
|
38
|
+
sort?: ReactNode;
|
|
39
|
+
/** The density/view-mode control — normally a `TableViewToggle`. Right-hand cluster. */
|
|
40
|
+
view?: ReactNode;
|
|
41
|
+
/** The far right: the one thing this page DOES. */
|
|
42
|
+
actions?: ReactNode;
|
|
43
|
+
className?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The toolbar above a list: ONE line, filters left, view controls and actions right.
|
|
48
|
+
*
|
|
49
|
+
* DESIGN.md §2, quoted because this component is nothing but that paragraph:
|
|
50
|
+
* *"Never scatter operational controls or filter tabs across multi-row lines. Merge
|
|
51
|
+
* navigation filters, search boxes, query parameters, execution buttons, and
|
|
52
|
+
* destructive triggers into a single cohesive layout line. Position filters to the
|
|
53
|
+
* left/center and main execution actions to the far right."*
|
|
54
|
+
*
|
|
55
|
+
* ── Why this is in the design system, not in the app ───────────────────────
|
|
56
|
+
* `apps/station` composed exactly this shape inline on BOTH of its lists — the queue
|
|
57
|
+
* (`ConsoleView`) and the intake (`IdeasView`) — about thirty identical lines each,
|
|
58
|
+
* differing only in their options. The retired `cursed-satellites` station had already
|
|
59
|
+
* met that fork and said so in `BoardChrome.tsx`'s header: two boards with the same
|
|
60
|
+
* INTERACTION built twice give you *"two toolbars that drift apart on the first change
|
|
61
|
+
* to either"*. Task `183` was right to forbid a station-local component and `184` is
|
|
62
|
+
* the consequence: the shape belongs to every app's list, so it lives here.
|
|
63
|
+
*
|
|
64
|
+
* ── Layout only. It has no memory ─────────────────────────────────────────
|
|
65
|
+
* 🔴 Search text, the chosen chips, the sort key and the view mode are all the
|
|
66
|
+
* caller's state, and one of them is already solved elsewhere: `useTableViewMode`
|
|
67
|
+
* persists rows ⇄ cards per account, server-side. A toolbar that grew its own
|
|
68
|
+
* storage would either fight that hook or quietly become a second, local, worse copy
|
|
69
|
+
* of it. `sort`/`view`/`actions` are SLOTS for the same reason — this one must work
|
|
70
|
+
* for `MasterDetail` pages and card grids, which is why it is not {@link TableToolbar}
|
|
71
|
+
* (that one reads everything off a TanStack `table` and cannot exist without one).
|
|
72
|
+
*
|
|
73
|
+
* ── `flex-wrap`, and no `role='toolbar'` ──────────────────────────────────
|
|
74
|
+
* Wrapping, not a grid and not a breakpoint swap: the console runs at 1440 on a desk
|
|
75
|
+
* and 390 on a phone, and wrapping is the one behaviour that keeps DESIGN.md §5's 44px
|
|
76
|
+
* targets at both widths without a second layout. `ml-auto` on the right-hand cluster
|
|
77
|
+
* keeps the controls right when everything fits on one line and lets them ride down as
|
|
78
|
+
* a group when it does not.
|
|
79
|
+
*
|
|
80
|
+
* No `role='toolbar'`: that role promises arrow-key roving focus between its controls,
|
|
81
|
+
* which would take the arrow keys away from the text box and the native `<select>`
|
|
82
|
+
* that live on this line. Tab order is the correct affordance here.
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* <ListToolbar
|
|
86
|
+
* search={search}
|
|
87
|
+
* onSearchChange={setSearch}
|
|
88
|
+
* searchPlaceholder='file name, repo or title'
|
|
89
|
+
* searchLabel='Search the queue'
|
|
90
|
+
* chips={buckets}
|
|
91
|
+
* chipValue={filter}
|
|
92
|
+
* onChipChange={(v) => setFilter(typeof v === 'string' ? v : null)}
|
|
93
|
+
* sort={<NativeSelect size='sm' aria-label='Sort the queue' … />}
|
|
94
|
+
* view={<TableViewToggle value={view.mode} onChange={view.setMode} />}
|
|
95
|
+
* />
|
|
96
|
+
*/
|
|
97
|
+
export function ListToolbar({
|
|
98
|
+
search,
|
|
99
|
+
onSearchChange,
|
|
100
|
+
searchPlaceholder,
|
|
101
|
+
searchLabel,
|
|
102
|
+
chips,
|
|
103
|
+
chipValue = null,
|
|
104
|
+
onChipChange,
|
|
105
|
+
multiChips = false,
|
|
106
|
+
chipsAllLabel,
|
|
107
|
+
filters,
|
|
108
|
+
sort,
|
|
109
|
+
view,
|
|
110
|
+
actions,
|
|
111
|
+
className,
|
|
112
|
+
}: ListToolbarProps) {
|
|
113
|
+
const showSearch = search !== undefined && onSearchChange !== undefined;
|
|
114
|
+
const showChips = chips !== undefined && chips.length > 0 && onChipChange !== undefined;
|
|
115
|
+
const showRight = Boolean(sort) || Boolean(view) || Boolean(actions);
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<div className={cn('flex flex-wrap items-center gap-x-3 gap-y-2', className)}>
|
|
119
|
+
{showSearch ? (
|
|
120
|
+
<Input
|
|
121
|
+
type='search'
|
|
122
|
+
value={search}
|
|
123
|
+
onChange={(e) => onSearchChange(e.currentTarget.value)}
|
|
124
|
+
placeholder={searchPlaceholder}
|
|
125
|
+
aria-label={searchLabel ?? searchPlaceholder ?? 'Search'}
|
|
126
|
+
// Full width on a phone (the chips then wrap beneath it, still tappable);
|
|
127
|
+
// a fixed, modest column from `sm:` so the chips share the line rather than
|
|
128
|
+
// being pushed off it by a box that grows with the container.
|
|
129
|
+
className='w-full min-w-40 sm:w-56'
|
|
130
|
+
/>
|
|
131
|
+
) : null}
|
|
132
|
+
{showChips ? (
|
|
133
|
+
<ChipFilterStrip
|
|
134
|
+
options={[...chips]}
|
|
135
|
+
value={chipValue}
|
|
136
|
+
onChange={onChipChange}
|
|
137
|
+
multi={multiChips}
|
|
138
|
+
{...(chipsAllLabel === undefined ? {} : { allLabel: chipsAllLabel })}
|
|
139
|
+
/>
|
|
140
|
+
) : null}
|
|
141
|
+
{filters}
|
|
142
|
+
{showRight ? (
|
|
143
|
+
<span className='ml-auto flex flex-wrap items-center gap-2'>
|
|
144
|
+
{sort}
|
|
145
|
+
{view}
|
|
146
|
+
{actions}
|
|
147
|
+
</span>
|
|
148
|
+
) : null}
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
}
|