@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.
- package/README.md +3 -3
- package/dist/components/ActionBar.d.ts +112 -0
- package/dist/components/ActionBar.d.ts.map +1 -0
- package/dist/components/DataGrid.d.ts +182 -0
- package/dist/components/DataGrid.d.ts.map +1 -0
- package/dist/components/FormulaAutocomplete.d.ts +29 -0
- package/dist/components/FormulaAutocomplete.d.ts.map +1 -0
- package/dist/components/Modal.d.ts +29 -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/Select.d.ts +2 -0
- package/dist/components/Select.d.ts.map +1 -1
- package/dist/components/index.d.ts +8 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/index.d.ts +419 -3
- package/dist/index.esm.js +2533 -350
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +2543 -348
- package/dist/index.js.map +1 -1
- package/dist/styles.css +81 -0
- package/dist/utils/formulaDefinitions.d.ts +25 -0
- package/dist/utils/formulaDefinitions.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/components/ActionBar.stories.tsx +246 -0
- package/src/components/ActionBar.tsx +242 -0
- package/src/components/DataGrid.stories.tsx +356 -0
- package/src/components/DataGrid.tsx +1025 -0
- package/src/components/FormulaAutocomplete.tsx +417 -0
- package/src/components/Modal.stories.tsx +205 -0
- package/src/components/Modal.tsx +38 -1
- package/src/components/PageHeader.stories.tsx +198 -0
- package/src/components/PageHeader.tsx +217 -0
- package/src/components/Select.tsx +121 -7
- package/src/components/Sidebar.tsx +2 -2
- package/src/components/index.ts +36 -0
- 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
|
@@ -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
|
+
}
|