@fpkit/acss 6.2.0 → 6.4.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/libs/chunk-25KCUE3R.cjs +17 -0
- package/libs/chunk-25KCUE3R.cjs.map +1 -0
- package/libs/chunk-34NWHFHP.js +10 -0
- package/libs/chunk-34NWHFHP.js.map +1 -0
- package/libs/{chunk-SQ44OCJ2.js → chunk-6NMLU5FA.js} +2 -2
- package/libs/{chunk-GVVCXXKI.cjs → chunk-6YVR4TDM.cjs} +3 -3
- package/libs/chunk-DSQ2TUCR.js +7 -0
- package/libs/chunk-DSQ2TUCR.js.map +1 -0
- package/libs/{chunk-H6A2CUWA.js → chunk-VQTCTLFN.js} +2 -2
- package/libs/chunk-ZJ4RUKI2.cjs +14 -0
- package/libs/chunk-ZJ4RUKI2.cjs.map +1 -0
- package/libs/{chunk-H4JRUNKU.cjs → chunk-ZOPHCNFD.cjs} +3 -3
- package/libs/components/alert/alert.css +1 -1
- package/libs/components/alert/alert.css.map +1 -1
- package/libs/components/alert/alert.min.css +2 -2
- package/libs/components/button.cjs +3 -3
- package/libs/components/button.d.cts +34 -1
- package/libs/components/button.d.ts +34 -1
- package/libs/components/button.js +1 -1
- package/libs/components/buttons/button.css +1 -1
- package/libs/components/buttons/button.css.map +1 -1
- package/libs/components/buttons/button.min.css +2 -2
- package/libs/components/buttons/icon-button.css +1 -0
- package/libs/components/buttons/icon-button.css.map +1 -0
- package/libs/components/buttons/icon-button.min.css +3 -0
- package/libs/components/dialog/dialog.cjs +4 -4
- package/libs/components/dialog/dialog.css +1 -1
- package/libs/components/dialog/dialog.css.map +1 -1
- package/libs/components/dialog/dialog.js +2 -2
- package/libs/components/dialog/dialog.min.css +2 -2
- package/libs/components/link/link.css +1 -1
- package/libs/components/link/link.min.css +1 -1
- package/libs/components/modal.cjs +3 -3
- package/libs/components/modal.js +2 -2
- package/libs/components/popover/popover.cjs +3 -8
- package/libs/components/popover/popover.css +1 -0
- package/libs/components/popover/popover.css.map +1 -0
- package/libs/components/popover/popover.d.cts +54 -26
- package/libs/components/popover/popover.d.ts +54 -26
- package/libs/components/popover/popover.js +1 -2
- package/libs/components/popover/popover.min.css +3 -0
- package/libs/hooks.cjs +3 -6
- package/libs/hooks.cjs.map +1 -1
- package/libs/hooks.d.cts +30 -10
- package/libs/hooks.d.ts +30 -10
- package/libs/hooks.js +5 -1
- package/libs/hooks.js.map +1 -1
- package/libs/index.cjs +35 -35
- package/libs/index.cjs.map +1 -1
- package/libs/index.css +1 -1
- package/libs/index.css.map +1 -1
- package/libs/index.d.cts +65 -3
- package/libs/index.d.ts +65 -3
- package/libs/index.js +9 -10
- package/libs/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/alert/alert.scss +0 -13
- package/src/components/buttons/README.mdx +107 -11
- package/src/components/buttons/STYLES.mdx +182 -47
- package/src/components/buttons/button.scss +93 -16
- package/src/components/buttons/button.stories.tsx +149 -0
- package/src/components/buttons/button.test.tsx +12 -0
- package/src/components/buttons/button.tsx +50 -6
- package/src/components/buttons/icon-button.mdx +204 -0
- package/src/components/buttons/icon-button.scss +83 -0
- package/src/components/buttons/icon-button.stories.tsx +200 -0
- package/src/components/buttons/icon-button.test.tsx +132 -0
- package/src/components/buttons/icon-button.tsx +75 -0
- package/src/components/dialog/dialog-modal.stories.tsx +71 -0
- package/src/components/dialog/dialog-modal.tsx +29 -3
- package/src/components/dialog/dialog.scss +1 -0
- package/src/components/dialog/dialog.test.tsx +119 -0
- package/src/components/dialog/dialog.types.ts +8 -1
- package/src/components/form/select.tsx +55 -51
- package/src/components/link/link.scss +2 -2
- package/src/components/popover/README.mdx +478 -0
- package/src/components/popover/STYLES.mdx +389 -0
- package/src/components/popover/index.ts +3 -0
- package/src/components/popover/popover.scss +249 -0
- package/src/components/popover/popover.stories.tsx +315 -15
- package/src/components/popover/popover.test.tsx +249 -37
- package/src/components/popover/popover.tsx +165 -62
- package/src/hooks/popover/popover.tsx +26 -10
- package/src/hooks/popover/use-popover.tsx +30 -10
- package/src/hooks.ts +5 -0
- package/src/index.scss +1 -0
- package/src/index.ts +1 -0
- package/src/sass/utilities/_display.scss +156 -0
- package/src/sass/utilities/_index.scss +3 -0
- package/src/sass/utilities/display.mdx +203 -0
- package/src/sass/utilities/display.stories.tsx +141 -0
- package/src/styles/alert/alert.css +0 -13
- package/src/styles/alert/alert.css.map +1 -1
- package/src/styles/buttons/button.css +78 -16
- package/src/styles/buttons/button.css.map +1 -1
- package/src/styles/buttons/icon-button.css +71 -0
- package/src/styles/buttons/icon-button.css.map +1 -0
- package/src/styles/dialog/dialog.css +1 -0
- package/src/styles/dialog/dialog.css.map +1 -1
- package/src/styles/index.css +404 -31
- package/src/styles/index.css.map +1 -1
- package/src/styles/link/link.css +2 -2
- package/src/styles/popover/popover.css +190 -0
- package/src/styles/popover/popover.css.map +1 -0
- package/src/types/popover.d.ts +64 -0
- package/libs/chunk-4I5MF54P.js +0 -8
- package/libs/chunk-4I5MF54P.js.map +0 -1
- package/libs/chunk-GCGKYLDG.js +0 -7
- package/libs/chunk-GCGKYLDG.js.map +0 -1
- package/libs/chunk-NZVSXRTB.cjs +0 -16
- package/libs/chunk-NZVSXRTB.cjs.map +0 -1
- package/libs/chunk-PDD4N5P5.cjs +0 -10
- package/libs/chunk-PDD4N5P5.cjs.map +0 -1
- package/libs/chunk-S7NIA6PI.cjs +0 -17
- package/libs/chunk-S7NIA6PI.cjs.map +0 -1
- package/libs/chunk-X2RDXWH5.js +0 -10
- package/libs/chunk-X2RDXWH5.js.map +0 -1
- /package/libs/{chunk-SQ44OCJ2.js.map → chunk-6NMLU5FA.js.map} +0 -0
- /package/libs/{chunk-GVVCXXKI.cjs.map → chunk-6YVR4TDM.cjs.map} +0 -0
- /package/libs/{chunk-H6A2CUWA.js.map → chunk-VQTCTLFN.js.map} +0 -0
- /package/libs/{chunk-H4JRUNKU.cjs.map → chunk-ZOPHCNFD.cjs.map} +0 -0
|
@@ -4,10 +4,14 @@ button {
|
|
|
4
4
|
--btn-size-sm: 0.8125rem;
|
|
5
5
|
--btn-size-md: 0.9375rem;
|
|
6
6
|
--btn-size-lg: 1.125rem;
|
|
7
|
-
--btn-
|
|
7
|
+
--btn-size-xl: 1.375rem;
|
|
8
|
+
--btn-size-2xl: 1.75rem;
|
|
9
|
+
--btn-pill: 100vw;
|
|
8
10
|
--btn-fs: var(--btn-size-md);
|
|
9
|
-
|
|
10
|
-
--btn-
|
|
11
|
+
|
|
12
|
+
--btn-height: calc(var(--btn-fs) * 2.75);
|
|
13
|
+
--btn-bg: var(--color-primary);
|
|
14
|
+
--btn-color: var(--color-text-inverse);
|
|
11
15
|
--btn-width: max-content;
|
|
12
16
|
|
|
13
17
|
font-size: var(--btn-fs);
|
|
@@ -36,7 +40,7 @@ button {
|
|
|
36
40
|
line-height: 0cap;
|
|
37
41
|
|
|
38
42
|
&[type] {
|
|
39
|
-
background-color: var(--btn-bg, var(--color-
|
|
43
|
+
background-color: var(--btn-bg, var(--color-primary));
|
|
40
44
|
--btn-border: solid var(--btn-sg);
|
|
41
45
|
}
|
|
42
46
|
|
|
@@ -93,8 +97,77 @@ button {
|
|
|
93
97
|
|
|
94
98
|
&[data-fp-btn~="pill"],
|
|
95
99
|
&[data-btn~="pill"],
|
|
96
|
-
&[data-style~="pill"]
|
|
97
|
-
|
|
100
|
+
&[data-style~="pill"],
|
|
101
|
+
&.btn-pill {
|
|
102
|
+
border-radius: var(--btn-pill, 100vw);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Color variants — map to semantic tokens from index.css
|
|
106
|
+
&[data-color="primary"] {
|
|
107
|
+
--btn-bg: var(--color-primary);
|
|
108
|
+
--btn-color: var(--color-text-inverse);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
&[data-color="secondary"] {
|
|
112
|
+
--btn-bg: var(--color-secondary);
|
|
113
|
+
--btn-color: var(--color-text-inverse);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
&[data-color="danger"] {
|
|
117
|
+
--btn-bg: var(--color-error);
|
|
118
|
+
--btn-color: var(--color-text-inverse);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
&[data-color="success"] {
|
|
122
|
+
--btn-bg: var(--color-success);
|
|
123
|
+
--btn-color: var(--color-text-inverse);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
&[data-color="warning"] {
|
|
127
|
+
--btn-bg: var(--color-warning);
|
|
128
|
+
--btn-color: var(--color-text-inverse);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Style variants via data-style (variant prop) — mirrors link button patterns
|
|
132
|
+
&[data-style~="outline"] {
|
|
133
|
+
--btn-bg: transparent;
|
|
134
|
+
--btn-color: currentColor;
|
|
135
|
+
--btn-border: 0.125rem solid currentColor;
|
|
136
|
+
|
|
137
|
+
&:is(:hover, :focus) {
|
|
138
|
+
background-color: color-mix(in srgb, currentColor 10%, transparent);
|
|
139
|
+
filter: none;
|
|
140
|
+
outline: 0.025rem solid currentColor;
|
|
141
|
+
outline-offset: 0;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
&[data-style~="text"] {
|
|
146
|
+
--btn-bg: transparent;
|
|
147
|
+
--btn-color: currentColor;
|
|
148
|
+
--btn-border: none;
|
|
149
|
+
--btn-height: unset;
|
|
150
|
+
--btn-width: unset;
|
|
151
|
+
--btn-padding-block: 0.75rem;
|
|
152
|
+
--btn-padding-inline: 0.75rem;
|
|
153
|
+
&:is(:hover, :focus) {
|
|
154
|
+
background-color: color-mix(in srgb, var(--btn-color) 10%, transparent);
|
|
155
|
+
outline: 0.025rem solid var(--btn-color);
|
|
156
|
+
outline-offset: 0;
|
|
157
|
+
filter: none;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
&[data-style~="icon"] {
|
|
162
|
+
padding: unset;
|
|
163
|
+
height: unset;
|
|
164
|
+
--btn-bg: transparent;
|
|
165
|
+
min-width: 1.5rem;
|
|
166
|
+
min-height: 1.5rem;
|
|
167
|
+
text-align: center;
|
|
168
|
+
display: inline-flex;
|
|
169
|
+
align-items: center;
|
|
170
|
+
justify-content: center;
|
|
98
171
|
}
|
|
99
172
|
|
|
100
173
|
&[data-btn~="xs"],
|
|
@@ -118,16 +191,20 @@ button {
|
|
|
118
191
|
--btn-fs: var(--btn-size-lg);
|
|
119
192
|
}
|
|
120
193
|
|
|
121
|
-
&[data-btn~="
|
|
122
|
-
.btn-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
194
|
+
&[data-btn~="xl"],
|
|
195
|
+
.btn-xl {
|
|
196
|
+
--btn-fs: var(--btn-size-xl);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
&[data-btn~="2xl"],
|
|
200
|
+
.btn-2xl {
|
|
201
|
+
--btn-fs: var(--btn-size-2xl);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
&[data-btn~="block"],
|
|
205
|
+
.btn-block {
|
|
206
|
+
--btn-width: 100%;
|
|
207
|
+
display: flex;
|
|
131
208
|
justify-content: center;
|
|
132
209
|
}
|
|
133
210
|
|
|
@@ -14,6 +14,27 @@ const meta = {
|
|
|
14
14
|
children: "Click me",
|
|
15
15
|
onClick: buttonClicked,
|
|
16
16
|
},
|
|
17
|
+
argTypes: {
|
|
18
|
+
size: {
|
|
19
|
+
control: "select",
|
|
20
|
+
options: ["xs", "sm", "md", "lg", "xl", "2xl"],
|
|
21
|
+
description: "Size token — maps to data-btn attribute",
|
|
22
|
+
},
|
|
23
|
+
block: {
|
|
24
|
+
control: "boolean",
|
|
25
|
+
description: "Stretch button to 100% container width — composes with size and variant",
|
|
26
|
+
},
|
|
27
|
+
variant: {
|
|
28
|
+
control: "select",
|
|
29
|
+
options: ["text", "pill", "icon", "outline"],
|
|
30
|
+
description: "Style variant — maps to data-style attribute",
|
|
31
|
+
},
|
|
32
|
+
color: {
|
|
33
|
+
control: "select",
|
|
34
|
+
options: ["primary", "secondary", "danger", "success", "warning"],
|
|
35
|
+
description: "Color variant using semantic design tokens — maps to data-color attribute",
|
|
36
|
+
},
|
|
37
|
+
},
|
|
17
38
|
parameters: {},
|
|
18
39
|
} as Meta;
|
|
19
40
|
|
|
@@ -109,6 +130,132 @@ export const Large: Story = {
|
|
|
109
130
|
},
|
|
110
131
|
} as Story;
|
|
111
132
|
|
|
133
|
+
// --- Size prop stories (typed API instead of raw data-btn) ---
|
|
134
|
+
|
|
135
|
+
export const SizeXS: Story = {
|
|
136
|
+
args: { size: "xs", children: "Extra Small" },
|
|
137
|
+
} as Story;
|
|
138
|
+
|
|
139
|
+
export const SizeSM: Story = {
|
|
140
|
+
args: { size: "sm", children: "Small" },
|
|
141
|
+
} as Story;
|
|
142
|
+
|
|
143
|
+
export const SizeLG: Story = {
|
|
144
|
+
args: { size: "lg", children: "Large" },
|
|
145
|
+
} as Story;
|
|
146
|
+
|
|
147
|
+
export const SizeXL: Story = {
|
|
148
|
+
args: { size: "xl", children: "Extra Large" },
|
|
149
|
+
} as Story;
|
|
150
|
+
|
|
151
|
+
export const Size2XL: Story = {
|
|
152
|
+
args: { size: "2xl", children: "2X Large" },
|
|
153
|
+
} as Story;
|
|
154
|
+
|
|
155
|
+
// --- Block stories ---
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Block button — stretches to 100% container width at the default size.
|
|
159
|
+
*/
|
|
160
|
+
export const Block: Story = {
|
|
161
|
+
args: { block: true, children: "Block Button" },
|
|
162
|
+
} as Story;
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Block button composed with size and color variants.
|
|
166
|
+
*/
|
|
167
|
+
export const BlockVariants: Story = {
|
|
168
|
+
render: () => (
|
|
169
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "1rem", maxWidth: "32rem" }}>
|
|
170
|
+
<Button type="button" block size="sm">Block Small</Button>
|
|
171
|
+
<Button type="button" block>Block Default</Button>
|
|
172
|
+
<Button type="button" block size="lg">Block Large</Button>
|
|
173
|
+
<Button type="button" block size="xl" color="primary">Block XL Primary</Button>
|
|
174
|
+
<Button type="button" block color="danger" variant="outline">Block Danger Outline</Button>
|
|
175
|
+
</div>
|
|
176
|
+
),
|
|
177
|
+
} as Story;
|
|
178
|
+
|
|
179
|
+
// --- Variant stories ---
|
|
180
|
+
|
|
181
|
+
export const Outline: Story = {
|
|
182
|
+
args: { variant: "outline", children: "Outline" },
|
|
183
|
+
} as Story;
|
|
184
|
+
|
|
185
|
+
export const Pill: Story = {
|
|
186
|
+
args: { variant: "pill", children: "Pill" },
|
|
187
|
+
} as Story;
|
|
188
|
+
|
|
189
|
+
export const TextVariant: Story = {
|
|
190
|
+
args: { variant: "text", children: "Text Button" },
|
|
191
|
+
} as Story;
|
|
192
|
+
|
|
193
|
+
// --- Color stories ---
|
|
194
|
+
|
|
195
|
+
export const Primary: Story = {
|
|
196
|
+
args: { color: "primary", children: "Primary" },
|
|
197
|
+
} as Story;
|
|
198
|
+
|
|
199
|
+
export const Secondary: Story = {
|
|
200
|
+
args: { color: "secondary", children: "Secondary" },
|
|
201
|
+
} as Story;
|
|
202
|
+
|
|
203
|
+
export const Danger: Story = {
|
|
204
|
+
args: { color: "danger", children: "Danger" },
|
|
205
|
+
} as Story;
|
|
206
|
+
|
|
207
|
+
export const Success: Story = {
|
|
208
|
+
args: { color: "success", children: "Success" },
|
|
209
|
+
} as Story;
|
|
210
|
+
|
|
211
|
+
export const Warning: Story = {
|
|
212
|
+
args: { color: "warning", children: "Warning" },
|
|
213
|
+
} as Story;
|
|
214
|
+
|
|
215
|
+
// --- Combination stories ---
|
|
216
|
+
|
|
217
|
+
export const PrimaryOutline: Story = {
|
|
218
|
+
args: { color: "primary", variant: "outline", children: "Primary Outline" },
|
|
219
|
+
} as Story;
|
|
220
|
+
|
|
221
|
+
export const DangerPill: Story = {
|
|
222
|
+
args: { color: "danger", variant: "pill", children: "Danger Pill" },
|
|
223
|
+
} as Story;
|
|
224
|
+
|
|
225
|
+
export const SuccessOutline: Story = {
|
|
226
|
+
args: { color: "success", variant: "outline", children: "Success Outline" },
|
|
227
|
+
} as Story;
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* All color variants side by side.
|
|
231
|
+
*/
|
|
232
|
+
export const AllColors: Story = {
|
|
233
|
+
render: () => (
|
|
234
|
+
<div style={{ display: "flex", gap: "1rem", flexWrap: "wrap" }}>
|
|
235
|
+
<Button type="button" color="primary">Primary</Button>
|
|
236
|
+
<Button type="button" color="secondary">Secondary</Button>
|
|
237
|
+
<Button type="button" color="danger">Danger</Button>
|
|
238
|
+
<Button type="button" color="success">Success</Button>
|
|
239
|
+
<Button type="button" color="warning">Warning</Button>
|
|
240
|
+
</div>
|
|
241
|
+
),
|
|
242
|
+
} as Story;
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* All variant styles side by side.
|
|
246
|
+
*/
|
|
247
|
+
export const AllVariants: Story = {
|
|
248
|
+
render: () => (
|
|
249
|
+
<div style={{ display: "flex", gap: "1rem", flexWrap: "wrap", alignItems: "center" }}>
|
|
250
|
+
<Button type="button" variant="outline">Outline</Button>
|
|
251
|
+
<Button type="button" variant="pill">Pill</Button>
|
|
252
|
+
<Button type="button" variant="text">Text</Button>
|
|
253
|
+
<Button type="button" color="primary" variant="outline">Primary Outline</Button>
|
|
254
|
+
<Button type="button" color="danger" variant="pill">Danger Pill</Button>
|
|
255
|
+
</div>
|
|
256
|
+
),
|
|
257
|
+
} as Story;
|
|
258
|
+
|
|
112
259
|
export const Custom: Story = {
|
|
113
260
|
args: {
|
|
114
261
|
styles: {
|
|
@@ -377,6 +524,8 @@ export const Customization: Story = {
|
|
|
377
524
|
- \`--btn-size-sm\`: 0.8125rem (13px)
|
|
378
525
|
- \`--btn-size-md\`: 0.9375rem (15px)
|
|
379
526
|
- \`--btn-size-lg\`: 1.125rem (18px)
|
|
527
|
+
- \`--btn-size-xl\`: 1.375rem (22px)
|
|
528
|
+
- \`--btn-size-2xl\`: 1.75rem (28px)
|
|
380
529
|
|
|
381
530
|
### Base Properties
|
|
382
531
|
- \`--btn-padding-inline\`: Horizontal padding (logical property)
|
|
@@ -79,6 +79,18 @@ describe("Button", () => {
|
|
|
79
79
|
expect(handlePointerEvents).toHaveBeenCalledTimes(1);
|
|
80
80
|
});
|
|
81
81
|
|
|
82
|
+
it("applies xl size via data-btn attribute", () => {
|
|
83
|
+
render(<Button type="button" size="xl">XL</Button>);
|
|
84
|
+
const button = screen.getByRole("button");
|
|
85
|
+
expect(button).toHaveAttribute("data-btn", "xl");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("applies 2xl size via data-btn attribute", () => {
|
|
89
|
+
render(<Button type="button" size="2xl">2XL</Button>);
|
|
90
|
+
const button = screen.getByRole("button");
|
|
91
|
+
expect(button).toHaveAttribute("data-btn", "2xl");
|
|
92
|
+
});
|
|
93
|
+
|
|
82
94
|
it("it is disabled when disabled is true", () => {
|
|
83
95
|
const handleClick = jest.fn();
|
|
84
96
|
render(
|
|
@@ -11,6 +11,39 @@ export type ButtonProps = Partial<React.ComponentProps<typeof UI>> &
|
|
|
11
11
|
* Required - 'button' | 'submit' | 'reset'
|
|
12
12
|
*/
|
|
13
13
|
type: "button" | "submit" | "reset";
|
|
14
|
+
/**
|
|
15
|
+
* Raw data-btn tokens. Merged with `size` and `block` — all three contribute
|
|
16
|
+
* whitespace-separated words to the final `data-btn` attribute value.
|
|
17
|
+
* @example <Button data-btn="pill">Pill button</Button>
|
|
18
|
+
*/
|
|
19
|
+
"data-btn"?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Size variant - maps to `data-btn` attribute, aligns with SCSS size tokens.
|
|
22
|
+
* Can coexist with a directly passed `data-btn` attribute (values are merged).
|
|
23
|
+
* @example <Button size="sm">Small</Button>
|
|
24
|
+
*/
|
|
25
|
+
size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
|
|
26
|
+
/**
|
|
27
|
+
* Style variant - maps to `data-style` attribute.
|
|
28
|
+
* - `"outline"` — transparent bg with border (mirrors link button style)
|
|
29
|
+
* - `"pill"` — fully rounded corners
|
|
30
|
+
* - `"text"` — ghost text button with subtle hover
|
|
31
|
+
* - `"icon"` — square icon-only, no padding
|
|
32
|
+
* @example <Button variant="outline">Bordered</Button>
|
|
33
|
+
*/
|
|
34
|
+
variant?: "text" | "pill" | "icon" | "outline";
|
|
35
|
+
/**
|
|
36
|
+
* Color variant - maps to `data-color` attribute using semantic color tokens.
|
|
37
|
+
* @example <Button color="danger">Delete</Button>
|
|
38
|
+
*/
|
|
39
|
+
color?: "primary" | "secondary" | "danger" | "success" | "warning";
|
|
40
|
+
/**
|
|
41
|
+
* Block layout — stretches the button to 100% of its container width.
|
|
42
|
+
* Composes with `size` and other `data-btn` values.
|
|
43
|
+
* @example <Button block>Full Width</Button>
|
|
44
|
+
* @example <Button size="lg" block>Large Full Width</Button>
|
|
45
|
+
*/
|
|
46
|
+
block?: boolean;
|
|
14
47
|
};
|
|
15
48
|
|
|
16
49
|
/**
|
|
@@ -73,6 +106,10 @@ export const Button = ({
|
|
|
73
106
|
disabled,
|
|
74
107
|
isDisabled,
|
|
75
108
|
classes,
|
|
109
|
+
size,
|
|
110
|
+
variant,
|
|
111
|
+
color,
|
|
112
|
+
block,
|
|
76
113
|
onPointerDown,
|
|
77
114
|
onPointerOver,
|
|
78
115
|
onPointerLeave,
|
|
@@ -96,30 +133,37 @@ export const Button = ({
|
|
|
96
133
|
className: classes,
|
|
97
134
|
// Note: onPointerOver and onPointerLeave are intentionally NOT wrapped
|
|
98
135
|
// to allow hover effects on disabled buttons for visual feedback
|
|
99
|
-
}
|
|
136
|
+
},
|
|
100
137
|
);
|
|
101
138
|
|
|
139
|
+
// Merge size, block, and any explicit data-btn passed by the consumer.
|
|
140
|
+
// SCSS uses [data-btn~="value"] (whitespace word match), so "lg block" targets both.
|
|
141
|
+
const { "data-btn": dataBtnProp, ...restProps } = props;
|
|
142
|
+
const dataBtnValue =
|
|
143
|
+
[size, block ? "block" : undefined, dataBtnProp]
|
|
144
|
+
.filter(Boolean)
|
|
145
|
+
.join(" ") || undefined;
|
|
146
|
+
|
|
102
147
|
/* Returning a button element with accessible disabled state */
|
|
103
148
|
return (
|
|
104
149
|
<UI
|
|
105
150
|
as="button"
|
|
106
151
|
type={type}
|
|
152
|
+
data-btn={dataBtnValue}
|
|
153
|
+
data-style={variant}
|
|
154
|
+
data-color={color}
|
|
107
155
|
aria-disabled={disabledProps["aria-disabled"]}
|
|
108
156
|
onPointerOver={onPointerOver}
|
|
109
157
|
onPointerLeave={onPointerLeave}
|
|
110
158
|
style={styles}
|
|
111
159
|
className={disabledProps.className}
|
|
160
|
+
{...restProps}
|
|
112
161
|
{...handlers}
|
|
113
|
-
{...props}
|
|
114
162
|
>
|
|
115
163
|
{children}
|
|
116
164
|
</UI>
|
|
117
165
|
);
|
|
118
166
|
};
|
|
119
167
|
|
|
120
|
-
export const IconButton = ({ icon, ...props }: ButtonProps) => {
|
|
121
|
-
return <Button {...props}>{icon}</Button>;
|
|
122
|
-
};
|
|
123
|
-
|
|
124
168
|
export default Button;
|
|
125
169
|
Button.displayName = "Button";
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { Meta } from "@storybook/addon-docs/blocks";
|
|
2
|
+
|
|
3
|
+
<Meta title="FP.React Components/Buttons/IconButton/Readme" />
|
|
4
|
+
|
|
5
|
+
# IconButton
|
|
6
|
+
|
|
7
|
+
## Summary
|
|
8
|
+
|
|
9
|
+
`IconButton` is an accessible icon-button component that wraps the base `Button`
|
|
10
|
+
with enforced accessible labelling and icon-specific defaults.
|
|
11
|
+
|
|
12
|
+
It handles the two most common icon-button patterns:
|
|
13
|
+
|
|
14
|
+
- **Icon-only** — a square tap target with a screen-reader label
|
|
15
|
+
- **Icon + label** — icon with visible text that hides on narrow viewports
|
|
16
|
+
|
|
17
|
+
`IconButton` extends all `Button` props (`size`, `variant`, `color`, `disabled`,
|
|
18
|
+
etc.) and enforces one critical constraint at the TypeScript level: exactly one
|
|
19
|
+
of `aria-label` or `aria-labelledby` must be present — passing both or neither
|
|
20
|
+
is a compile-time error.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Props
|
|
25
|
+
|
|
26
|
+
| Prop | Type | Default | Description |
|
|
27
|
+
| ----------------- | -------------------------------------------------------------------- | ---------- | -------------------------------------------------------------------------------------------- |
|
|
28
|
+
| `icon` | `React.ReactNode` | — | **Required.** The icon element rendered inside the button. |
|
|
29
|
+
| `aria-label` | `string` | — | Accessible name for icon-only buttons. XOR with `aria-labelledby`. |
|
|
30
|
+
| `aria-labelledby` | `string` | — | References an external element's `id` as the accessible name. XOR with `aria-label`. |
|
|
31
|
+
| `label` | `string` | — | Optional label text alongside the icon. Hidden below `48rem` (768px); always in the accessibility tree.|
|
|
32
|
+
| `type` | `'button' \| 'submit' \| 'reset'` | `'button'` | Button type attribute. Required. |
|
|
33
|
+
| `variant` | `'icon' \| 'outline' \| 'text' \| 'pill'` | `'icon'` | Style variant. Default `'icon'` gives a transparent, square, no-padding button. |
|
|
34
|
+
| `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| '2xl'` | — | Size token. Scales font size and height via `--btn-fs`. |
|
|
35
|
+
| `color` | `'primary' \| 'secondary' \| 'danger' \| 'success' \| 'warning'` | — | Semantic color token. Sets `--btn-bg` and `--btn-color` via `data-color`. |
|
|
36
|
+
| `disabled` | `boolean` | `false` | Disables the button using WCAG-compliant `aria-disabled` (stays keyboard-focusable). |
|
|
37
|
+
| `onClick` | `(e: React.MouseEvent<HTMLButtonElement>) => void` | — | Click handler. |
|
|
38
|
+
| `styles` | `React.CSSProperties` | — | Inline styles — use to override CSS custom properties e.g. `--btn-color: red`. |
|
|
39
|
+
| `classes` | `string` | — | Additional CSS classes appended to the button element. |
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Sizing — 3rem Fixed Tap Target
|
|
44
|
+
|
|
45
|
+
The default `variant="icon"` applies a fixed `width: 3rem; height: 3rem` to
|
|
46
|
+
every `IconButton` instance:
|
|
47
|
+
|
|
48
|
+
```css
|
|
49
|
+
button[data-icon-btn] {
|
|
50
|
+
width: 3rem; /* 48px at default font size */
|
|
51
|
+
height: 3rem; /* 48px at default font size */
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`3rem` equals **48px** at the browser's default 16px root font size, meeting
|
|
56
|
+
the WCAG 2.5.5 (AAA) minimum target size recommendation of 44×44 CSS pixels.
|
|
57
|
+
The fixed size ensures a consistent, touchable target regardless of icon size
|
|
58
|
+
or parent context.
|
|
59
|
+
|
|
60
|
+
When a `label` is present (the `has-label` variant), width becomes `max-content`
|
|
61
|
+
and padding is restored to `0.75rem` inline, but the `3rem` height is preserved
|
|
62
|
+
for a consistent touch target.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Label Visibility
|
|
67
|
+
|
|
68
|
+
When `label` is provided, the text is rendered in a `<span data-icon-label>`.
|
|
69
|
+
Visibility is controlled entirely by CSS — no prop required:
|
|
70
|
+
|
|
71
|
+
- **Below `48rem` (768px):** the span is visually hidden via the visually-hidden
|
|
72
|
+
technique applied by the `[data-icon-label]` media query in `icon-button.scss`.
|
|
73
|
+
The span remains in the DOM and the accessibility tree — screen readers
|
|
74
|
+
announce the label at every viewport size.
|
|
75
|
+
- **At `48rem` and above:** the label is fully visible alongside the icon.
|
|
76
|
+
|
|
77
|
+
The breakpoint is a SCSS variable (not a CSS custom property) because media
|
|
78
|
+
query conditions are evaluated at parse time and cannot reference runtime values:
|
|
79
|
+
|
|
80
|
+
```scss
|
|
81
|
+
$icon-label-bp: 48rem !default; // Override before import to customise
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Usage Examples
|
|
87
|
+
|
|
88
|
+
### Icon-only button
|
|
89
|
+
|
|
90
|
+
Requires `aria-label`. The label is only announced by screen readers — not
|
|
91
|
+
visible on screen.
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
import { IconButton } from "@fpkit/acss";
|
|
95
|
+
|
|
96
|
+
<IconButton
|
|
97
|
+
type="button"
|
|
98
|
+
aria-label="Close menu"
|
|
99
|
+
icon={<CloseIcon />}
|
|
100
|
+
/>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Icon + visible label
|
|
104
|
+
|
|
105
|
+
Use `variant="outline"` (or any non-`icon` variant) to restore padding alongside
|
|
106
|
+
the label. The default `variant="icon"` sets `padding: 0`, which collapses the
|
|
107
|
+
layout around a label. The label hides automatically on mobile (< 48rem) via CSS.
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
<IconButton
|
|
111
|
+
type="button"
|
|
112
|
+
aria-label="Settings"
|
|
113
|
+
icon={<SettingsIcon />}
|
|
114
|
+
label="Settings"
|
|
115
|
+
variant="outline"
|
|
116
|
+
/>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Labelled by external element
|
|
120
|
+
|
|
121
|
+
Use `aria-labelledby` to reference an existing label element in the DOM. Useful
|
|
122
|
+
when the button sits next to a heading or description that already names the action.
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
<span id="delete-label">Delete item</span>
|
|
126
|
+
<IconButton
|
|
127
|
+
type="button"
|
|
128
|
+
aria-labelledby="delete-label"
|
|
129
|
+
icon={<TrashIcon />}
|
|
130
|
+
/>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Semantic color variants
|
|
134
|
+
|
|
135
|
+
Color tokens apply to the icon via `currentColor` — the icon's `stroke` or
|
|
136
|
+
`fill` inherits the button's `--btn-color`.
|
|
137
|
+
|
|
138
|
+
```tsx
|
|
139
|
+
<IconButton type="button" aria-label="Delete" icon={<TrashIcon />} color="danger" />
|
|
140
|
+
<IconButton type="button" aria-label="Confirm" icon={<CheckIcon />} color="success" />
|
|
141
|
+
<IconButton type="button" aria-label="Settings" icon={<GearIcon />} color="primary" variant="outline" />
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Disabled state
|
|
145
|
+
|
|
146
|
+
Uses the WCAG-compliant `aria-disabled` pattern — the button remains focusable
|
|
147
|
+
and visible in the tab order so keyboard users can discover it and read any
|
|
148
|
+
associated tooltip or help text.
|
|
149
|
+
|
|
150
|
+
```tsx
|
|
151
|
+
<IconButton
|
|
152
|
+
type="button"
|
|
153
|
+
aria-label="Upload (unavailable)"
|
|
154
|
+
icon={<UploadIcon />}
|
|
155
|
+
disabled
|
|
156
|
+
/>
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Custom color via CSS custom property
|
|
160
|
+
|
|
161
|
+
```tsx
|
|
162
|
+
<IconButton
|
|
163
|
+
type="button"
|
|
164
|
+
aria-label="Star"
|
|
165
|
+
icon={<StarIcon />}
|
|
166
|
+
styles={{ "--btn-color": "#f59e0b" }}
|
|
167
|
+
/>
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Accessibility Notes
|
|
173
|
+
|
|
174
|
+
- **`aria-label` is required** for icon-only buttons. An icon with no label
|
|
175
|
+
gives screen reader users no indication of the button's purpose.
|
|
176
|
+
- **XOR enforcement** — the TypeScript type allows exactly one of `aria-label`
|
|
177
|
+
or `aria-labelledby`. Passing both or neither is a compile-time error.
|
|
178
|
+
- **`label` does not replace `aria-label`** — even when a visible `label` is
|
|
179
|
+
provided, you must still pass `aria-label` (or `aria-labelledby`). The `label`
|
|
180
|
+
prop controls visual presentation only; `aria-label` provides the computed
|
|
181
|
+
accessible name.
|
|
182
|
+
- **`disabled` keeps focus** — `IconButton` uses `aria-disabled="true"` instead
|
|
183
|
+
of the native `disabled` attribute. The button stays in the tab order and can
|
|
184
|
+
receive focus, satisfying WCAG 2.1.1 (Keyboard) and 4.1.2 (Name, Role, Value).
|
|
185
|
+
- **Icon `aria-hidden`** — always render icons with `aria-hidden="true"` so
|
|
186
|
+
screen readers do not double-announce both the SVG content and the button label.
|
|
187
|
+
|
|
188
|
+
```tsx
|
|
189
|
+
// Correct — icon is decorative, label carries the meaning
|
|
190
|
+
<IconButton
|
|
191
|
+
type="button"
|
|
192
|
+
aria-label="Close"
|
|
193
|
+
icon={<svg aria-hidden="true">...</svg>}
|
|
194
|
+
/>
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Related
|
|
200
|
+
|
|
201
|
+
- [Button README](./README.mdx) — base `Button` props and variants
|
|
202
|
+
- [Button Styles](./STYLES.mdx) — full CSS custom property reference
|
|
203
|
+
- [WCAG 2.5.5 Target Size](https://www.w3.org/WAI/WCAG21/Understanding/target-size.html)
|
|
204
|
+
- [WCAG 2.4.1 Bypass Blocks](https://www.w3.org/WAI/WCAG21/Understanding/bypass-blocks.html)
|