@papernote/ui 1.3.1 → 1.6.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/components/ActionBar.d.ts +112 -0
- package/dist/components/ActionBar.d.ts.map +1 -0
- package/dist/components/BottomNavigation.d.ts +98 -0
- package/dist/components/BottomNavigation.d.ts.map +1 -0
- package/dist/components/Checkbox.d.ts +2 -0
- package/dist/components/Checkbox.d.ts.map +1 -1
- package/dist/components/CheckboxList.d.ts +81 -0
- package/dist/components/CheckboxList.d.ts.map +1 -0
- package/dist/components/Chip.d.ts +92 -1
- package/dist/components/Chip.d.ts.map +1 -1
- package/dist/components/ConfirmDialog.d.ts +43 -1
- package/dist/components/ConfirmDialog.d.ts.map +1 -1
- package/dist/components/DataTable.d.ts +10 -1
- package/dist/components/DataTable.d.ts.map +1 -1
- package/dist/components/DataTableCardView.d.ts +99 -0
- package/dist/components/DataTableCardView.d.ts.map +1 -0
- package/dist/components/ExpandablePanel.d.ts +142 -0
- package/dist/components/ExpandablePanel.d.ts.map +1 -0
- package/dist/components/FloatingActionButton.d.ts +98 -0
- package/dist/components/FloatingActionButton.d.ts.map +1 -0
- package/dist/components/Input.d.ts +45 -1
- package/dist/components/Input.d.ts.map +1 -1
- package/dist/components/MobileHeader.d.ts +98 -0
- package/dist/components/MobileHeader.d.ts.map +1 -0
- package/dist/components/MobileLayout.d.ts +121 -0
- package/dist/components/MobileLayout.d.ts.map +1 -0
- package/dist/components/Modal.d.ts +78 -1
- package/dist/components/Modal.d.ts.map +1 -1
- package/dist/components/PageHeader.d.ts +86 -0
- package/dist/components/PageHeader.d.ts.map +1 -0
- package/dist/components/PullToRefresh.d.ts +87 -0
- package/dist/components/PullToRefresh.d.ts.map +1 -0
- package/dist/components/QueryTransparency.d.ts +1 -1
- package/dist/components/QueryTransparency.d.ts.map +1 -1
- package/dist/components/SearchableList.d.ts +83 -0
- package/dist/components/SearchableList.d.ts.map +1 -0
- package/dist/components/Select.d.ts +16 -2
- package/dist/components/Select.d.ts.map +1 -1
- package/dist/components/Sidebar.d.ts +40 -1
- package/dist/components/Sidebar.d.ts.map +1 -1
- package/dist/components/SwipeActions.d.ts +93 -0
- package/dist/components/SwipeActions.d.ts.map +1 -0
- package/dist/components/Switch.d.ts +1 -0
- package/dist/components/Switch.d.ts.map +1 -1
- package/dist/components/Textarea.d.ts +13 -0
- package/dist/components/Textarea.d.ts.map +1 -1
- package/dist/components/index.d.ts +31 -3
- package/dist/components/index.d.ts.map +1 -1
- package/dist/context/MobileContext.d.ts +168 -0
- package/dist/context/MobileContext.d.ts.map +1 -0
- package/dist/hooks/useResponsive.d.ts +158 -0
- package/dist/hooks/useResponsive.d.ts.map +1 -0
- package/dist/index.d.ts +1871 -51
- package/dist/index.esm.js +3025 -196
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +3063 -194
- package/dist/index.js.map +1 -1
- package/dist/styles.css +434 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/ActionBar.stories.tsx +246 -0
- package/src/components/ActionBar.tsx +242 -0
- package/src/components/BottomNavigation.stories.tsx +142 -0
- package/src/components/BottomNavigation.tsx +225 -0
- package/src/components/Checkbox.stories.tsx +162 -0
- package/src/components/Checkbox.tsx +22 -6
- package/src/components/CheckboxList.stories.tsx +311 -0
- package/src/components/CheckboxList.tsx +433 -0
- package/src/components/Chip.stories.tsx +389 -0
- package/src/components/Chip.tsx +182 -3
- package/src/components/ConfirmDialog.tsx +56 -4
- package/src/components/DataTable.tsx +60 -1
- package/src/components/DataTableCardView.stories.tsx +307 -0
- package/src/components/DataTableCardView.tsx +419 -0
- package/src/components/ExpandablePanel.stories.tsx +620 -0
- package/src/components/ExpandablePanel.tsx +383 -0
- package/src/components/FloatingActionButton.stories.tsx +197 -0
- package/src/components/FloatingActionButton.tsx +301 -0
- package/src/components/Grid.stories.tsx +16 -16
- package/src/components/Input.stories.tsx +214 -0
- package/src/components/Input.tsx +81 -4
- package/src/components/MobileHeader.stories.tsx +205 -0
- package/src/components/MobileHeader.tsx +233 -0
- package/src/components/MobileLayout.stories.tsx +338 -0
- package/src/components/MobileLayout.tsx +313 -0
- package/src/components/Modal.stories.tsx +388 -0
- package/src/components/Modal.tsx +122 -4
- package/src/components/PageHeader.stories.tsx +198 -0
- package/src/components/PageHeader.tsx +217 -0
- package/src/components/PullToRefresh.stories.tsx +321 -0
- package/src/components/PullToRefresh.tsx +294 -0
- package/src/components/QueryTransparency.tsx +1 -1
- package/src/components/SearchableList.stories.tsx +437 -0
- package/src/components/SearchableList.tsx +326 -0
- package/src/components/Select.stories.tsx +190 -0
- package/src/components/Select.tsx +353 -137
- package/src/components/Sidebar.tsx +193 -10
- package/src/components/SwipeActions.stories.tsx +327 -0
- package/src/components/SwipeActions.tsx +387 -0
- package/src/components/Switch.stories.tsx +158 -0
- package/src/components/Switch.tsx +12 -3
- package/src/components/Textarea.tsx +31 -1
- package/src/components/index.ts +69 -3
- package/src/context/MobileContext.tsx +296 -0
- package/src/hooks/useResponsive.ts +360 -0
- package/src/types/index.ts +4 -0
- package/tailwind.config.js +56 -1
package/dist/types/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Core types for notebook-ui library
|
|
3
3
|
*/
|
|
4
|
+
import type { QueryTransparencyInfo } from '../components/QueryTransparency';
|
|
4
5
|
/**
|
|
5
6
|
* Base interface for all data items that can be displayed in tables
|
|
6
7
|
*/
|
|
@@ -19,6 +20,7 @@ export interface PaginationResponse<T = BaseDataItem> {
|
|
|
19
20
|
pageSize: number;
|
|
20
21
|
executedQuery?: string;
|
|
21
22
|
executionTimeMs?: number;
|
|
23
|
+
queryInfo?: QueryTransparencyInfo;
|
|
22
24
|
}
|
|
23
25
|
/**
|
|
24
26
|
* Parameters for fetching paginated data
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;IACpB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB,CAAC,CAAC,GAAG,YAAY;IAClD,KAAK,EAAE,CAAC,EAAE,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IAEjB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,eAAe,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAC;AAE7E;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;IACpB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB,CAAC,CAAC,GAAG,YAAY;IAClD,KAAK,EAAE,CAAC,EAAE,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IAEjB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB,SAAS,CAAC,EAAE,qBAAqB,CAAC;CACnC;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,IAAI,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,aAAa,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,IAAI,CAAC;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,KAAK,GAAG,MAAM,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB,CAAC,CAAC,GAAG,YAAY;IACnD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACzC,WAAW,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;IAChC,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,OAAO,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,CAAC;IAC1E,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB,CAAC,CAAC,GAAG,YAAY;IAClD,IAAI,EAAE,CAAC,EAAE,CAAC;IACV,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,IAAI,EAAE,UAAU,GAAG,IAAI,GAAG,SAAS,CAAC;IACpC,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,UAAU,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IACvD,OAAO,EAAE,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,KAAK,IAAI,CAAC;IAC3C,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import ActionBar from './ActionBar';
|
|
3
|
+
import Text from './Text';
|
|
4
|
+
import Button from './Button';
|
|
5
|
+
import { Download, Trash2, Archive, Tag, Mail, Copy, X, Search, Filter } from 'lucide-react';
|
|
6
|
+
|
|
7
|
+
const meta: Meta<typeof ActionBar> = {
|
|
8
|
+
title: 'Layout/ActionBar',
|
|
9
|
+
component: ActionBar,
|
|
10
|
+
parameters: {
|
|
11
|
+
layout: 'fullscreen',
|
|
12
|
+
docs: {
|
|
13
|
+
description: {
|
|
14
|
+
component: 'A flexible toolbar component for page-level and contextual actions. Use for bulk actions, form buttons, or any contextual toolbar needs.',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
tags: ['autodocs'],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export default meta;
|
|
22
|
+
type Story = StoryObj<typeof ActionBar>;
|
|
23
|
+
|
|
24
|
+
export const Default: Story = {
|
|
25
|
+
args: {
|
|
26
|
+
leftContent: <Text weight="medium">3 items selected</Text>,
|
|
27
|
+
actions: [
|
|
28
|
+
{ id: 'export', label: 'Export', icon: <Download className="h-4 w-4" />, onClick: () => alert('Export') },
|
|
29
|
+
{ id: 'delete', label: 'Delete', icon: <Trash2 className="h-4 w-4" />, onClick: () => alert('Delete'), variant: 'danger' },
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const BulkActions: Story = {
|
|
35
|
+
args: {
|
|
36
|
+
leftContent: <Text weight="medium">12 items selected</Text>,
|
|
37
|
+
actions: [
|
|
38
|
+
{ id: 'archive', label: 'Archive', icon: <Archive className="h-4 w-4" />, onClick: () => {} },
|
|
39
|
+
{ id: 'tag', label: 'Add Tag', icon: <Tag className="h-4 w-4" />, onClick: () => {} },
|
|
40
|
+
{ id: 'export', label: 'Export', icon: <Download className="h-4 w-4" />, onClick: () => {} },
|
|
41
|
+
{ id: 'delete', label: 'Delete', icon: <Trash2 className="h-4 w-4" />, onClick: () => {}, variant: 'danger' },
|
|
42
|
+
],
|
|
43
|
+
showDismiss: true,
|
|
44
|
+
onDismiss: () => alert('Selection cleared'),
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const WithDismiss: Story = {
|
|
49
|
+
args: {
|
|
50
|
+
leftContent: <Text weight="medium">5 emails selected</Text>,
|
|
51
|
+
actions: [
|
|
52
|
+
{ id: 'reply', label: 'Reply All', icon: <Mail className="h-4 w-4" />, onClick: () => {} },
|
|
53
|
+
{ id: 'archive', label: 'Archive', icon: <Archive className="h-4 w-4" />, onClick: () => {} },
|
|
54
|
+
{ id: 'delete', label: 'Delete', icon: <Trash2 className="h-4 w-4" />, onClick: () => {}, variant: 'danger' },
|
|
55
|
+
],
|
|
56
|
+
showDismiss: true,
|
|
57
|
+
onDismiss: () => alert('Dismissed'),
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const FormActions: Story = {
|
|
62
|
+
args: {
|
|
63
|
+
position: 'bottom',
|
|
64
|
+
rightContent: (
|
|
65
|
+
<>
|
|
66
|
+
<Button variant="ghost" onClick={() => alert('Cancel')}>Cancel</Button>
|
|
67
|
+
<Button variant="primary" onClick={() => alert('Save')}>Save Changes</Button>
|
|
68
|
+
</>
|
|
69
|
+
),
|
|
70
|
+
},
|
|
71
|
+
decorators: [
|
|
72
|
+
(Story) => (
|
|
73
|
+
<div style={{ height: '300px', display: 'flex', flexDirection: 'column' }}>
|
|
74
|
+
<div className="flex-1 p-6 bg-paper-50">
|
|
75
|
+
<Text>Form content goes here...</Text>
|
|
76
|
+
</div>
|
|
77
|
+
<Story />
|
|
78
|
+
</div>
|
|
79
|
+
),
|
|
80
|
+
],
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export const StickyBottom: Story = {
|
|
84
|
+
args: {
|
|
85
|
+
position: 'bottom',
|
|
86
|
+
sticky: true,
|
|
87
|
+
rightContent: (
|
|
88
|
+
<>
|
|
89
|
+
<Button variant="ghost" onClick={() => {}}>Discard</Button>
|
|
90
|
+
<Button variant="secondary" onClick={() => {}}>Save Draft</Button>
|
|
91
|
+
<Button variant="primary" onClick={() => {}}>Publish</Button>
|
|
92
|
+
</>
|
|
93
|
+
),
|
|
94
|
+
},
|
|
95
|
+
decorators: [
|
|
96
|
+
(Story) => (
|
|
97
|
+
<div style={{ height: '150vh' }}>
|
|
98
|
+
<div className="p-6">
|
|
99
|
+
<Text size="lg" weight="bold">Scroll down to see sticky bottom bar</Text>
|
|
100
|
+
<div className="mt-4 space-y-4">
|
|
101
|
+
{Array.from({ length: 15 }).map((_, i) => (
|
|
102
|
+
<div key={i} className="p-4 bg-paper-100 rounded-lg">
|
|
103
|
+
Form field {i + 1}
|
|
104
|
+
</div>
|
|
105
|
+
))}
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
<Story />
|
|
109
|
+
</div>
|
|
110
|
+
),
|
|
111
|
+
],
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export const InfoVariant: Story = {
|
|
115
|
+
args: {
|
|
116
|
+
variant: 'info',
|
|
117
|
+
centerContent: <Text size="sm">Showing 42 results for "wireless headphones"</Text>,
|
|
118
|
+
showDismiss: true,
|
|
119
|
+
onDismiss: () => alert('Clear search'),
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export const WarningVariant: Story = {
|
|
124
|
+
args: {
|
|
125
|
+
variant: 'warning',
|
|
126
|
+
leftContent: <Text size="sm" weight="medium">You have unsaved changes</Text>,
|
|
127
|
+
actions: [
|
|
128
|
+
{ id: 'discard', label: 'Discard', onClick: () => {}, variant: 'ghost' },
|
|
129
|
+
{ id: 'save', label: 'Save Now', onClick: () => {}, variant: 'primary' },
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export const PrimaryVariant: Story = {
|
|
135
|
+
args: {
|
|
136
|
+
variant: 'primary',
|
|
137
|
+
leftContent: <Text size="sm" weight="medium">Batch operation ready</Text>,
|
|
138
|
+
actions: [
|
|
139
|
+
{ id: 'cancel', label: 'Cancel', onClick: () => {}, variant: 'ghost' },
|
|
140
|
+
{ id: 'execute', label: 'Execute', onClick: () => {}, variant: 'primary' },
|
|
141
|
+
],
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export const Compact: Story = {
|
|
146
|
+
args: {
|
|
147
|
+
compact: true,
|
|
148
|
+
leftContent: <Text size="sm">Quick actions</Text>,
|
|
149
|
+
actions: [
|
|
150
|
+
{ id: 'copy', label: 'Copy', icon: <Copy className="h-4 w-4" />, onClick: () => {} },
|
|
151
|
+
{ id: 'delete', label: 'Delete', icon: <Trash2 className="h-4 w-4" />, onClick: () => {}, variant: 'danger' },
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export const WithCenterContent: Story = {
|
|
157
|
+
args: {
|
|
158
|
+
leftContent: <Text size="sm" color="muted">Page 1 of 10</Text>,
|
|
159
|
+
centerContent: (
|
|
160
|
+
<div className="flex items-center gap-2">
|
|
161
|
+
<Search className="h-4 w-4 text-ink-400" />
|
|
162
|
+
<Text size="sm">Filtered by: Active users</Text>
|
|
163
|
+
</div>
|
|
164
|
+
),
|
|
165
|
+
actions: [
|
|
166
|
+
{ id: 'filter', label: 'Edit Filters', icon: <Filter className="h-4 w-4" />, onClick: () => {} },
|
|
167
|
+
],
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
export const LoadingAction: Story = {
|
|
172
|
+
args: {
|
|
173
|
+
leftContent: <Text weight="medium">Processing 50 items...</Text>,
|
|
174
|
+
actions: [
|
|
175
|
+
{ id: 'cancel', label: 'Cancel', onClick: () => {}, variant: 'ghost' },
|
|
176
|
+
{ id: 'processing', label: 'Processing...', onClick: () => {}, variant: 'primary', loading: true },
|
|
177
|
+
],
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
export const Hidden: Story = {
|
|
182
|
+
args: {
|
|
183
|
+
visible: false,
|
|
184
|
+
leftContent: <Text>This bar is hidden</Text>,
|
|
185
|
+
},
|
|
186
|
+
parameters: {
|
|
187
|
+
docs: {
|
|
188
|
+
description: {
|
|
189
|
+
story: 'The ActionBar can be conditionally shown/hidden using the `visible` prop. This is useful for showing bulk action bars only when items are selected.',
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
export const ConditionalDisplay: Story = {
|
|
196
|
+
render: () => {
|
|
197
|
+
const selectedCount = 3; // Simulated selection
|
|
198
|
+
return (
|
|
199
|
+
<div>
|
|
200
|
+
<ActionBar
|
|
201
|
+
visible={selectedCount > 0}
|
|
202
|
+
leftContent={<Text weight="medium">{selectedCount} items selected</Text>}
|
|
203
|
+
actions={[
|
|
204
|
+
{ id: 'export', label: 'Export', icon: <Download className="h-4 w-4" />, onClick: () => {} },
|
|
205
|
+
{ id: 'delete', label: 'Delete', icon: <Trash2 className="h-4 w-4" />, onClick: () => {}, variant: 'danger' },
|
|
206
|
+
]}
|
|
207
|
+
showDismiss
|
|
208
|
+
onDismiss={() => {}}
|
|
209
|
+
/>
|
|
210
|
+
<div className="p-6">
|
|
211
|
+
<Text color="muted">
|
|
212
|
+
The ActionBar above appears when items are selected.
|
|
213
|
+
In a real app, clicking dismiss would clear the selection and hide the bar.
|
|
214
|
+
</Text>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
);
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
export const MultipleRows: Story = {
|
|
222
|
+
render: () => (
|
|
223
|
+
<div>
|
|
224
|
+
<ActionBar
|
|
225
|
+
leftContent={<Text size="sm" weight="medium">Filters: Category = Electronics, Status = Active</Text>}
|
|
226
|
+
actions={[
|
|
227
|
+
{ id: 'clear', label: 'Clear All', onClick: () => {}, variant: 'ghost' },
|
|
228
|
+
]}
|
|
229
|
+
compact
|
|
230
|
+
/>
|
|
231
|
+
<ActionBar
|
|
232
|
+
variant="primary"
|
|
233
|
+
leftContent={<Text size="sm" weight="medium">8 items selected</Text>}
|
|
234
|
+
actions={[
|
|
235
|
+
{ id: 'export', label: 'Export', icon: <Download className="h-4 w-4" />, onClick: () => {} },
|
|
236
|
+
{ id: 'delete', label: 'Delete', icon: <Trash2 className="h-4 w-4" />, onClick: () => {}, variant: 'danger' },
|
|
237
|
+
]}
|
|
238
|
+
showDismiss
|
|
239
|
+
onDismiss={() => {}}
|
|
240
|
+
/>
|
|
241
|
+
<div className="p-6">
|
|
242
|
+
<Text color="muted">Multiple ActionBars can be stacked for different purposes.</Text>
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
),
|
|
246
|
+
};
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { X } from 'lucide-react';
|
|
3
|
+
|
|
4
|
+
export interface ActionBarAction {
|
|
5
|
+
/** Unique identifier for the action */
|
|
6
|
+
id: string;
|
|
7
|
+
/** Button label text */
|
|
8
|
+
label: string;
|
|
9
|
+
/** Icon to display (from lucide-react) */
|
|
10
|
+
icon?: ReactNode;
|
|
11
|
+
/** Click handler */
|
|
12
|
+
onClick: () => void;
|
|
13
|
+
/** Button variant */
|
|
14
|
+
variant?: 'primary' | 'secondary' | 'ghost' | 'danger' | 'outline';
|
|
15
|
+
/** Disabled state */
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
/** Loading state */
|
|
18
|
+
loading?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ActionBarProps {
|
|
22
|
+
/** Content to display on the left side (e.g., selection count, status text) */
|
|
23
|
+
leftContent?: ReactNode;
|
|
24
|
+
/** Content to display in the center */
|
|
25
|
+
centerContent?: ReactNode;
|
|
26
|
+
/** Content to display on the right side (overrides actions) */
|
|
27
|
+
rightContent?: ReactNode;
|
|
28
|
+
/** Action buttons to display on the right */
|
|
29
|
+
actions?: ActionBarAction[];
|
|
30
|
+
/** Position of the action bar */
|
|
31
|
+
position?: 'top' | 'bottom';
|
|
32
|
+
/** Make the bar sticky */
|
|
33
|
+
sticky?: boolean;
|
|
34
|
+
/** Show the action bar (useful for conditional display like bulk selection) */
|
|
35
|
+
visible?: boolean;
|
|
36
|
+
/** Callback when close/dismiss button is clicked */
|
|
37
|
+
onDismiss?: () => void;
|
|
38
|
+
/** Show dismiss button */
|
|
39
|
+
showDismiss?: boolean;
|
|
40
|
+
/** Additional CSS classes */
|
|
41
|
+
className?: string;
|
|
42
|
+
/** Background variant */
|
|
43
|
+
variant?: 'default' | 'primary' | 'warning' | 'info';
|
|
44
|
+
/** Compact mode with less padding */
|
|
45
|
+
compact?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* ActionBar - Flexible toolbar for page-level and contextual actions
|
|
50
|
+
*
|
|
51
|
+
* A versatile action container that can be used for:
|
|
52
|
+
* - Bulk actions when rows are selected
|
|
53
|
+
* - Page-level actions and controls
|
|
54
|
+
* - Form action buttons (Save/Cancel)
|
|
55
|
+
* - Contextual toolbars
|
|
56
|
+
*
|
|
57
|
+
* @example Basic bulk actions bar
|
|
58
|
+
* ```tsx
|
|
59
|
+
* <ActionBar
|
|
60
|
+
* visible={selectedIds.length > 0}
|
|
61
|
+
* leftContent={<Text weight="medium">{selectedIds.length} selected</Text>}
|
|
62
|
+
* actions={[
|
|
63
|
+
* { id: 'export', label: 'Export', icon: <Download />, onClick: handleExport },
|
|
64
|
+
* { id: 'delete', label: 'Delete', icon: <Trash />, onClick: handleDelete, variant: 'danger' },
|
|
65
|
+
* ]}
|
|
66
|
+
* onDismiss={() => setSelectedIds([])}
|
|
67
|
+
* showDismiss
|
|
68
|
+
* />
|
|
69
|
+
* ```
|
|
70
|
+
*
|
|
71
|
+
* @example Sticky bottom form actions
|
|
72
|
+
* ```tsx
|
|
73
|
+
* <ActionBar
|
|
74
|
+
* position="bottom"
|
|
75
|
+
* sticky
|
|
76
|
+
* rightContent={
|
|
77
|
+
* <>
|
|
78
|
+
* <Button variant="ghost" onClick={handleCancel}>Cancel</Button>
|
|
79
|
+
* <Button variant="primary" onClick={handleSave} loading={isSaving}>Save Changes</Button>
|
|
80
|
+
* </>
|
|
81
|
+
* }
|
|
82
|
+
* />
|
|
83
|
+
* ```
|
|
84
|
+
*
|
|
85
|
+
* @example Info bar with center content
|
|
86
|
+
* ```tsx
|
|
87
|
+
* <ActionBar
|
|
88
|
+
* variant="info"
|
|
89
|
+
* centerContent={
|
|
90
|
+
* <Text size="sm">Showing results for "search term" - 42 items found</Text>
|
|
91
|
+
* }
|
|
92
|
+
* onDismiss={clearSearch}
|
|
93
|
+
* showDismiss
|
|
94
|
+
* />
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
export default function ActionBar({
|
|
98
|
+
leftContent,
|
|
99
|
+
centerContent,
|
|
100
|
+
rightContent,
|
|
101
|
+
actions,
|
|
102
|
+
position = 'top',
|
|
103
|
+
sticky = false,
|
|
104
|
+
visible = true,
|
|
105
|
+
onDismiss,
|
|
106
|
+
showDismiss = false,
|
|
107
|
+
className = '',
|
|
108
|
+
variant = 'default',
|
|
109
|
+
compact = false,
|
|
110
|
+
}: ActionBarProps) {
|
|
111
|
+
if (!visible) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const variantStyles = {
|
|
116
|
+
default: 'bg-white border-paper-200',
|
|
117
|
+
primary: 'bg-accent-50 border-accent-200',
|
|
118
|
+
warning: 'bg-warning-50 border-warning-200',
|
|
119
|
+
info: 'bg-blue-50 border-blue-200',
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const buttonVariantStyles: Record<string, string> = {
|
|
123
|
+
primary: 'bg-accent-500 text-white border-accent-500 hover:bg-accent-600 hover:shadow-sm',
|
|
124
|
+
secondary: 'bg-white text-ink-700 border-paper-300 hover:bg-paper-50 hover:border-paper-400 shadow-xs hover:shadow-sm',
|
|
125
|
+
ghost: 'bg-transparent text-ink-600 border-transparent hover:text-ink-800 hover:bg-paper-100',
|
|
126
|
+
danger: 'bg-error-500 text-white border-error-500 hover:bg-error-600 hover:shadow-sm',
|
|
127
|
+
outline: 'bg-transparent text-ink-700 border-paper-300 hover:bg-paper-50 hover:border-ink-400',
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const positionStyles = {
|
|
131
|
+
top: sticky ? 'sticky top-0 z-40 border-b' : 'border-b',
|
|
132
|
+
bottom: sticky ? 'sticky bottom-0 z-40 border-t' : 'border-t',
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<div
|
|
137
|
+
className={`
|
|
138
|
+
${variantStyles[variant]}
|
|
139
|
+
${positionStyles[position]}
|
|
140
|
+
${compact ? 'px-4 py-2' : 'px-6 py-3'}
|
|
141
|
+
${className}
|
|
142
|
+
`}
|
|
143
|
+
role="toolbar"
|
|
144
|
+
aria-label="Action bar"
|
|
145
|
+
>
|
|
146
|
+
<div className="flex items-center justify-between gap-4">
|
|
147
|
+
{/* Left section */}
|
|
148
|
+
<div className="flex items-center gap-3 min-w-0 flex-shrink-0">
|
|
149
|
+
{showDismiss && onDismiss && (
|
|
150
|
+
<button
|
|
151
|
+
onClick={onDismiss}
|
|
152
|
+
className="
|
|
153
|
+
flex items-center justify-center
|
|
154
|
+
w-8 h-8 rounded-full
|
|
155
|
+
text-ink-400 hover:text-ink-600
|
|
156
|
+
hover:bg-paper-100
|
|
157
|
+
transition-colors
|
|
158
|
+
"
|
|
159
|
+
aria-label="Dismiss"
|
|
160
|
+
>
|
|
161
|
+
<X className="h-4 w-4" />
|
|
162
|
+
</button>
|
|
163
|
+
)}
|
|
164
|
+
{leftContent}
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
{/* Center section */}
|
|
168
|
+
{centerContent && (
|
|
169
|
+
<div className="flex-1 flex items-center justify-center min-w-0">
|
|
170
|
+
{centerContent}
|
|
171
|
+
</div>
|
|
172
|
+
)}
|
|
173
|
+
|
|
174
|
+
{/* Right section */}
|
|
175
|
+
<div className="flex items-center gap-2 flex-shrink-0">
|
|
176
|
+
{rightContent}
|
|
177
|
+
{actions && actions.map((action) => (
|
|
178
|
+
<button
|
|
179
|
+
key={action.id}
|
|
180
|
+
onClick={action.onClick}
|
|
181
|
+
disabled={action.disabled || action.loading}
|
|
182
|
+
className={`
|
|
183
|
+
inline-flex items-center justify-center gap-2
|
|
184
|
+
px-3 py-1.5 text-sm font-medium rounded-lg border
|
|
185
|
+
transition-all duration-200
|
|
186
|
+
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-accent-400
|
|
187
|
+
disabled:opacity-40 disabled:cursor-not-allowed
|
|
188
|
+
${buttonVariantStyles[action.variant || 'secondary']}
|
|
189
|
+
`}
|
|
190
|
+
>
|
|
191
|
+
{action.loading ? (
|
|
192
|
+
<svg
|
|
193
|
+
className="h-4 w-4 animate-spin"
|
|
194
|
+
fill="none"
|
|
195
|
+
viewBox="0 0 24 24"
|
|
196
|
+
>
|
|
197
|
+
<circle
|
|
198
|
+
className="opacity-25"
|
|
199
|
+
cx="12"
|
|
200
|
+
cy="12"
|
|
201
|
+
r="10"
|
|
202
|
+
stroke="currentColor"
|
|
203
|
+
strokeWidth="4"
|
|
204
|
+
/>
|
|
205
|
+
<path
|
|
206
|
+
className="opacity-75"
|
|
207
|
+
fill="currentColor"
|
|
208
|
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
209
|
+
/>
|
|
210
|
+
</svg>
|
|
211
|
+
) : action.icon ? (
|
|
212
|
+
<span className="h-4 w-4">{action.icon}</span>
|
|
213
|
+
) : null}
|
|
214
|
+
<span>{action.label}</span>
|
|
215
|
+
</button>
|
|
216
|
+
))}
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* ActionBar.Left - Semantic wrapper for left content
|
|
225
|
+
*/
|
|
226
|
+
export function ActionBarLeft({ children }: { children: ReactNode }) {
|
|
227
|
+
return <>{children}</>;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* ActionBar.Center - Semantic wrapper for center content
|
|
232
|
+
*/
|
|
233
|
+
export function ActionBarCenter({ children }: { children: ReactNode }) {
|
|
234
|
+
return <>{children}</>;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* ActionBar.Right - Semantic wrapper for right content
|
|
239
|
+
*/
|
|
240
|
+
export function ActionBarRight({ children }: { children: ReactNode }) {
|
|
241
|
+
return <>{children}</>;
|
|
242
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { Home, Search, Bell, User, Plus, Settings } from 'lucide-react';
|
|
3
|
+
import BottomNavigation, { BottomNavigationSpacer } from './BottomNavigation';
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof BottomNavigation> = {
|
|
6
|
+
title: 'Mobile/BottomNavigation',
|
|
7
|
+
component: BottomNavigation,
|
|
8
|
+
parameters: {
|
|
9
|
+
layout: 'fullscreen',
|
|
10
|
+
viewport: {
|
|
11
|
+
defaultViewport: 'mobile1',
|
|
12
|
+
},
|
|
13
|
+
docs: {
|
|
14
|
+
description: {
|
|
15
|
+
component: 'iOS/Android-style fixed bottom navigation bar with icons, labels, and badges.',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
decorators: [
|
|
20
|
+
(Story) => (
|
|
21
|
+
<div style={{ minHeight: '100vh', paddingBottom: '80px', background: '#f5f5f4' }}>
|
|
22
|
+
<div style={{ padding: '16px' }}>
|
|
23
|
+
<h1 style={{ fontSize: '24px', fontWeight: 'bold', marginBottom: '16px' }}>Page Content</h1>
|
|
24
|
+
<p style={{ color: '#666' }}>Scroll down to see the bottom navigation stays fixed.</p>
|
|
25
|
+
{Array.from({ length: 20 }).map((_, i) => (
|
|
26
|
+
<div key={i} style={{ padding: '16px', margin: '8px 0', background: 'white', borderRadius: '8px' }}>
|
|
27
|
+
Item {i + 1}
|
|
28
|
+
</div>
|
|
29
|
+
))}
|
|
30
|
+
</div>
|
|
31
|
+
<Story />
|
|
32
|
+
</div>
|
|
33
|
+
),
|
|
34
|
+
],
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export default meta;
|
|
38
|
+
type Story = StoryObj<typeof BottomNavigation>;
|
|
39
|
+
|
|
40
|
+
const defaultItems = [
|
|
41
|
+
{ id: 'home', label: 'Home', icon: <Home className="w-6 h-6" />, href: '/' },
|
|
42
|
+
{ id: 'search', label: 'Search', icon: <Search className="w-6 h-6" />, href: '/search' },
|
|
43
|
+
{ id: 'notifications', label: 'Alerts', icon: <Bell className="w-6 h-6" />, badge: 3 },
|
|
44
|
+
{ id: 'profile', label: 'Profile', icon: <User className="w-6 h-6" />, href: '/profile' },
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
export const Default: Story = {
|
|
48
|
+
args: {
|
|
49
|
+
items: defaultItems,
|
|
50
|
+
activeId: 'home',
|
|
51
|
+
onNavigate: (id, href) => console.log('Navigate:', id, href),
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const WithBadges: Story = {
|
|
56
|
+
args: {
|
|
57
|
+
items: [
|
|
58
|
+
{ id: 'home', label: 'Home', icon: <Home className="w-6 h-6" /> },
|
|
59
|
+
{ id: 'search', label: 'Search', icon: <Search className="w-6 h-6" /> },
|
|
60
|
+
{ id: 'notifications', label: 'Alerts', icon: <Bell className="w-6 h-6" />, badge: 12 },
|
|
61
|
+
{ id: 'profile', label: 'Profile', icon: <User className="w-6 h-6" />, badge: 99 },
|
|
62
|
+
],
|
|
63
|
+
activeId: 'notifications',
|
|
64
|
+
onNavigate: (id) => console.log('Navigate:', id),
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const LargeBadge: Story = {
|
|
69
|
+
args: {
|
|
70
|
+
items: [
|
|
71
|
+
{ id: 'home', label: 'Home', icon: <Home className="w-6 h-6" /> },
|
|
72
|
+
{ id: 'notifications', label: 'Alerts', icon: <Bell className="w-6 h-6" />, badge: 150 },
|
|
73
|
+
{ id: 'profile', label: 'Profile', icon: <User className="w-6 h-6" /> },
|
|
74
|
+
],
|
|
75
|
+
activeId: 'home',
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const FiveItems: Story = {
|
|
80
|
+
args: {
|
|
81
|
+
items: [
|
|
82
|
+
{ id: 'home', label: 'Home', icon: <Home className="w-6 h-6" /> },
|
|
83
|
+
{ id: 'search', label: 'Search', icon: <Search className="w-6 h-6" /> },
|
|
84
|
+
{ id: 'add', label: 'Add', icon: <Plus className="w-6 h-6" /> },
|
|
85
|
+
{ id: 'notifications', label: 'Alerts', icon: <Bell className="w-6 h-6" />, badge: 5 },
|
|
86
|
+
{ id: 'settings', label: 'Settings', icon: <Settings className="w-6 h-6" /> },
|
|
87
|
+
],
|
|
88
|
+
activeId: 'home',
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const WithoutLabels: Story = {
|
|
93
|
+
args: {
|
|
94
|
+
items: defaultItems,
|
|
95
|
+
activeId: 'home',
|
|
96
|
+
showLabels: false,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const WithDisabledItem: Story = {
|
|
101
|
+
args: {
|
|
102
|
+
items: [
|
|
103
|
+
{ id: 'home', label: 'Home', icon: <Home className="w-6 h-6" /> },
|
|
104
|
+
{ id: 'search', label: 'Search', icon: <Search className="w-6 h-6" />, disabled: true },
|
|
105
|
+
{ id: 'notifications', label: 'Alerts', icon: <Bell className="w-6 h-6" /> },
|
|
106
|
+
{ id: 'profile', label: 'Profile', icon: <User className="w-6 h-6" /> },
|
|
107
|
+
],
|
|
108
|
+
activeId: 'home',
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export const WithClickHandlers: Story = {
|
|
113
|
+
args: {
|
|
114
|
+
items: [
|
|
115
|
+
{ id: 'home', label: 'Home', icon: <Home className="w-6 h-6" />, onClick: () => alert('Home clicked!') },
|
|
116
|
+
{ id: 'add', label: 'Add', icon: <Plus className="w-6 h-6" />, onClick: () => alert('Add clicked!') },
|
|
117
|
+
{ id: 'profile', label: 'Profile', icon: <User className="w-6 h-6" />, onClick: () => alert('Profile clicked!') },
|
|
118
|
+
],
|
|
119
|
+
activeId: 'home',
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export const SpacerExample: Story = {
|
|
124
|
+
render: () => (
|
|
125
|
+
<div style={{ minHeight: '100vh', background: '#f5f5f4' }}>
|
|
126
|
+
<div style={{ padding: '16px' }}>
|
|
127
|
+
<h1 style={{ fontSize: '24px', fontWeight: 'bold', marginBottom: '16px' }}>With Spacer</h1>
|
|
128
|
+
<p>The spacer prevents content from being hidden behind the navigation.</p>
|
|
129
|
+
{Array.from({ length: 10 }).map((_, i) => (
|
|
130
|
+
<div key={i} style={{ padding: '16px', margin: '8px 0', background: 'white', borderRadius: '8px' }}>
|
|
131
|
+
Content Item {i + 1}
|
|
132
|
+
</div>
|
|
133
|
+
))}
|
|
134
|
+
<BottomNavigationSpacer />
|
|
135
|
+
</div>
|
|
136
|
+
<BottomNavigation
|
|
137
|
+
items={defaultItems}
|
|
138
|
+
activeId="home"
|
|
139
|
+
/>
|
|
140
|
+
</div>
|
|
141
|
+
),
|
|
142
|
+
};
|