@keboola/design 1.3.1 → 2.0.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/icons.cjs +24 -0
- package/dist/icons.js +1 -1
- package/dist/index.cjs +274 -72
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +274 -74
- package/dist/index.js.map +1 -1
- package/dist/markdown-editor.cjs +39 -26
- package/dist/markdown-editor.cjs.map +1 -1
- package/dist/markdown-editor.js +39 -26
- package/dist/markdown-editor.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +4 -3
- package/src/blocks/AIKit/PlanApproval.tsx +29 -12
- package/src/blocks/AIKit/PromptInput.test.tsx +53 -0
- package/src/blocks/AIKit/PromptInput.tsx +1 -1
- package/src/blocks/AIKit/Question.tsx +39 -11
- package/src/blocks/AIKit/SuggestedPrompt.stories.tsx +2 -8
- package/src/blocks/EmptyState/EmptyState.stories.tsx +5 -5
- package/src/blocks/EmptyState/EmptyState.tsx +2 -2
- package/src/blocks/ErrorState/ErrorState.stories.tsx +6 -6
- package/src/blocks/ErrorState/ErrorState.tsx +3 -3
- package/src/blocks/PageHeader/AGENTS.md +1 -1
- package/src/blocks/PageHeader/PageHeader.stories.tsx +6 -3
- package/src/blocks/PageHeader/PageHeaderIndex.tsx +2 -2
- package/src/blocks/PageLayout/AGENTS.md +3 -3
- package/src/blocks/PageLayout/PageLayout.stories.tsx +7 -4
- package/src/blocks/PageLayout/PageLayout.tsx +1 -18
- package/src/blocks/PageToolbar/PageToolbar.stories.tsx +79 -74
- package/src/blocks/PageToolbar/PageToolbar.tsx +45 -4
- package/src/blocks/PageToolbar/PageToolbarToken.tsx +61 -0
- package/src/blocks/SearchInput/SearchInput.tsx +2 -2
- package/src/components/Alert/AlertDescription.tsx +5 -0
- package/src/components/Badge/Badge.stories.tsx +3 -5
- package/src/components/Breadcrumb/Breadcrumb.stories.tsx +4 -4
- package/src/components/Breadcrumb/BreadcrumbLink.tsx +1 -1
- package/src/components/Button/ButtonInline.stories.tsx +3 -3
- package/src/components/Button/ButtonInline.tsx +4 -0
- package/src/components/Card/Card.stories.tsx +2 -2
- package/src/components/FormGroup/FormGroup.stories.tsx +2 -2
- package/src/components/HoverCard/HoverCardContent.tsx +5 -0
- package/src/components/Input/Input.stories.tsx +2 -2
- package/src/components/InputGroup/InputGroup.stories.tsx +4 -5
- package/src/components/InputGroup/InputGroup.tsx +1 -1
- package/src/components/InputGroup/InputGroupAddon.tsx +35 -24
- package/src/components/Kbd/Kbd.stories.tsx +2 -2
- package/src/components/Label/Label.stories.tsx +2 -2
- package/src/components/Modal/ModalBody.tsx +5 -0
- package/src/components/Navbar/Navbar.stories.tsx +4 -5
- package/src/components/Pagination/Pagination.stories.tsx +0 -1
- package/src/components/Popover/PopoverContent.tsx +4 -0
- package/src/components/Select/SelectTrigger.tsx +1 -1
- package/src/components/Separator/Separator.stories.tsx +19 -0
- package/src/components/Separator/Separator.test.tsx +39 -0
- package/src/components/Separator/Separator.tsx +42 -11
- package/src/components/Sidebar/Sidebar.stories.tsx +6 -5
- package/src/components/Table/Table.stories.tsx +16 -16
- package/src/components/Timeline/Timeline.stories.tsx +8 -8
- package/src/components/Timeline/TimelineConnector.tsx +1 -1
- package/src/components/ToggleGroup/ToggleGroup.stories.tsx +60 -0
- package/src/components/ToggleGroup/ToggleGroup.tsx +99 -0
- package/src/components/ToggleGroup/index.ts +1 -0
- package/src/components/Tooltip/Tooltip.stories.tsx +2 -2
- package/src/hooks/useAutosizeInput.stories.tsx +2 -2
- package/src/icons/index.ts +6 -0
- package/src/index.ts +1 -0
- package/src/patterns/FilterToolbar.stories.tsx +248 -44
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@keboola/design",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Keboola UI platform design system — shadcn-aligned React primitives, Keboola-branded blocks, and the form bindings built on TanStack Form.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"@testing-library/user-event": "^14.6.1",
|
|
75
75
|
"@types/react": "^19.2.15",
|
|
76
76
|
"@vitejs/plugin-react": "^6.0.0",
|
|
77
|
-
"@vitest/browser": "4.1.
|
|
77
|
+
"@vitest/browser": "4.1.8",
|
|
78
78
|
"@vitest/browser-playwright": "4.1.2",
|
|
79
79
|
"eslint-plugin-storybook": "^10.3.5",
|
|
80
80
|
"oxfmt": "^0.44.0",
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
"vitest": "^4.1.2"
|
|
89
89
|
},
|
|
90
90
|
"dependencies": {
|
|
91
|
-
"@awesome.me/kit-be8bcf7e5e": "1.0.
|
|
91
|
+
"@awesome.me/kit-be8bcf7e5e": "1.0.223",
|
|
92
92
|
"@base-ui/react": "1.5.0",
|
|
93
93
|
"@codemirror/autocomplete": "^6.20.1",
|
|
94
94
|
"@codemirror/commands": "^6.10.3",
|
|
@@ -132,6 +132,7 @@
|
|
|
132
132
|
"@radix-ui/react-switch": "^1.2.6",
|
|
133
133
|
"@radix-ui/react-tabs": "^1.1.13",
|
|
134
134
|
"@radix-ui/react-toggle": "^1.1.10",
|
|
135
|
+
"@radix-ui/react-toggle-group": "^1.1.11",
|
|
135
136
|
"@radix-ui/react-tooltip": "^1.2.8",
|
|
136
137
|
"@tanstack/react-form": "^1.29.1",
|
|
137
138
|
"@testing-library/jest-dom": "^6.8.0",
|
|
@@ -2,6 +2,7 @@ import { type FormEvent, type KeyboardEvent, type ReactNode, useState } from 're
|
|
|
2
2
|
|
|
3
3
|
import { Badge } from '../../components/Badge';
|
|
4
4
|
import { Button } from '../../components/Button';
|
|
5
|
+
import { Tooltip } from '../../components/Tooltip';
|
|
5
6
|
import { ChevronDown, Send, X } from '../../icons';
|
|
6
7
|
import { cn } from '../../utils';
|
|
7
8
|
import { Textarea } from '../Textarea';
|
|
@@ -23,6 +24,7 @@ type PlanApprovalProps = {
|
|
|
23
24
|
onRequestChanges?: (reason: string) => void;
|
|
24
25
|
onClose?: () => void;
|
|
25
26
|
isSubmitting?: boolean;
|
|
27
|
+
approveTooltip?: ReactNode;
|
|
26
28
|
|
|
27
29
|
pendingPillLabel?: string;
|
|
28
30
|
approvedPillLabel?: string;
|
|
@@ -123,6 +125,7 @@ export const PlanApproval = ({
|
|
|
123
125
|
onRequestChanges,
|
|
124
126
|
onClose,
|
|
125
127
|
isSubmitting = false,
|
|
128
|
+
approveTooltip,
|
|
126
129
|
|
|
127
130
|
pendingPillLabel = 'Ready for approval',
|
|
128
131
|
approvedPillLabel = 'Approved',
|
|
@@ -324,18 +327,32 @@ export const PlanApproval = ({
|
|
|
324
327
|
</Button>
|
|
325
328
|
)
|
|
326
329
|
)}
|
|
327
|
-
{onApprove &&
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
330
|
+
{onApprove &&
|
|
331
|
+
(approveTooltip ? (
|
|
332
|
+
<Tooltip tooltip={approveTooltip} placement="top" triggerAsChild>
|
|
333
|
+
<Button
|
|
334
|
+
argo
|
|
335
|
+
variant="primary"
|
|
336
|
+
size="small"
|
|
337
|
+
onClick={onApprove}
|
|
338
|
+
disabled={isSubmitting || isRequestingChanges}
|
|
339
|
+
data-testid="plan-approve-button"
|
|
340
|
+
>
|
|
341
|
+
{approveLabel}
|
|
342
|
+
</Button>
|
|
343
|
+
</Tooltip>
|
|
344
|
+
) : (
|
|
345
|
+
<Button
|
|
346
|
+
argo
|
|
347
|
+
variant="primary"
|
|
348
|
+
size="small"
|
|
349
|
+
onClick={onApprove}
|
|
350
|
+
disabled={isSubmitting || isRequestingChanges}
|
|
351
|
+
data-testid="plan-approve-button"
|
|
352
|
+
>
|
|
353
|
+
{approveLabel}
|
|
354
|
+
</Button>
|
|
355
|
+
))}
|
|
339
356
|
</div>
|
|
340
357
|
)}
|
|
341
358
|
</div>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { PromptInput } from './PromptInput';
|
|
5
|
+
|
|
6
|
+
describe('<PromptInput />', () => {
|
|
7
|
+
it('does not call onAbort when Enter is pressed during streaming', () => {
|
|
8
|
+
const onAbort = vi.fn();
|
|
9
|
+
const onSend = vi.fn();
|
|
10
|
+
|
|
11
|
+
render(<PromptInput status="streaming" value="hello" onSend={onSend} onAbort={onAbort} />);
|
|
12
|
+
|
|
13
|
+
const textarea = screen.getByRole('textbox');
|
|
14
|
+
fireEvent.keyDown(textarea, { key: 'Enter' });
|
|
15
|
+
|
|
16
|
+
expect(onAbort).not.toHaveBeenCalled();
|
|
17
|
+
expect(onSend).not.toHaveBeenCalled();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('calls onAbort when the stop button is clicked during streaming', () => {
|
|
21
|
+
const onAbort = vi.fn();
|
|
22
|
+
|
|
23
|
+
render(<PromptInput status="streaming" value="" onAbort={onAbort} />);
|
|
24
|
+
|
|
25
|
+
const form = screen.getByTestId('prompt-input');
|
|
26
|
+
const button = form.querySelector('button[type="submit"]')!;
|
|
27
|
+
fireEvent.click(button);
|
|
28
|
+
|
|
29
|
+
expect(onAbort).toHaveBeenCalledOnce();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('calls onSend when Enter is pressed in ready state', () => {
|
|
33
|
+
const onSend = vi.fn();
|
|
34
|
+
|
|
35
|
+
render(<PromptInput status="ready" value="hello" onSend={onSend} />);
|
|
36
|
+
|
|
37
|
+
const textarea = screen.getByRole('textbox');
|
|
38
|
+
fireEvent.keyDown(textarea, { key: 'Enter' });
|
|
39
|
+
|
|
40
|
+
expect(onSend).toHaveBeenCalledWith('hello');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('calls onContinue when Enter is pressed in interrupted state', () => {
|
|
44
|
+
const onContinue = vi.fn();
|
|
45
|
+
|
|
46
|
+
render(<PromptInput status="interrupted" value="" onContinue={onContinue} />);
|
|
47
|
+
|
|
48
|
+
const textarea = screen.getByRole('textbox');
|
|
49
|
+
fireEvent.keyDown(textarea, { key: 'Enter' });
|
|
50
|
+
|
|
51
|
+
expect(onContinue).toHaveBeenCalledOnce();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -138,7 +138,7 @@ export const PromptInput = ({
|
|
|
138
138
|
const handleOnKeyDown: KeyboardEventHandler<HTMLTextAreaElement> = (event) => {
|
|
139
139
|
if (event.key === 'Enter' && !event.shiftKey && !event.ctrlKey && !event.metaKey) {
|
|
140
140
|
event.preventDefault();
|
|
141
|
-
handleSubmit();
|
|
141
|
+
if (status !== 'streaming') handleSubmit();
|
|
142
142
|
}
|
|
143
143
|
};
|
|
144
144
|
|
|
@@ -4,6 +4,7 @@ import { Button } from '../../components/Button';
|
|
|
4
4
|
import { Checkbox } from '../../components/Checkbox';
|
|
5
5
|
import { TextInput } from '../../components/Input';
|
|
6
6
|
import { Progress } from '../../components/Progress';
|
|
7
|
+
import { Tooltip } from '../../components/Tooltip';
|
|
7
8
|
import { ChevronLeft, ChevronRight, X } from '../../icons';
|
|
8
9
|
import { cn } from '../../utils';
|
|
9
10
|
|
|
@@ -23,12 +24,14 @@ type Props = {
|
|
|
23
24
|
onSelectionChange?: (selectedIds: readonly string[]) => void;
|
|
24
25
|
customAnswer?: string;
|
|
25
26
|
onCustomAnswerChange?: (value: string) => void;
|
|
27
|
+
onCustomAnswerConfirm?: () => void;
|
|
26
28
|
showCustomAnswer?: boolean;
|
|
27
29
|
customAnswerPlaceholder?: string;
|
|
28
30
|
showSkip?: boolean;
|
|
29
31
|
onSkip?: () => void;
|
|
30
32
|
onClose?: () => void;
|
|
31
33
|
onNext?: () => void;
|
|
34
|
+
submitTooltip?: ReactNode;
|
|
32
35
|
onPrevious?: () => void;
|
|
33
36
|
currentStep?: number;
|
|
34
37
|
totalSteps?: number;
|
|
@@ -52,12 +55,14 @@ export const Question = ({
|
|
|
52
55
|
onSelectionChange,
|
|
53
56
|
customAnswer = '',
|
|
54
57
|
onCustomAnswerChange,
|
|
58
|
+
onCustomAnswerConfirm,
|
|
55
59
|
showCustomAnswer = true,
|
|
56
60
|
customAnswerPlaceholder = 'Type your answer…',
|
|
57
61
|
showSkip = true,
|
|
58
62
|
onSkip,
|
|
59
63
|
onClose,
|
|
60
64
|
onNext,
|
|
65
|
+
submitTooltip,
|
|
61
66
|
onPrevious,
|
|
62
67
|
currentStep,
|
|
63
68
|
totalSteps,
|
|
@@ -238,6 +243,16 @@ export const Question = ({
|
|
|
238
243
|
value={customAnswer}
|
|
239
244
|
onValueChange={(value) => onCustomAnswerChange?.(value)}
|
|
240
245
|
onFocus={handleCustomFocus}
|
|
246
|
+
onKeyDown={
|
|
247
|
+
onCustomAnswerConfirm
|
|
248
|
+
? (e: KeyboardEvent<HTMLInputElement>) => {
|
|
249
|
+
if (e.key === 'Enter' && !e.shiftKey && !(e.metaKey || e.ctrlKey)) {
|
|
250
|
+
e.preventDefault();
|
|
251
|
+
onCustomAnswerConfirm();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
: undefined
|
|
255
|
+
}
|
|
241
256
|
placeholder={customAnswerPlaceholder}
|
|
242
257
|
sizing="small"
|
|
243
258
|
testId="question-custom"
|
|
@@ -302,17 +317,30 @@ export const Question = ({
|
|
|
302
317
|
Skip
|
|
303
318
|
</Button>
|
|
304
319
|
)}
|
|
305
|
-
{(multiSelect || isCustomSelected) &&
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
320
|
+
{(multiSelect || isCustomSelected) &&
|
|
321
|
+
(submitTooltip ? (
|
|
322
|
+
<Tooltip tooltip={submitTooltip} placement="top" triggerAsChild>
|
|
323
|
+
<Button
|
|
324
|
+
argo
|
|
325
|
+
variant="primary"
|
|
326
|
+
size="small"
|
|
327
|
+
onClick={onNext}
|
|
328
|
+
data-testid="question-next-button"
|
|
329
|
+
>
|
|
330
|
+
{isLastStep ? 'Submit' : 'Next'}
|
|
331
|
+
</Button>
|
|
332
|
+
</Tooltip>
|
|
333
|
+
) : (
|
|
334
|
+
<Button
|
|
335
|
+
argo
|
|
336
|
+
variant="primary"
|
|
337
|
+
size="small"
|
|
338
|
+
onClick={onNext}
|
|
339
|
+
data-testid="question-next-button"
|
|
340
|
+
>
|
|
341
|
+
{isLastStep ? 'Submit' : 'Next'}
|
|
342
|
+
</Button>
|
|
343
|
+
))}
|
|
316
344
|
</div>
|
|
317
345
|
</div>
|
|
318
346
|
</div>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { Play } from '../../icons';
|
|
4
4
|
|
|
5
5
|
import { SuggestedPrompt } from './SuggestedPrompt';
|
|
6
6
|
|
|
@@ -53,13 +53,7 @@ export const LongLabel: Story = {
|
|
|
53
53
|
export const WithCustomIcon: Story = {
|
|
54
54
|
args: {
|
|
55
55
|
label: 'Run a transformation',
|
|
56
|
-
icon:
|
|
57
|
-
<Icon
|
|
58
|
-
className="tw-size-4 tw-shrink-0 tw-basis-4 tw-text-secondary-500"
|
|
59
|
-
fixedWidth
|
|
60
|
-
icon="play"
|
|
61
|
-
/>
|
|
62
|
-
),
|
|
56
|
+
icon: <Play className="tw-size-4 tw-shrink-0 tw-basis-4 tw-text-secondary-500" />,
|
|
63
57
|
disabled: false,
|
|
64
58
|
},
|
|
65
59
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
2
|
|
|
3
3
|
import { Button } from '../../components/Button';
|
|
4
|
-
import {
|
|
4
|
+
import { Bell, List, Search, Table } from '../../icons';
|
|
5
5
|
|
|
6
6
|
import { EmptyState } from './EmptyState';
|
|
7
7
|
|
|
@@ -24,7 +24,7 @@ export const ListEmpty: Story = {
|
|
|
24
24
|
render: () => (
|
|
25
25
|
<EmptyState>
|
|
26
26
|
<EmptyState.Icon>
|
|
27
|
-
<
|
|
27
|
+
<List />
|
|
28
28
|
</EmptyState.Icon>
|
|
29
29
|
<EmptyState.Title>No items yet</EmptyState.Title>
|
|
30
30
|
<EmptyState.Description>
|
|
@@ -42,7 +42,7 @@ export const TableEmpty: Story = {
|
|
|
42
42
|
render: () => (
|
|
43
43
|
<EmptyState>
|
|
44
44
|
<EmptyState.Icon>
|
|
45
|
-
<
|
|
45
|
+
<Table />
|
|
46
46
|
</EmptyState.Icon>
|
|
47
47
|
<EmptyState.Title>No rows found</EmptyState.Title>
|
|
48
48
|
<EmptyState.Description>
|
|
@@ -63,7 +63,7 @@ export const SearchNoResults: Story = {
|
|
|
63
63
|
render: () => (
|
|
64
64
|
<EmptyState>
|
|
65
65
|
<EmptyState.Icon>
|
|
66
|
-
<
|
|
66
|
+
<Search />
|
|
67
67
|
</EmptyState.Icon>
|
|
68
68
|
<EmptyState.Title>No results found</EmptyState.Title>
|
|
69
69
|
<EmptyState.Description>
|
|
@@ -84,7 +84,7 @@ export const CompactListEmpty: Story = {
|
|
|
84
84
|
render: () => (
|
|
85
85
|
<EmptyState variant="compact">
|
|
86
86
|
<EmptyState.Icon variant="compact">
|
|
87
|
-
<
|
|
87
|
+
<Bell />
|
|
88
88
|
</EmptyState.Icon>
|
|
89
89
|
<EmptyState.Title variant="compact">No notifications</EmptyState.Title>
|
|
90
90
|
<EmptyState.Description variant="compact">You're all caught up!</EmptyState.Description>
|
|
@@ -19,8 +19,8 @@ const emptyStateIconClassnames = cva(
|
|
|
19
19
|
{
|
|
20
20
|
variants: {
|
|
21
21
|
variant: {
|
|
22
|
-
default: ['tw-size-16', 'tw-text-4xl'],
|
|
23
|
-
compact: ['tw-size-10', 'tw-text-2xl'],
|
|
22
|
+
default: ['tw-size-16', 'tw-text-4xl', '[&_svg]:tw-size-10'],
|
|
23
|
+
compact: ['tw-size-10', 'tw-text-2xl', '[&_svg]:tw-size-6'],
|
|
24
24
|
},
|
|
25
25
|
},
|
|
26
26
|
defaultVariants: {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
2
|
|
|
3
3
|
import { Button } from '../../components/Button';
|
|
4
|
-
import {
|
|
4
|
+
import { AlertCircle, AlertTriangle, Bug, Unlink, XCircle } from '../../icons';
|
|
5
5
|
|
|
6
6
|
import { ErrorState } from './ErrorState';
|
|
7
7
|
|
|
@@ -24,7 +24,7 @@ export const ClientError: Story = {
|
|
|
24
24
|
render: () => (
|
|
25
25
|
<ErrorState>
|
|
26
26
|
<ErrorState.Icon>
|
|
27
|
-
<
|
|
27
|
+
<AlertCircle />
|
|
28
28
|
</ErrorState.Icon>
|
|
29
29
|
<ErrorState.Title>Page not found</ErrorState.Title>
|
|
30
30
|
<ErrorState.Description>
|
|
@@ -46,7 +46,7 @@ export const ServerError: Story = {
|
|
|
46
46
|
render: () => (
|
|
47
47
|
<ErrorState>
|
|
48
48
|
<ErrorState.Icon>
|
|
49
|
-
<
|
|
49
|
+
<Bug />
|
|
50
50
|
</ErrorState.Icon>
|
|
51
51
|
<ErrorState.Title>Something went wrong</ErrorState.Title>
|
|
52
52
|
<ErrorState.Description>
|
|
@@ -68,7 +68,7 @@ export const NetworkFailure: Story = {
|
|
|
68
68
|
render: () => (
|
|
69
69
|
<ErrorState>
|
|
70
70
|
<ErrorState.Icon>
|
|
71
|
-
<
|
|
71
|
+
<Unlink />
|
|
72
72
|
</ErrorState.Icon>
|
|
73
73
|
<ErrorState.Title>No connection</ErrorState.Title>
|
|
74
74
|
<ErrorState.Description>
|
|
@@ -86,7 +86,7 @@ export const Destructive: Story = {
|
|
|
86
86
|
render: () => (
|
|
87
87
|
<ErrorState variant="destructive">
|
|
88
88
|
<ErrorState.Icon variant="destructive">
|
|
89
|
-
<
|
|
89
|
+
<AlertTriangle />
|
|
90
90
|
</ErrorState.Icon>
|
|
91
91
|
<ErrorState.Title variant="destructive">Critical error</ErrorState.Title>
|
|
92
92
|
<ErrorState.Description variant="destructive">
|
|
@@ -109,7 +109,7 @@ export const CompactServerError: Story = {
|
|
|
109
109
|
render: () => (
|
|
110
110
|
<ErrorState variant="compact">
|
|
111
111
|
<ErrorState.Icon variant="compact">
|
|
112
|
-
<
|
|
112
|
+
<XCircle />
|
|
113
113
|
</ErrorState.Icon>
|
|
114
114
|
<ErrorState.Title variant="compact">Failed to load</ErrorState.Title>
|
|
115
115
|
<ErrorState.Description variant="compact">
|
|
@@ -18,9 +18,9 @@ const errorStateClassnames = cva(['tw-flex', 'tw-flex-col', 'tw-items-center', '
|
|
|
18
18
|
const errorStateIconClassnames = cva(['tw-flex', 'tw-items-center', 'tw-justify-center'], {
|
|
19
19
|
variants: {
|
|
20
20
|
variant: {
|
|
21
|
-
default: ['tw-size-16', 'tw-text-4xl', 'tw-text-warning-500'],
|
|
22
|
-
compact: ['tw-size-10', 'tw-text-2xl', 'tw-text-warning-500'],
|
|
23
|
-
destructive: ['tw-size-16', 'tw-text-4xl', 'tw-text-error-500'],
|
|
21
|
+
default: ['tw-size-16', 'tw-text-4xl', '[&_svg]:tw-size-10', 'tw-text-warning-500'],
|
|
22
|
+
compact: ['tw-size-10', 'tw-text-2xl', '[&_svg]:tw-size-6', 'tw-text-warning-500'],
|
|
23
|
+
destructive: ['tw-size-16', 'tw-text-4xl', '[&_svg]:tw-size-10', 'tw-text-error-500'],
|
|
24
24
|
},
|
|
25
25
|
},
|
|
26
26
|
defaultVariants: {
|
|
@@ -48,7 +48,7 @@ either variant.
|
|
|
48
48
|
## Rules from the patterns
|
|
49
49
|
|
|
50
50
|
- **Index**: no breadcrumbs, no overflow menu, no search/filter/sort. Search
|
|
51
|
-
belongs in `
|
|
51
|
+
belongs in `PageToolbar`.
|
|
52
52
|
- **Detail**: breadcrumbs are required (use real entity names; last item is
|
|
53
53
|
not a link). Overflow menu lives on the breadcrumbs row, never in the title
|
|
54
54
|
row.
|
|
@@ -18,6 +18,7 @@ import { DropdownMenu } from '../../components/DropdownMenu';
|
|
|
18
18
|
import { MarkdownEditor } from '../../components/MarkdownEditor';
|
|
19
19
|
import { Tabs, TabsContent } from '../../components/Tabs';
|
|
20
20
|
import { PageLayout } from '../PageLayout';
|
|
21
|
+
import { PageToolbar } from '../PageToolbar';
|
|
21
22
|
import { SplitButton } from '../SplitButton';
|
|
22
23
|
|
|
23
24
|
import { PageHeader } from './PageHeader';
|
|
@@ -69,9 +70,11 @@ export const IndexFlows: Story = {
|
|
|
69
70
|
</>
|
|
70
71
|
}
|
|
71
72
|
/>
|
|
72
|
-
<
|
|
73
|
-
<
|
|
74
|
-
|
|
73
|
+
<PageToolbar>
|
|
74
|
+
<PageToolbar.Search>
|
|
75
|
+
<Placeholder label="Search input + filter pills" height="tw-h-10" />
|
|
76
|
+
</PageToolbar.Search>
|
|
77
|
+
</PageToolbar>
|
|
75
78
|
<PageLayout.Content>
|
|
76
79
|
<Placeholder label="EntityList" height="tw-h-96" />
|
|
77
80
|
</PageLayout.Content>
|
|
@@ -11,7 +11,7 @@ import { descriptionClass, titleClass, titleRowClass } from './styles';
|
|
|
11
11
|
// - At most one primary `action`; pair with the `documentation` icon button.
|
|
12
12
|
/**
|
|
13
13
|
* Props for `PageHeader.Index`. Index pages have no breadcrumbs and no overflow menu — search and filters
|
|
14
|
-
* belong in `
|
|
14
|
+
* belong in `PageToolbar`, not here.
|
|
15
15
|
*/
|
|
16
16
|
type PageHeaderIndexProps = Omit<ComponentPropsWithoutRef<'header'>, 'title'> & {
|
|
17
17
|
title: ReactNode;
|
|
@@ -35,7 +35,7 @@ type PageHeaderIndexProps = Omit<ComponentPropsWithoutRef<'header'>, 'title'> &
|
|
|
35
35
|
* **When NOT to use:** for single-entity detail pages use `PageHeader.Detail`; for layouts that don't fit
|
|
36
36
|
* either pattern host custom JSX in `PageLayout.Header` directly.
|
|
37
37
|
* **Composition:** nest inside `PageLayout.Header`. Always pass `Button argo` into `action` and
|
|
38
|
-
* `documentation`. Search and filter chips belong in `
|
|
38
|
+
* `documentation`. Search and filter chips belong in `PageToolbar`, not here.
|
|
39
39
|
*/
|
|
40
40
|
export const PageHeaderIndex = ({
|
|
41
41
|
ref,
|
|
@@ -24,7 +24,7 @@ PageLayout (Root) ← max-width container, vertical rhythm (gap-4)
|
|
|
24
24
|
├── PageLayout.Breadcrumbs ← Detail pattern only; usually composed by PageHeader.Detail
|
|
25
25
|
├── PageLayout.Header ← host PageHeader.Index / PageHeader.Detail (or custom JSX)
|
|
26
26
|
├── PageLayout.Tabs ← escape hatch for routing-based tab rows
|
|
27
|
-
├──
|
|
27
|
+
├── PageToolbar ← search / filter pills (NEVER in header) — separate block
|
|
28
28
|
├── PageLayout.Content ← main slot
|
|
29
29
|
└── PageLayout.Footer ← bottom action bar (form submit, `sticky` variant)
|
|
30
30
|
```
|
|
@@ -42,7 +42,7 @@ All slots take any `ReactNode` — no structured props live on the slot itself.
|
|
|
42
42
|
<PageLayout.Header>
|
|
43
43
|
<PageHeader.Index title="Storage" metadata="3 buckets and 39 tables" … />
|
|
44
44
|
</PageLayout.Header>
|
|
45
|
-
<
|
|
45
|
+
<PageToolbar>{searchAndFilters}</PageToolbar>
|
|
46
46
|
<PageLayout.Content>{list}</PageLayout.Content>
|
|
47
47
|
</PageLayout>
|
|
48
48
|
|
|
@@ -73,7 +73,7 @@ provides the chrome.
|
|
|
73
73
|
- **Index pages do not show breadcrumbs.** Skip `PageLayout.Breadcrumbs`
|
|
74
74
|
entirely on top-level list pages.
|
|
75
75
|
- **No search / filters / sorting in the header** — those belong in
|
|
76
|
-
`
|
|
76
|
+
`PageToolbar`.
|
|
77
77
|
|
|
78
78
|
## Component rules
|
|
79
79
|
|
|
@@ -3,6 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
|
3
3
|
import { Breadcrumb } from '../../components/Breadcrumb';
|
|
4
4
|
import { Button } from '../../components/Button';
|
|
5
5
|
import { Tabs, TabsContent } from '../../components/Tabs';
|
|
6
|
+
import { PageToolbar } from '../PageToolbar';
|
|
6
7
|
|
|
7
8
|
import { PageLayout } from './PageLayout';
|
|
8
9
|
|
|
@@ -15,7 +16,7 @@ export default {
|
|
|
15
16
|
docs: {
|
|
16
17
|
description: {
|
|
17
18
|
component:
|
|
18
|
-
'Compound block that gives every kbc-ui page the same shape — max-width container, vertical rhythm, and slots for `Breadcrumbs`, `Header`, `Tabs`, `
|
|
19
|
+
'Compound block that gives every kbc-ui page the same shape — max-width container, vertical rhythm, and slots for `Breadcrumbs`, `Header`, `Tabs`, `Content`, `Footer`. The slots are intentionally thin — for the structured Index / Detail page-header patterns reach for the `PageHeader` block, and for the search / filter row reach for the `PageToolbar` block. The page-navigation `Sidebar` lives **outside** `PageLayout` in the app shell.',
|
|
19
20
|
},
|
|
20
21
|
},
|
|
21
22
|
},
|
|
@@ -53,9 +54,11 @@ export const Anatomy: Story = {
|
|
|
53
54
|
<Placeholder label="PageHeader.Index / PageHeader.Detail goes here" height="tw-h-24" />
|
|
54
55
|
</PageLayout.Header>
|
|
55
56
|
|
|
56
|
-
<
|
|
57
|
-
<
|
|
58
|
-
|
|
57
|
+
<PageToolbar>
|
|
58
|
+
<PageToolbar.Search>
|
|
59
|
+
<Placeholder label="Search input + filter pills" height="tw-h-10" />
|
|
60
|
+
</PageToolbar.Search>
|
|
61
|
+
</PageToolbar>
|
|
59
62
|
|
|
60
63
|
<PageLayout.Content>
|
|
61
64
|
<Placeholder label="EntityList / Table" height="tw-h-96" />
|
|
@@ -75,21 +75,6 @@ const Tabs = ({
|
|
|
75
75
|
</div>
|
|
76
76
|
);
|
|
77
77
|
|
|
78
|
-
const Toolbar = ({
|
|
79
|
-
ref,
|
|
80
|
-
className,
|
|
81
|
-
children,
|
|
82
|
-
...props
|
|
83
|
-
}: ComponentPropsWithoutRef<'div'> & { ref?: Ref<ElementRef<'div'>> }) => (
|
|
84
|
-
<div
|
|
85
|
-
ref={ref}
|
|
86
|
-
className={cn('tw-flex tw-w-full tw-flex-wrap tw-items-center tw-gap-3', className)}
|
|
87
|
-
{...props}
|
|
88
|
-
>
|
|
89
|
-
{children}
|
|
90
|
-
</div>
|
|
91
|
-
);
|
|
92
|
-
|
|
93
78
|
const Content = ({
|
|
94
79
|
ref,
|
|
95
80
|
className,
|
|
@@ -135,7 +120,7 @@ const Footer = ({
|
|
|
135
120
|
* **When NOT to use:** the left-rail `Sidebar` lives in the app shell outside `PageLayout`. For embedded
|
|
136
121
|
* widgets or modal bodies the layout chrome is the modal / card itself, not `PageLayout`.
|
|
137
122
|
* **Composition:** `PageLayout` is opinionated about chrome only — slots accept any `ReactNode`. Put
|
|
138
|
-
* search / filter pills in `
|
|
123
|
+
* search / filter pills in `PageToolbar`, never in the header. Form submit bars go in
|
|
139
124
|
* `PageLayout.Footer sticky`.
|
|
140
125
|
*
|
|
141
126
|
* @see RECIPES.md#list-detail — for the index → detail page pattern
|
|
@@ -147,8 +132,6 @@ export const PageLayout = Object.assign(Root, {
|
|
|
147
132
|
Header,
|
|
148
133
|
/** Escape hatch for routing-based tab rows. Use `Tabs` / `TabsContent` core components inside, never a raw `<button>`. */
|
|
149
134
|
Tabs,
|
|
150
|
-
/** Search / filter pill row. Belongs here, NOT in the header per the page-header patterns. */
|
|
151
|
-
Toolbar,
|
|
152
135
|
/** Main content slot. Lists, cards, tables. */
|
|
153
136
|
Content,
|
|
154
137
|
/** Bottom action bar — form submit + cancel. Pass `sticky` for a footer that pins to the viewport. */
|