@papernote/ui 1.5.0 → 1.7.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.
Files changed (37) hide show
  1. package/README.md +3 -3
  2. package/dist/components/ActionBar.d.ts +112 -0
  3. package/dist/components/ActionBar.d.ts.map +1 -0
  4. package/dist/components/DataGrid.d.ts +182 -0
  5. package/dist/components/DataGrid.d.ts.map +1 -0
  6. package/dist/components/FormulaAutocomplete.d.ts +29 -0
  7. package/dist/components/FormulaAutocomplete.d.ts.map +1 -0
  8. package/dist/components/Modal.d.ts +29 -1
  9. package/dist/components/Modal.d.ts.map +1 -1
  10. package/dist/components/PageHeader.d.ts +86 -0
  11. package/dist/components/PageHeader.d.ts.map +1 -0
  12. package/dist/components/Select.d.ts +2 -0
  13. package/dist/components/Select.d.ts.map +1 -1
  14. package/dist/components/index.d.ts +8 -0
  15. package/dist/components/index.d.ts.map +1 -1
  16. package/dist/index.d.ts +419 -3
  17. package/dist/index.esm.js +2533 -350
  18. package/dist/index.esm.js.map +1 -1
  19. package/dist/index.js +2543 -348
  20. package/dist/index.js.map +1 -1
  21. package/dist/styles.css +81 -0
  22. package/dist/utils/formulaDefinitions.d.ts +25 -0
  23. package/dist/utils/formulaDefinitions.d.ts.map +1 -0
  24. package/package.json +1 -1
  25. package/src/components/ActionBar.stories.tsx +246 -0
  26. package/src/components/ActionBar.tsx +242 -0
  27. package/src/components/DataGrid.stories.tsx +356 -0
  28. package/src/components/DataGrid.tsx +1025 -0
  29. package/src/components/FormulaAutocomplete.tsx +417 -0
  30. package/src/components/Modal.stories.tsx +205 -0
  31. package/src/components/Modal.tsx +38 -1
  32. package/src/components/PageHeader.stories.tsx +198 -0
  33. package/src/components/PageHeader.tsx +217 -0
  34. package/src/components/Select.tsx +121 -7
  35. package/src/components/Sidebar.tsx +2 -2
  36. package/src/components/index.ts +36 -0
  37. package/src/utils/formulaDefinitions.ts +1228 -0
package/dist/styles.css CHANGED
@@ -1224,6 +1224,10 @@ input:checked + .slider:before{
1224
1224
  pointer-events: auto;
1225
1225
  }
1226
1226
 
1227
+ .\!visible{
1228
+ visibility: visible !important;
1229
+ }
1230
+
1227
1231
  .visible{
1228
1232
  visibility: visible;
1229
1233
  }
@@ -2118,6 +2122,10 @@ input:checked + .slider:before{
2118
2122
  min-width: 8rem;
2119
2123
  }
2120
2124
 
2125
+ .min-w-48{
2126
+ min-width: 12rem;
2127
+ }
2128
+
2121
2129
  .min-w-5{
2122
2130
  min-width: 1.25rem;
2123
2131
  }
@@ -2242,6 +2250,14 @@ input:checked + .slider:before{
2242
2250
  border-collapse: collapse;
2243
2251
  }
2244
2252
 
