@nationaldesignstudio/react 0.0.8 → 0.0.9
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/atoms/button/button.d.ts +1 -1
- package/dist/components/atoms/button/icon-button.d.ts +20 -0
- package/dist/components/organisms/navbar/navbar.d.ts +1 -0
- package/dist/index.js +4659 -1876
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/atoms/accordion/accordion.stories.tsx +1 -1
- package/src/components/atoms/button/button.stories.tsx +0 -9
- package/src/components/atoms/button/button.tsx +1 -1
- package/src/components/atoms/button/icon-button.stories.tsx +0 -9
- package/src/components/atoms/button/icon-button.tsx +35 -0
- package/src/components/atoms/pager-control/pager-control.stories.tsx +0 -3
- package/src/components/organisms/navbar/navbar.tsx +31 -79
- package/src/components/sections/banner/banner.tsx +3 -5
- package/src/components/sections/faq-section/faq-section.tsx +2 -1
- package/src/components/sections/hero/hero.tsx +1 -0
- package/src/components/sections/tout/tout.tsx +1 -0
- package/src/components/sections/two-column-section/two-column-section.tsx +8 -11
- package/src/stories/TokenShowcase.stories.tsx +3 -3
package/package.json
CHANGED
|
@@ -58,23 +58,14 @@ export const CharcoalOutlineQuiet = () => (
|
|
|
58
58
|
);
|
|
59
59
|
|
|
60
60
|
export const Ivory = () => <Button variant="ivory">Ivory</Button>;
|
|
61
|
-
Ivory.parameters = {
|
|
62
|
-
backgrounds: { default: "dark" },
|
|
63
|
-
};
|
|
64
61
|
|
|
65
62
|
export const IvoryOutline = () => (
|
|
66
63
|
<Button variant="ivoryOutline">Ivory Outline</Button>
|
|
67
64
|
);
|
|
68
|
-
IvoryOutline.parameters = {
|
|
69
|
-
backgrounds: { default: "dark" },
|
|
70
|
-
};
|
|
71
65
|
|
|
72
66
|
export const IvoryOutlineQuiet = () => (
|
|
73
67
|
<Button variant="ivoryOutlineQuiet">Ivory Outline Quiet</Button>
|
|
74
68
|
);
|
|
75
|
-
IvoryOutlineQuiet.parameters = {
|
|
76
|
-
backgrounds: { default: "dark" },
|
|
77
|
-
};
|
|
78
69
|
|
|
79
70
|
// =============================================================================
|
|
80
71
|
// Sizes
|
|
@@ -49,7 +49,7 @@ const buttonVariants = cva(
|
|
|
49
49
|
ivoryOutlineQuiet:
|
|
50
50
|
"border border-alpha-white-20 text-alpha-white-60 hover:border-alpha-white-30 hover:text-alpha-white-80 active:bg-alpha-white-5 focus-visible:ring-gray-50 focus-visible:ring-offset-gray-1000",
|
|
51
51
|
// Secondary - gray filled button (for dark backgrounds)
|
|
52
|
-
|
|
52
|
+
gray:
|
|
53
53
|
"bg-gray-800 text-gray-100 hover:bg-gray-700 active:bg-gray-600 focus-visible:ring-gray-700 focus-visible:ring-offset-gray-1000",
|
|
54
54
|
},
|
|
55
55
|
size: {
|
|
@@ -120,27 +120,18 @@ export const Ivory = () => (
|
|
|
120
120
|
<SearchIcon />
|
|
121
121
|
</IconButton>
|
|
122
122
|
);
|
|
123
|
-
Ivory.parameters = {
|
|
124
|
-
backgrounds: { default: "dark" },
|
|
125
|
-
};
|
|
126
123
|
|
|
127
124
|
export const IvoryOutline = () => (
|
|
128
125
|
<IconButton variant="ivoryOutline">
|
|
129
126
|
<ArrowRightIcon />
|
|
130
127
|
</IconButton>
|
|
131
128
|
);
|
|
132
|
-
IvoryOutline.parameters = {
|
|
133
|
-
backgrounds: { default: "dark" },
|
|
134
|
-
};
|
|
135
129
|
|
|
136
130
|
export const IvoryOutlineQuiet = () => (
|
|
137
131
|
<IconButton variant="ivoryOutlineQuiet">
|
|
138
132
|
<ArrowRightIcon />
|
|
139
133
|
</IconButton>
|
|
140
134
|
);
|
|
141
|
-
IvoryOutlineQuiet.parameters = {
|
|
142
|
-
backgrounds: { default: "dark" },
|
|
143
|
-
};
|
|
144
135
|
|
|
145
136
|
// =============================================================================
|
|
146
137
|
// Sizes
|
|
@@ -6,6 +6,26 @@ import { cn } from "@/lib/utils";
|
|
|
6
6
|
/**
|
|
7
7
|
* IconButton component based on Figma BaseKit / Interface / Icon Button
|
|
8
8
|
*
|
|
9
|
+
* **IMPORTANT: Accessibility Requirement**
|
|
10
|
+
* Icon-only buttons MUST have an accessible label. Provide one of:
|
|
11
|
+
* - `aria-label`: A text description of the button's action (recommended)
|
|
12
|
+
* - `aria-labelledby`: Reference to an element containing the label
|
|
13
|
+
* - `title`: Tooltip text (less preferred, but provides a label)
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```tsx
|
|
17
|
+
* // Correct usage with aria-label
|
|
18
|
+
* <IconButton aria-label="Close menu">
|
|
19
|
+
* <CloseIcon />
|
|
20
|
+
* </IconButton>
|
|
21
|
+
*
|
|
22
|
+
* // Correct usage with aria-labelledby
|
|
23
|
+
* <IconButton aria-labelledby="close-label">
|
|
24
|
+
* <CloseIcon />
|
|
25
|
+
* </IconButton>
|
|
26
|
+
* <span id="close-label" className="sr-only">Close menu</span>
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
9
29
|
* Variants:
|
|
10
30
|
* - charcoal: Dark filled button (for light backgrounds)
|
|
11
31
|
* - charcoalOutline: Dark outlined button (for light backgrounds)
|
|
@@ -75,6 +95,21 @@ export interface IconButtonProps
|
|
|
75
95
|
|
|
76
96
|
const IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>(
|
|
77
97
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
|
98
|
+
// Development warning for missing accessible label
|
|
99
|
+
React.useEffect(() => {
|
|
100
|
+
if (import.meta.env?.DEV) {
|
|
101
|
+
const hasAccessibleLabel =
|
|
102
|
+
props["aria-label"] ||
|
|
103
|
+
props["aria-labelledby"] ||
|
|
104
|
+
props.title;
|
|
105
|
+
if (!hasAccessibleLabel) {
|
|
106
|
+
console.warn(
|
|
107
|
+
"IconButton: Missing accessible label. Icon-only buttons must have an aria-label, aria-labelledby, or title attribute for screen reader users.",
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}, [props["aria-label"], props["aria-labelledby"], props.title]);
|
|
112
|
+
|
|
78
113
|
const Comp = asChild ? Slot : "button";
|
|
79
114
|
return (
|
|
80
115
|
<Comp
|
|
@@ -79,9 +79,6 @@ Playground.args = {
|
|
|
79
79
|
export const Charcoal = () => <PagerControl count={4} variant="charcoal" />;
|
|
80
80
|
|
|
81
81
|
export const Ivory = () => <PagerControl count={4} variant="ivory" />;
|
|
82
|
-
Ivory.parameters = {
|
|
83
|
-
backgrounds: { default: "dark" },
|
|
84
|
-
};
|
|
85
82
|
|
|
86
83
|
// =============================================================================
|
|
87
84
|
// Sizes
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Dialog } from "@base-ui-components/react/dialog";
|
|
1
2
|
import { Slot } from "@radix-ui/react-slot";
|
|
2
3
|
import * as React from "react";
|
|
3
4
|
import { cn } from "@/lib/utils";
|
|
@@ -168,6 +169,7 @@ export interface NavbarMobileMenuProps
|
|
|
168
169
|
/**
|
|
169
170
|
* Mobile menu container that displays navigation links on mobile devices.
|
|
170
171
|
* Hidden on desktop (md and above). Should be used with NavbarMobileMenuButton.
|
|
172
|
+
* Built on Base UI Dialog for accessibility (focus trap, escape key, click-outside).
|
|
171
173
|
*/
|
|
172
174
|
const NavbarMobileMenu = React.forwardRef<
|
|
173
175
|
HTMLDivElement,
|
|
@@ -175,87 +177,37 @@ const NavbarMobileMenu = React.forwardRef<
|
|
|
175
177
|
>(({ className, children, ...props }, ref) => {
|
|
176
178
|
const { isMobileMenuOpen, setIsMobileMenuOpen } =
|
|
177
179
|
React.useContext(NavbarContext);
|
|
178
|
-
const menuRef = React.useRef<HTMLDivElement>(null);
|
|
179
|
-
|
|
180
|
-
// Close menu when clicking outside
|
|
181
|
-
React.useEffect(() => {
|
|
182
|
-
if (!isMobileMenuOpen) return;
|
|
183
|
-
|
|
184
|
-
const handleClickOutside = (event: MouseEvent) => {
|
|
185
|
-
const target = event.target as Node;
|
|
186
|
-
if (menuRef.current && !menuRef.current.contains(target)) {
|
|
187
|
-
setIsMobileMenuOpen(false);
|
|
188
|
-
}
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
// Use setTimeout to avoid immediate close on button click
|
|
192
|
-
const timeoutId = setTimeout(() => {
|
|
193
|
-
document.addEventListener("mousedown", handleClickOutside);
|
|
194
|
-
}, 0);
|
|
195
|
-
|
|
196
|
-
return () => {
|
|
197
|
-
clearTimeout(timeoutId);
|
|
198
|
-
document.removeEventListener("mousedown", handleClickOutside);
|
|
199
|
-
};
|
|
200
|
-
}, [isMobileMenuOpen, setIsMobileMenuOpen]);
|
|
201
|
-
|
|
202
|
-
// Prevent body scroll when menu is open
|
|
203
|
-
React.useEffect(() => {
|
|
204
|
-
if (isMobileMenuOpen) {
|
|
205
|
-
document.body.style.overflow = "hidden";
|
|
206
|
-
} else {
|
|
207
|
-
document.body.style.overflow = "";
|
|
208
|
-
}
|
|
209
|
-
return () => {
|
|
210
|
-
document.body.style.overflow = "";
|
|
211
|
-
};
|
|
212
|
-
}, [isMobileMenuOpen]);
|
|
213
|
-
|
|
214
|
-
// Close menu on escape key
|
|
215
|
-
React.useEffect(() => {
|
|
216
|
-
if (!isMobileMenuOpen) return;
|
|
217
|
-
|
|
218
|
-
const handleEscape = (event: KeyboardEvent) => {
|
|
219
|
-
if (event.key === "Escape") {
|
|
220
|
-
setIsMobileMenuOpen(false);
|
|
221
|
-
}
|
|
222
|
-
};
|
|
223
|
-
|
|
224
|
-
document.addEventListener("keydown", handleEscape);
|
|
225
|
-
return () => document.removeEventListener("keydown", handleEscape);
|
|
226
|
-
}, [isMobileMenuOpen, setIsMobileMenuOpen]);
|
|
227
|
-
|
|
228
|
-
if (!isMobileMenuOpen) return null;
|
|
229
180
|
|
|
230
181
|
return (
|
|
231
|
-
<
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
182
|
+
<Dialog.Root open={isMobileMenuOpen} onOpenChange={setIsMobileMenuOpen}>
|
|
183
|
+
<Dialog.Portal>
|
|
184
|
+
<Dialog.Popup
|
|
185
|
+
ref={ref}
|
|
186
|
+
className={cn(
|
|
187
|
+
"fixed inset-0 z-50 bg-gray-50 md:hidden",
|
|
188
|
+
// Position below navbar
|
|
189
|
+
"pt-[calc(var(--navbar-height,60px)+1px)]",
|
|
190
|
+
// Smooth transition
|
|
191
|
+
"transition-opacity duration-200",
|
|
192
|
+
className,
|
|
193
|
+
)}
|
|
194
|
+
{...props}
|
|
195
|
+
>
|
|
196
|
+
<Dialog.Title className="sr-only">Navigation menu</Dialog.Title>
|
|
197
|
+
<div
|
|
198
|
+
className={cn(
|
|
199
|
+
"flex flex-col",
|
|
200
|
+
// Padding matching navbar
|
|
201
|
+
"px-spacing-20 py-spacing-16",
|
|
202
|
+
// Gap between links
|
|
203
|
+
"gap-spacing-8",
|
|
204
|
+
)}
|
|
205
|
+
>
|
|
206
|
+
{children}
|
|
207
|
+
</div>
|
|
208
|
+
</Dialog.Popup>
|
|
209
|
+
</Dialog.Portal>
|
|
210
|
+
</Dialog.Root>
|
|
259
211
|
);
|
|
260
212
|
});
|
|
261
213
|
NavbarMobileMenu.displayName = "NavbarMobileMenu";
|
|
@@ -86,8 +86,6 @@ export interface BannerProps
|
|
|
86
86
|
*/
|
|
87
87
|
const Banner = React.forwardRef<HTMLElement, BannerProps>(
|
|
88
88
|
({ className, theme, heading, description, action, ...props }, ref) => {
|
|
89
|
-
const isDark = theme === "dark";
|
|
90
|
-
|
|
91
89
|
return (
|
|
92
90
|
<section
|
|
93
91
|
ref={ref}
|
|
@@ -98,7 +96,7 @@ const Banner = React.forwardRef<HTMLElement, BannerProps>(
|
|
|
98
96
|
<div
|
|
99
97
|
className={cn(
|
|
100
98
|
"flex flex-col md:flex-row gap-spacing-20 items-start md:items-center md:justify-between",
|
|
101
|
-
|
|
99
|
+
theme === "dark" && "border-t border-gray-700 py-spacing-36",
|
|
102
100
|
)}
|
|
103
101
|
>
|
|
104
102
|
{/* Copy section */}
|
|
@@ -106,7 +104,7 @@ const Banner = React.forwardRef<HTMLElement, BannerProps>(
|
|
|
106
104
|
<h2
|
|
107
105
|
className={cn(
|
|
108
106
|
"typography-subheading-small",
|
|
109
|
-
|
|
107
|
+
theme === "dark" ? "text-gray-100" : "text-gray-900",
|
|
110
108
|
)}
|
|
111
109
|
>
|
|
112
110
|
{heading}
|
|
@@ -114,7 +112,7 @@ const Banner = React.forwardRef<HTMLElement, BannerProps>(
|
|
|
114
112
|
<p
|
|
115
113
|
className={cn(
|
|
116
114
|
"typography-body-small",
|
|
117
|
-
|
|
115
|
+
theme === "dark" ? "text-gray-500" : "text-gray-800",
|
|
118
116
|
)}
|
|
119
117
|
>
|
|
120
118
|
{description}
|
|
@@ -48,13 +48,14 @@ export interface FaqSectionProps
|
|
|
48
48
|
*/
|
|
49
49
|
const FaqSection = React.forwardRef<HTMLElement, FaqSectionProps>(
|
|
50
50
|
(
|
|
51
|
-
{ className, theme = "dark", title = "Frequently Asked Questions", children, ...props },
|
|
51
|
+
{ className, theme = "dark", title = "Frequently Asked Questions", children, layout, ...props },
|
|
52
52
|
ref,
|
|
53
53
|
) => {
|
|
54
54
|
return (
|
|
55
55
|
<TwoColumnSection
|
|
56
56
|
ref={ref}
|
|
57
57
|
theme={theme}
|
|
58
|
+
layout={layout ?? undefined}
|
|
58
59
|
title={title}
|
|
59
60
|
className={cn(
|
|
60
61
|
// Override title typography to be larger
|
|
@@ -87,10 +87,7 @@ export interface TwoColumnSectionProps
|
|
|
87
87
|
* ```
|
|
88
88
|
*/
|
|
89
89
|
const TwoColumnSection = React.forwardRef<HTMLElement, TwoColumnSectionProps>(
|
|
90
|
-
({ className, theme, layout, title, lead, children, ...props }, ref) => {
|
|
91
|
-
const isDark = theme === "dark" || theme === undefined;
|
|
92
|
-
const isEqual = layout === "equal";
|
|
93
|
-
|
|
90
|
+
({ className, theme = "dark", layout, title, lead, children, ...props }, ref) => {
|
|
94
91
|
return (
|
|
95
92
|
<section
|
|
96
93
|
ref={ref}
|
|
@@ -101,10 +98,10 @@ const TwoColumnSection = React.forwardRef<HTMLElement, TwoColumnSectionProps>(
|
|
|
101
98
|
<div
|
|
102
99
|
className={cn(
|
|
103
100
|
"border-t pt-spacing-36",
|
|
104
|
-
|
|
101
|
+
theme === "dark" ? "border-gray-700" : "border-gray-300",
|
|
105
102
|
// Grid layout
|
|
106
103
|
"grid grid-cols-1 gap-spacing-56",
|
|
107
|
-
|
|
104
|
+
layout === "equal"
|
|
108
105
|
? "md:grid-cols-2"
|
|
109
106
|
: "lg:grid-cols-24 lg:gap-spacing-56",
|
|
110
107
|
)}
|
|
@@ -113,22 +110,22 @@ const TwoColumnSection = React.forwardRef<HTMLElement, TwoColumnSectionProps>(
|
|
|
113
110
|
<h2
|
|
114
111
|
className={cn(
|
|
115
112
|
"typography-subheading-medium",
|
|
116
|
-
|
|
113
|
+
theme === "dark" ? "text-gray-100" : "text-gray-900",
|
|
117
114
|
// Column span based on layout
|
|
118
|
-
|
|
115
|
+
layout !== "equal" && "lg:col-span-9",
|
|
119
116
|
)}
|
|
120
117
|
>
|
|
121
118
|
{title}
|
|
122
119
|
</h2>
|
|
123
120
|
|
|
124
121
|
{/* Content column */}
|
|
125
|
-
<div className={cn("flex flex-col gap-spacing-56",
|
|
122
|
+
<div className={cn("flex flex-col gap-spacing-56", layout !== "equal" && "lg:col-span-15")}>
|
|
126
123
|
{/* Lead content - brighter/prominent */}
|
|
127
124
|
{lead && (
|
|
128
125
|
<div
|
|
129
126
|
className={cn(
|
|
130
127
|
"typography-body-large",
|
|
131
|
-
|
|
128
|
+
theme === "dark" ? "text-gray-100" : "text-gray-900",
|
|
132
129
|
)}
|
|
133
130
|
>
|
|
134
131
|
{typeof lead === "string" ? <p>{lead}</p> : lead}
|
|
@@ -139,7 +136,7 @@ const TwoColumnSection = React.forwardRef<HTMLElement, TwoColumnSectionProps>(
|
|
|
139
136
|
<div
|
|
140
137
|
className={cn(
|
|
141
138
|
"typography-body-medium flex flex-col gap-[1em]",
|
|
142
|
-
|
|
139
|
+
theme === "dark" ? "text-gray-400" : "text-gray-600",
|
|
143
140
|
)}
|
|
144
141
|
>
|
|
145
142
|
{children}
|
|
@@ -39,7 +39,7 @@ export const Colors: Story = {
|
|
|
39
39
|
|
|
40
40
|
export const Spacing: Story = {
|
|
41
41
|
render: () => (
|
|
42
|
-
<div className="p-8
|
|
42
|
+
<div className="p-8 min-h-screen">
|
|
43
43
|
<div className="max-w-4xl mx-auto">
|
|
44
44
|
<SpacingTokens />
|
|
45
45
|
</div>
|
|
@@ -49,7 +49,7 @@ export const Spacing: Story = {
|
|
|
49
49
|
|
|
50
50
|
export const Typography: Story = {
|
|
51
51
|
render: () => (
|
|
52
|
-
<div className="p-8
|
|
52
|
+
<div className="p-8 min-h-screen">
|
|
53
53
|
<div className="max-w-6xl mx-auto">
|
|
54
54
|
<TypographyTokens />
|
|
55
55
|
</div>
|
|
@@ -59,7 +59,7 @@ export const Typography: Story = {
|
|
|
59
59
|
|
|
60
60
|
export const SemanticSpacing: Story = {
|
|
61
61
|
render: () => (
|
|
62
|
-
<div className="p-8
|
|
62
|
+
<div className="p-8 min-h-screen">
|
|
63
63
|
<div className="max-w-4xl mx-auto">
|
|
64
64
|
<SemanticSpacingTokens />
|
|
65
65
|
</div>
|