2253
+ .origin-bottom{
2254
+ transform-origin: bottom;
2255
+ }
2256
+
2257
+ .origin-top{
2258
+ transform-origin: top;
2259
+ }
2260
+
2245
2261
  .-translate-x-1\/2{
2246
2262
  --tw-translate-x: -50%;
2247
2263
  transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
@@ -3083,6 +3099,11 @@ input:checked + .slider:before{
3083
3099
  border-color: rgb(110 109 94 / var(--tw-border-opacity, 1));
3084
3100
  }
3085
3101
 
3102
+ .border-blue-200{
3103
+ --tw-border-opacity: 1;
3104
+ border-color: rgb(191 219 254 / var(--tw-border-opacity, 1));
3105
+ }
3106
+
3086
3107
  .border-blue-500{
3087
3108
  --tw-border-opacity: 1;
3088
3109
  border-color: rgb(59 130 246 / var(--tw-border-opacity, 1));
@@ -3189,11 +3210,21 @@ input:checked + .slider:before{
3189
3210
  border-color: rgb(226 232 240 / 0.8);
3190
3211
  }
3191
3212
 
3213
+ .border-stone-100{
3214
+ --tw-border-opacity: 1;
3215
+ border-color: rgb(245 245 244 / var(--tw-border-opacity, 1));
3216
+ }
3217
+
3192
3218
  .border-stone-200{
3193
3219
  --tw-border-opacity: 1;
3194
3220
  border-color: rgb(231 229 228 / var(--tw-border-opacity, 1));
3195
3221
  }
3196
3222
 
3223
+ .border-stone-50{
3224
+ --tw-border-opacity: 1;
3225
+ border-color: rgb(250 250 249 / var(--tw-border-opacity, 1));
3226
+ }
3227
+
3197
3228
  .border-success-200{
3198
3229
  --tw-border-opacity: 1;
3199
3230
  border-color: rgb(187 247 208 / var(--tw-border-opacity, 1));
@@ -3317,6 +3348,11 @@ input:checked + .slider:before{
3317
3348
  background-color: rgb(0 0 0 / 0.5);
3318
3349
  }
3319
3350
 
3351
+ .bg-blue-50{
3352
+ --tw-bg-opacity: 1;
3353
+ background-color: rgb(239 246 255 / var(--tw-bg-opacity, 1));
3354
+ }
3355
+
3320
3356
  .bg-current{
3321
3357
  background-color: currentColor;
3322
3358
  }
@@ -3466,6 +3502,16 @@ input:checked + .slider:before{
3466
3502
  background-color: rgb(248 250 252 / 0.8);
3467
3503
  }
3468
3504
 
3505
+ .bg-stone-100{
3506
+ --tw-bg-opacity: 1;
3507
+ background-color: rgb(245 245 244 / var(--tw-bg-opacity, 1));
3508
+ }
3509
+
3510
+ .bg-stone-50{
3511
+ --tw-bg-opacity: 1;
3512
+ background-color: rgb(250 250 249 / var(--tw-bg-opacity, 1));
3513
+ }
3514
+
3469
3515
  .bg-success-100{
3470
3516
  --tw-bg-opacity: 1;
3471
3517
  background-color: rgb(220 252 231 / var(--tw-bg-opacity, 1));
@@ -4456,6 +4502,10 @@ input:checked + .slider:before{
4456
4502
  box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
4457
4503
  }
4458
4504
 
4505
+ .ring-inset{
4506
+ --tw-ring-inset: inset;
4507
+ }
4508
+
4459
4509
  .ring-accent-300{
4460
4510
  --tw-ring-opacity: 1;
4461
4511
  --tw-ring-color: rgb(212 210 200 / var(--tw-ring-opacity, 1));
@@ -4491,6 +4541,11 @@ input:checked + .slider:before{
4491
4541
  --tw-ring-color: rgb(203 213 225 / var(--tw-ring-opacity, 1));
4492
4542
  }
4493
4543
 
4544
+ .ring-primary-500{
4545
+ --tw-ring-opacity: 1;
4546
+ --tw-ring-color: rgb(100 116 139 / var(--tw-ring-opacity, 1));
4547
+ }
4548
+
4494
4549
  .ring-success-300{
4495
4550
  --tw-ring-opacity: 1;
4496
4551
  --tw-ring-color: rgb(134 239 172 / var(--tw-ring-opacity, 1));
@@ -5105,6 +5160,16 @@ input:checked + .slider:before{
5105
5160
  background-color: rgb(254 242 242 / var(--tw-bg-opacity, 1));
5106
5161
  }
5107
5162
 
5163
+ .hover\:bg-stone-100:hover{
5164
+ --tw-bg-opacity: 1;
5165
+ background-color: rgb(245 245 244 / var(--tw-bg-opacity, 1));
5166
+ }
5167
+
5168
+ .hover\:bg-stone-200:hover{
5169
+ --tw-bg-opacity: 1;
5170
+ background-color: rgb(231 229 228 / var(--tw-bg-opacity, 1));
5171
+ }
5172
+
5108
5173
  .hover\:bg-success-200:hover{
5109
5174
  --tw-bg-opacity: 1;
5110
5175
  background-color: rgb(187 247 208 / var(--tw-bg-opacity, 1));
@@ -5509,6 +5574,10 @@ input:checked + .slider:before{
5509
5574
  display: inline;
5510
5575
  }
5511
5576
 
5577
+ .sm\:inline-flex{
5578
+ display: inline-flex;
5579
+ }
5580
+
5512
5581
  .sm\:hidden{
5513
5582
  display: none;
5514
5583
  }
@@ -5537,6 +5606,18 @@ input:checked + .slider:before{
5537
5606
  grid-template-columns: repeat(6, minmax(0, 1fr));
5538
5607
  }
5539
5608
 
5609
+ .sm\:flex-row{
5610
+ flex-direction: row;
5611
+ }
5612
+
5613
+ .sm\:items-center{
5614
+ align-items: center;
5615
+ }
5616
+
5617
+ .sm\:justify-between{
5618
+ justify-content: space-between;
5619
+ }
5620
+
5540
5621
  .sm\:pb-12{
5541
5622
  padding-bottom: 3rem;
5542
5623
  }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Formula definitions for DataGrid intellisense
3
+ * Based on fast-formula-parser supported functions
4
+ */
5
+ export interface FormulaParameter {
6
+ name: string;
7
+ description: string;
8
+ optional?: boolean;
9
+ }
10
+ export interface FormulaDefinition {
11
+ name: string;
12
+ category: FormulaCategory;
13
+ description: string;
14
+ syntax: string;
15
+ parameters: FormulaParameter[];
16
+ example?: string;
17
+ }
18
+ export type FormulaCategory = 'Math' | 'Statistical' | 'Lookup' | 'Text' | 'Logical' | 'Date' | 'Information' | 'Financial';
19
+ export declare const FORMULA_DEFINITIONS: FormulaDefinition[];
20
+ export declare const FORMULA_NAMES: string[];
21
+ export declare const getFormulasByCategory: (category: FormulaCategory) => FormulaDefinition[];
22
+ export declare const searchFormulas: (query: string) => FormulaDefinition[];
23
+ export declare const getFormula: (name: string) => FormulaDefinition | undefined;
24
+ export declare const FORMULA_CATEGORIES: FormulaCategory[];
25
+ //# sourceMappingURL=formulaDefinitions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"formulaDefinitions.d.ts","sourceRoot":"","sources":["../../src/utils/formulaDefinitions.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,eAAe,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,gBAAgB,EAAE,CAAC;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,MAAM,eAAe,GACvB,MAAM,GACN,aAAa,GACb,QAAQ,GACR,MAAM,GACN,SAAS,GACT,MAAM,GACN,aAAa,GACb,WAAW,CAAC;AAEhB,eAAO,MAAM,mBAAmB,EAAE,iBAAiB,EAgpClD,CAAC;AAGF,eAAO,MAAM,aAAa,UAAyC,CAAC;AAGpE,eAAO,MAAM,qBAAqB,GAAI,UAAU,eAAe,KAAG,iBAAiB,EACvB,CAAC;AAG7D,eAAO,MAAM,cAAc,GAAI,OAAO,MAAM,KAAG,iBAAiB,EAG/D,CAAC;AAGF,eAAO,MAAM,UAAU,GAAI,MAAM,MAAM,KAAG,iBAAiB,GAAG,SACE,CAAC;AAGjE,eAAO,MAAM,kBAAkB,EAAE,eAAe,EAS/C,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@papernote/ui",
3
- "version": "1.5.0",
3
+ "version": "1.7.0",
4
4
  "type": "module",
5
5
  "description": "A modern React component library with a paper notebook aesthetic - minimal, professional, and expressive",
6
6
  "main": "dist/index.js",
@@ -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
+ }