@nationaldesignstudio/react 0.0.14 → 0.0.16
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/tailwind.css +15 -1
- package/dist/tokens.css +45 -60
- package/package.json +5 -10
- package/src/App.css +0 -0
- package/src/App.tsx +7 -0
- package/src/assets/fonts/PPNeueMontreal-Variable.woff2 +0 -0
- package/src/assets/react.svg +1 -0
- package/src/components/atoms/accordion/accordion.stories.tsx +228 -0
- package/src/components/atoms/accordion/accordion.tsx +219 -0
- package/src/components/atoms/accordion/index.ts +6 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-chromium-darwin.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-chromium-linux.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-outline-chromium-darwin.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-outline-chromium-linux.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-outline-quiet-chromium-darwin.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-outline-quiet-chromium-linux.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-disabled-chromium-darwin.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-disabled-chromium-linux.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-chromium-darwin.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-chromium-linux.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-outline-chromium-darwin.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-outline-chromium-linux.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-outline-quiet-chromium-darwin.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-outline-quiet-chromium-linux.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-large-chromium-darwin.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-large-chromium-linux.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-medium-chromium-darwin.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-medium-chromium-linux.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-small-chromium-darwin.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-small-chromium-linux.png +0 -0
- package/src/components/atoms/button/button.stories.tsx +102 -0
- package/src/components/atoms/button/button.test.tsx +135 -0
- package/src/components/atoms/button/button.tsx +139 -0
- package/src/components/atoms/button/button.visual.test.tsx +102 -0
- package/src/components/atoms/button/icon-button.stories.tsx +166 -0
- package/src/components/atoms/button/icon-button.tsx +120 -0
- package/src/components/atoms/button/index.ts +6 -0
- package/src/components/atoms/ndstudio-footer/index.ts +1 -0
- package/src/components/atoms/ndstudio-footer/ndstudio-footer.tsx +55 -0
- package/src/components/atoms/pager-control/index.ts +5 -0
- package/src/components/atoms/pager-control/pager-control.stories.tsx +209 -0
- package/src/components/atoms/pager-control/pager-control.test.tsx +130 -0
- package/src/components/atoms/pager-control/pager-control.tsx +329 -0
- package/src/components/dev-tools/dev-toolbar/dev-toolbar.stories.tsx +82 -0
- package/src/components/dev-tools/dev-toolbar/dev-toolbar.tsx +196 -0
- package/src/components/dev-tools/dev-toolbar/index.ts +1 -0
- package/src/components/dev-tools/grid-overlay/grid-overlay.tsx +41 -0
- package/src/components/dev-tools/grid-overlay/index.ts +1 -0
- package/src/components/dev-tools/index.ts +2 -0
- package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-default-vertical-chromium-darwin.png +0 -0
- package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-default-vertical-chromium-linux.png +0 -0
- package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-horizontal-chromium-darwin.png +0 -0
- package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-horizontal-chromium-linux.png +0 -0
- package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-minimal-chromium-darwin.png +0 -0
- package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-minimal-chromium-linux.png +0 -0
- package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-actions-chromium-darwin.png +0 -0
- package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-actions-chromium-linux.png +0 -0
- package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-eyebrow-chromium-darwin.png +0 -0
- package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-eyebrow-chromium-linux.png +0 -0
- package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-image-chromium-darwin.png +0 -0
- package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-image-chromium-linux.png +0 -0
- package/src/components/organisms/card/card.stories.tsx +293 -0
- package/src/components/organisms/card/card.test.tsx +245 -0
- package/src/components/organisms/card/card.tsx +225 -0
- package/src/components/organisms/card/card.visual.test.tsx +197 -0
- package/src/components/organisms/card/index.ts +19 -0
- package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-active-link-chromium-darwin.png +0 -0
- package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-active-link-chromium-linux.png +0 -0
- package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-brand-only-chromium-darwin.png +0 -0
- package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-brand-only-chromium-linux.png +0 -0
- package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-default-chromium-darwin.png +0 -0
- package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-default-chromium-linux.png +0 -0
- package/src/components/organisms/navbar/index.ts +18 -0
- package/src/components/organisms/navbar/navbar.stories.tsx +313 -0
- package/src/components/organisms/navbar/navbar.test.tsx +190 -0
- package/src/components/organisms/navbar/navbar.tsx +323 -0
- package/src/components/organisms/navbar/navbar.visual.test.tsx +85 -0
- package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-custom-icon-chromium-darwin.png +0 -0
- package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-custom-icon-chromium-linux.png +0 -0
- package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-custom-text-chromium-darwin.png +0 -0
- package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-custom-text-chromium-linux.png +0 -0
- package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-default-chromium-darwin.png +0 -0
- package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-default-chromium-linux.png +0 -0
- package/src/components/organisms/us-gov-banner/index.ts +1 -0
- package/src/components/organisms/us-gov-banner/us-gov-banner.stories.tsx +35 -0
- package/src/components/organisms/us-gov-banner/us-gov-banner.test.tsx +107 -0
- package/src/components/organisms/us-gov-banner/us-gov-banner.tsx +73 -0
- package/src/components/organisms/us-gov-banner/us-gov-banner.visual.test.tsx +46 -0
- package/src/components/sections/banner/banner.stories.tsx +150 -0
- package/src/components/sections/banner/banner.test.tsx +185 -0
- package/src/components/sections/banner/banner.tsx +130 -0
- package/src/components/sections/banner/index.ts +2 -0
- package/src/components/sections/card-grid/card-grid.stories.tsx +351 -0
- package/src/components/sections/card-grid/card-grid.tsx +116 -0
- package/src/components/sections/card-grid/index.ts +1 -0
- package/src/components/sections/faq-section/faq-section.stories.tsx +453 -0
- package/src/components/sections/faq-section/faq-section.tsx +84 -0
- package/src/components/sections/faq-section/index.ts +2 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-desktop-chromium-darwin.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-desktop-chromium-linux.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-mobile-chromium-darwin.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-mobile-chromium-linux.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-tablet-chromium-darwin.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-tablet-chromium-linux.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-desktop-chromium-darwin.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-desktop-chromium-linux.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-mobile-chromium-darwin.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-mobile-chromium-linux.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-tablet-chromium-darwin.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-tablet-chromium-linux.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-desktop-chromium-darwin.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-desktop-chromium-linux.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-mobile-chromium-darwin.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-mobile-chromium-linux.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-tablet-chromium-darwin.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-tablet-chromium-linux.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-custom-class-chromium-darwin.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-custom-class-chromium-linux.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-default-chromium-linux.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-long-title-chromium-darwin.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-long-title-chromium-linux.png +0 -0
- package/src/components/sections/hero/hero.stories.tsx +274 -0
- package/src/components/sections/hero/hero.test.tsx +135 -0
- package/src/components/sections/hero/hero.tsx +453 -0
- package/src/components/sections/hero/hero.visual.test.tsx +140 -0
- package/src/components/sections/hero/index.ts +10 -0
- package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-h3-heading-chromium-darwin.png +0 -0
- package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-h3-heading-chromium-linux.png +0 -0
- package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-multiple-paragraphs-chromium-darwin.png +0 -0
- package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-multiple-paragraphs-chromium-linux.png +0 -0
- package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-multiple-sections-chromium-darwin.png +0 -0
- package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-multiple-sections-chromium-linux.png +0 -0
- package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-single-section-chromium-darwin.png +0 -0
- package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-single-section-chromium-linux.png +0 -0
- package/src/components/sections/prose/index.ts +6 -0
- package/src/components/sections/prose/prose.stories.tsx +144 -0
- package/src/components/sections/prose/prose.test.tsx +178 -0
- package/src/components/sections/prose/prose.tsx +88 -0
- package/src/components/sections/prose/prose.visual.test.tsx +105 -0
- package/src/components/sections/river/index.ts +1 -0
- package/src/components/sections/river/river.stories.tsx +237 -0
- package/src/components/sections/river/river.test.tsx +268 -0
- package/src/components/sections/river/river.tsx +173 -0
- package/src/components/sections/tout/index.ts +1 -0
- package/src/components/sections/tout/tout.stories.tsx +171 -0
- package/src/components/sections/tout/tout.test.tsx +242 -0
- package/src/components/sections/tout/tout.tsx +270 -0
- package/src/components/sections/two-column-section/index.ts +5 -0
- package/src/components/sections/two-column-section/two-column-section.stories.tsx +285 -0
- package/src/components/sections/two-column-section/two-column-section.tsx +162 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/use-event-listener.ts +73 -0
- package/src/index.ts +155 -0
- package/src/lib/theme.ts +1000 -0
- package/src/lib/utils.ts +6 -0
- package/src/main.tsx +13 -0
- package/src/stories/GridSystem.stories.tsx +84 -0
- package/src/stories/Introduction.mdx +114 -0
- package/src/stories/ThemeProvider.stories.tsx +357 -0
- package/src/stories/TokenShowcase.stories.tsx +92 -0
- package/src/stories/TokenShowcase.tsx +1429 -0
- package/src/styles.css +11 -0
- package/src/theme/ThemeProvider.tsx +297 -0
- package/src/theme/hooks.ts +40 -0
- package/src/theme/index.ts +43 -0
- package/src/theme/utils.ts +104 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Accordion as BaseAccordion } from "@base-ui-components/react/accordion";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { tv, type VariantProps } from "tailwind-variants";
|
|
6
|
+
import { cn } from "@/lib/utils";
|
|
7
|
+
|
|
8
|
+
// =============================================================================
|
|
9
|
+
// Accordion Variants
|
|
10
|
+
// =============================================================================
|
|
11
|
+
|
|
12
|
+
const accordionVariants = tv({
|
|
13
|
+
base: "flex flex-col",
|
|
14
|
+
variants: {
|
|
15
|
+
variant: {
|
|
16
|
+
dark: "",
|
|
17
|
+
light: "",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
defaultVariants: {
|
|
21
|
+
variant: "dark",
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const accordionItemVariants = tv({
|
|
26
|
+
base: "border-b overflow-hidden",
|
|
27
|
+
variants: {
|
|
28
|
+
variant: {
|
|
29
|
+
dark: "border-gray-300",
|
|
30
|
+
light: "border-gray-500",
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
defaultVariants: {
|
|
34
|
+
variant: "dark",
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const accordionTriggerVariants = tv({
|
|
39
|
+
base: [
|
|
40
|
+
// Uses primitive spacing tokens
|
|
41
|
+
"flex w-full items-center justify-between py-spacing-24 text-left",
|
|
42
|
+
"typography-body-large transition-colors cursor-pointer",
|
|
43
|
+
],
|
|
44
|
+
variants: {
|
|
45
|
+
variant: {
|
|
46
|
+
dark: "text-gray-100 data-[open]:text-white",
|
|
47
|
+
light: "text-gray-800 data-[open]:text-gray-900",
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
defaultVariants: {
|
|
51
|
+
variant: "dark",
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const accordionPanelVariants = tv({
|
|
56
|
+
// Uses primitive spacing tokens
|
|
57
|
+
base: "typography-body-large pb-spacing-24",
|
|
58
|
+
variants: {
|
|
59
|
+
variant: {
|
|
60
|
+
dark: "text-gray-100",
|
|
61
|
+
light: "text-gray-800",
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
defaultVariants: {
|
|
65
|
+
variant: "dark",
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// =============================================================================
|
|
70
|
+
// Accordion Context
|
|
71
|
+
// =============================================================================
|
|
72
|
+
|
|
73
|
+
const AccordionContext = React.createContext<{
|
|
74
|
+
variant: "dark" | "light";
|
|
75
|
+
}>({
|
|
76
|
+
variant: "dark",
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// =============================================================================
|
|
80
|
+
// Accordion
|
|
81
|
+
// =============================================================================
|
|
82
|
+
|
|
83
|
+
export interface AccordionProps
|
|
84
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
85
|
+
VariantProps<typeof accordionVariants> {
|
|
86
|
+
/**
|
|
87
|
+
* Allow multiple items to be expanded at once
|
|
88
|
+
* @default false
|
|
89
|
+
*/
|
|
90
|
+
allowMultiple?: boolean;
|
|
91
|
+
/**
|
|
92
|
+
* The ID(s) of items that should be expanded by default
|
|
93
|
+
*/
|
|
94
|
+
defaultExpanded?: string | string[];
|
|
95
|
+
children: React.ReactNode;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Accordion component for expandable/collapsible content sections.
|
|
100
|
+
* Built on Base UI's Accordion primitive.
|
|
101
|
+
*
|
|
102
|
+
* Variants:
|
|
103
|
+
* - dark: Dark theme styling (default)
|
|
104
|
+
* - light: Light theme styling
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```tsx
|
|
108
|
+
* <Accordion defaultExpanded="item-1" variant="dark">
|
|
109
|
+
* <AccordionItem id="item-1" title="Question 1">
|
|
110
|
+
* Answer to question 1
|
|
111
|
+
* </AccordionItem>
|
|
112
|
+
* <AccordionItem id="item-2" title="Question 2">
|
|
113
|
+
* Answer to question 2
|
|
114
|
+
* </AccordionItem>
|
|
115
|
+
* </Accordion>
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
const Accordion = React.forwardRef<HTMLDivElement, AccordionProps>(
|
|
119
|
+
(
|
|
120
|
+
{
|
|
121
|
+
className,
|
|
122
|
+
allowMultiple = false,
|
|
123
|
+
defaultExpanded,
|
|
124
|
+
variant = "light",
|
|
125
|
+
children,
|
|
126
|
+
},
|
|
127
|
+
ref,
|
|
128
|
+
) => {
|
|
129
|
+
// Normalize defaultExpanded to array format for Base UI
|
|
130
|
+
const defaultValue = React.useMemo((): string[] | undefined => {
|
|
131
|
+
if (!defaultExpanded) return undefined;
|
|
132
|
+
if (Array.isArray(defaultExpanded)) return defaultExpanded;
|
|
133
|
+
return [defaultExpanded];
|
|
134
|
+
}, [defaultExpanded]);
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<AccordionContext.Provider value={{ variant: variant ?? "dark" }}>
|
|
138
|
+
<BaseAccordion.Root
|
|
139
|
+
ref={ref}
|
|
140
|
+
className={accordionVariants({ variant, class: className })}
|
|
141
|
+
defaultValue={defaultValue}
|
|
142
|
+
multiple={allowMultiple}
|
|
143
|
+
>
|
|
144
|
+
{children}
|
|
145
|
+
</BaseAccordion.Root>
|
|
146
|
+
</AccordionContext.Provider>
|
|
147
|
+
);
|
|
148
|
+
},
|
|
149
|
+
);
|
|
150
|
+
Accordion.displayName = "Accordion";
|
|
151
|
+
|
|
152
|
+
// =============================================================================
|
|
153
|
+
// AccordionItem
|
|
154
|
+
// =============================================================================
|
|
155
|
+
|
|
156
|
+
export interface AccordionItemProps
|
|
157
|
+
extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
|
|
158
|
+
/**
|
|
159
|
+
* Unique identifier for this item
|
|
160
|
+
*/
|
|
161
|
+
id: string;
|
|
162
|
+
/**
|
|
163
|
+
* The question/title displayed in the header
|
|
164
|
+
*/
|
|
165
|
+
title: string;
|
|
166
|
+
/**
|
|
167
|
+
* The answer/content revealed when expanded
|
|
168
|
+
*/
|
|
169
|
+
children: React.ReactNode;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Individual accordion item with collapsible content.
|
|
174
|
+
* Must be used within an Accordion component.
|
|
175
|
+
*/
|
|
176
|
+
const AccordionItem = React.forwardRef<HTMLDivElement, AccordionItemProps>(
|
|
177
|
+
({ className, id, title, children }, ref) => {
|
|
178
|
+
const { variant } = React.useContext(AccordionContext);
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<BaseAccordion.Item
|
|
182
|
+
ref={ref}
|
|
183
|
+
value={id}
|
|
184
|
+
className={accordionItemVariants({ variant, class: className })}
|
|
185
|
+
>
|
|
186
|
+
{/* Header - always visible */}
|
|
187
|
+
<BaseAccordion.Header>
|
|
188
|
+
<BaseAccordion.Trigger
|
|
189
|
+
className={accordionTriggerVariants({ variant })}
|
|
190
|
+
>
|
|
191
|
+
<span>{title}</span>
|
|
192
|
+
<span
|
|
193
|
+
className={cn(
|
|
194
|
+
"text-base transition-transform duration-200",
|
|
195
|
+
"[[data-open]_&]:rotate-45",
|
|
196
|
+
)}
|
|
197
|
+
>
|
|
198
|
+
+
|
|
199
|
+
</span>
|
|
200
|
+
</BaseAccordion.Trigger>
|
|
201
|
+
</BaseAccordion.Header>
|
|
202
|
+
|
|
203
|
+
{/* Content - collapsible */}
|
|
204
|
+
<BaseAccordion.Panel
|
|
205
|
+
className={cn(
|
|
206
|
+
"h-[var(--accordion-panel-height)] overflow-hidden",
|
|
207
|
+
"transition-[height] duration-300 ease-out",
|
|
208
|
+
"[&[data-starting-style]]:h-0 [&[data-ending-style]]:h-0",
|
|
209
|
+
)}
|
|
210
|
+
>
|
|
211
|
+
<div className={accordionPanelVariants({ variant })}>{children}</div>
|
|
212
|
+
</BaseAccordion.Panel>
|
|
213
|
+
</BaseAccordion.Item>
|
|
214
|
+
);
|
|
215
|
+
},
|
|
216
|
+
);
|
|
217
|
+
AccordionItem.displayName = "AccordionItem";
|
|
218
|
+
|
|
219
|
+
export { Accordion, AccordionItem, accordionVariants };
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { Button } from ".";
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof Button> = {
|
|
5
|
+
title: "Atoms/Button",
|
|
6
|
+
} as Meta<typeof Button>;
|
|
7
|
+
|
|
8
|
+
export default meta;
|
|
9
|
+
type Story = StoryObj<typeof Button>;
|
|
10
|
+
|
|
11
|
+
export const Playground: Story = {
|
|
12
|
+
render: (args) => <Button {...args}>Button</Button>,
|
|
13
|
+
};
|
|
14
|
+
Playground.argTypes = {
|
|
15
|
+
size: {
|
|
16
|
+
control: {
|
|
17
|
+
type: "radio",
|
|
18
|
+
},
|
|
19
|
+
options: ["sm", "default", "lg"],
|
|
20
|
+
},
|
|
21
|
+
disabled: {
|
|
22
|
+
control: {
|
|
23
|
+
type: "boolean",
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
variant: {
|
|
27
|
+
control: {
|
|
28
|
+
type: "radio",
|
|
29
|
+
},
|
|
30
|
+
options: [
|
|
31
|
+
"primary",
|
|
32
|
+
"primaryOutline",
|
|
33
|
+
"secondary",
|
|
34
|
+
"charcoal",
|
|
35
|
+
"charcoalOutline",
|
|
36
|
+
"charcoalOutlineQuiet",
|
|
37
|
+
"ivory",
|
|
38
|
+
"ivoryOutline",
|
|
39
|
+
"ivoryOutlineQuiet",
|
|
40
|
+
"gray",
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
Playground.args = {
|
|
45
|
+
size: "default",
|
|
46
|
+
disabled: false,
|
|
47
|
+
variant: "primary",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// =============================================================================
|
|
51
|
+
// Semantic Variants (recommended)
|
|
52
|
+
// =============================================================================
|
|
53
|
+
|
|
54
|
+
export const Primary = () => <Button variant="primary">Primary</Button>;
|
|
55
|
+
|
|
56
|
+
export const PrimaryOutline = () => (
|
|
57
|
+
<Button variant="primaryOutline">Primary Outline</Button>
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
export const Secondary = () => <Button variant="secondary">Secondary</Button>;
|
|
61
|
+
|
|
62
|
+
// =============================================================================
|
|
63
|
+
// Legacy Variants
|
|
64
|
+
// =============================================================================
|
|
65
|
+
|
|
66
|
+
export const Charcoal = () => <Button variant="charcoal">Charcoal</Button>;
|
|
67
|
+
|
|
68
|
+
export const CharcoalOutline = () => (
|
|
69
|
+
<Button variant="charcoalOutline">Charcoal Outline</Button>
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
export const CharcoalOutlineQuiet = () => (
|
|
73
|
+
<Button variant="charcoalOutlineQuiet">Charcoal Outline Quiet</Button>
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
export const Ivory = () => <Button variant="ivory">Ivory</Button>;
|
|
77
|
+
|
|
78
|
+
export const IvoryOutline = () => (
|
|
79
|
+
<Button variant="ivoryOutline">Ivory Outline</Button>
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
export const IvoryOutlineQuiet = () => (
|
|
83
|
+
<Button variant="ivoryOutlineQuiet">Ivory Outline Quiet</Button>
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
export const Gray = () => <Button variant="gray">Gray</Button>;
|
|
87
|
+
|
|
88
|
+
// =============================================================================
|
|
89
|
+
// Sizes
|
|
90
|
+
// =============================================================================
|
|
91
|
+
|
|
92
|
+
export const Small = () => <Button size="sm">Small</Button>;
|
|
93
|
+
|
|
94
|
+
export const Medium = () => <Button size="default">Medium</Button>;
|
|
95
|
+
|
|
96
|
+
export const Large = () => <Button size="lg">Large</Button>;
|
|
97
|
+
|
|
98
|
+
// =============================================================================
|
|
99
|
+
// States
|
|
100
|
+
// =============================================================================
|
|
101
|
+
|
|
102
|
+
export const Disabled = () => <Button disabled>Disabled</Button>;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { describe, expect, test, vi } from "vitest";
|
|
2
|
+
import { page, userEvent } from "vitest/browser";
|
|
3
|
+
import { render } from "vitest-browser-react";
|
|
4
|
+
import { Button } from "./button";
|
|
5
|
+
|
|
6
|
+
describe("Button", () => {
|
|
7
|
+
describe("Accessibility", () => {
|
|
8
|
+
test("has correct button role", async () => {
|
|
9
|
+
render(<Button>Click me</Button>);
|
|
10
|
+
await expect
|
|
11
|
+
.element(page.getByRole("button", { name: "Click me" }))
|
|
12
|
+
.toBeInTheDocument();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("is focusable via keyboard", async () => {
|
|
16
|
+
render(<Button>Focusable</Button>);
|
|
17
|
+
await userEvent.keyboard("{Tab}");
|
|
18
|
+
await expect
|
|
19
|
+
.element(page.getByRole("button", { name: "Focusable" }))
|
|
20
|
+
.toHaveFocus();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("disabled button has disabled attribute", async () => {
|
|
24
|
+
render(<Button disabled>Disabled</Button>);
|
|
25
|
+
await expect
|
|
26
|
+
.element(page.getByRole("button", { name: "Disabled" }))
|
|
27
|
+
.toBeDisabled();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("disabled button is not focusable", async () => {
|
|
31
|
+
render(
|
|
32
|
+
<>
|
|
33
|
+
<Button disabled>Disabled</Button>
|
|
34
|
+
<Button>After</Button>
|
|
35
|
+
</>,
|
|
36
|
+
);
|
|
37
|
+
await userEvent.keyboard("{Tab}");
|
|
38
|
+
// Focus should skip the disabled button and go to the next one
|
|
39
|
+
await expect
|
|
40
|
+
.element(page.getByRole("button", { name: "After" }))
|
|
41
|
+
.toHaveFocus();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("button with aria-label has accessible name", async () => {
|
|
45
|
+
render(<Button aria-label="Close dialog">×</Button>);
|
|
46
|
+
await expect
|
|
47
|
+
.element(page.getByRole("button", { name: "Close dialog" }))
|
|
48
|
+
.toBeInTheDocument();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("button type defaults to button (not submit)", async () => {
|
|
52
|
+
render(<Button>Click me</Button>);
|
|
53
|
+
await expect
|
|
54
|
+
.element(page.getByRole("button", { name: "Click me" }))
|
|
55
|
+
.toHaveAttribute("type", "button");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("button type can be set explicitly to submit", async () => {
|
|
59
|
+
render(<Button type="submit">Submit</Button>);
|
|
60
|
+
await expect
|
|
61
|
+
.element(page.getByRole("button", { name: "Submit" }))
|
|
62
|
+
.toHaveAttribute("type", "submit");
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("Interactions", () => {
|
|
67
|
+
test("calls onClick when clicked", async () => {
|
|
68
|
+
const handleClick = vi.fn();
|
|
69
|
+
render(<Button onClick={handleClick}>Click me</Button>);
|
|
70
|
+
await page.getByRole("button", { name: "Click me" }).click();
|
|
71
|
+
expect(handleClick).toHaveBeenCalledOnce();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("responds to Enter key when focused", async () => {
|
|
75
|
+
const handleClick = vi.fn();
|
|
76
|
+
render(<Button onClick={handleClick}>Enter key</Button>);
|
|
77
|
+
page.getByRole("button", { name: "Enter key" }).element().focus();
|
|
78
|
+
await userEvent.keyboard("{Enter}");
|
|
79
|
+
expect(handleClick).toHaveBeenCalledOnce();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("responds to Space key when focused", async () => {
|
|
83
|
+
const handleClick = vi.fn();
|
|
84
|
+
render(<Button onClick={handleClick}>Space key</Button>);
|
|
85
|
+
page.getByRole("button", { name: "Space key" }).element().focus();
|
|
86
|
+
await userEvent.keyboard(" ");
|
|
87
|
+
expect(handleClick).toHaveBeenCalledOnce();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("does not fire onClick when disabled", async () => {
|
|
91
|
+
const handleClick = vi.fn();
|
|
92
|
+
render(
|
|
93
|
+
<Button disabled onClick={handleClick}>
|
|
94
|
+
Disabled
|
|
95
|
+
</Button>,
|
|
96
|
+
);
|
|
97
|
+
await page
|
|
98
|
+
.getByRole("button", { name: "Disabled" })
|
|
99
|
+
.click({ force: true });
|
|
100
|
+
expect(handleClick).not.toHaveBeenCalled();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("supports multiple clicks", async () => {
|
|
104
|
+
const handleClick = vi.fn();
|
|
105
|
+
render(<Button onClick={handleClick}>Multi click</Button>);
|
|
106
|
+
const button = page.getByRole("button", { name: "Multi click" });
|
|
107
|
+
await button.click();
|
|
108
|
+
await button.click();
|
|
109
|
+
await button.click();
|
|
110
|
+
expect(handleClick).toHaveBeenCalledTimes(3);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe("render prop", () => {
|
|
115
|
+
test("renders as anchor element with button role when render prop is used", async () => {
|
|
116
|
+
render(
|
|
117
|
+
// biome-ignore lint/a11y/useAnchorContent: Content provided via Button children
|
|
118
|
+
<Button render={<a href="/test" />}>Link Button</Button>,
|
|
119
|
+
);
|
|
120
|
+
// Base UI keeps role="button" for accessibility when rendering as another element
|
|
121
|
+
const button = page.getByRole("button", { name: "Link Button" });
|
|
122
|
+
await expect.element(button).toBeInTheDocument();
|
|
123
|
+
await expect.element(button).toHaveAttribute("href", "/test");
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe("Variants", () => {
|
|
128
|
+
test("applies primary variant classes by default", async () => {
|
|
129
|
+
render(<Button>Default</Button>);
|
|
130
|
+
const button = page.getByRole("button", { name: "Default" });
|
|
131
|
+
// Button uses semantic token classes
|
|
132
|
+
await expect.element(button).toHaveClass(/bg-button-primary-bg/);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Button as BaseButton,
|
|
3
|
+
type ButtonProps as BaseButtonProps,
|
|
4
|
+
} from "@base-ui-components/react/button";
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
import { tv, type VariantProps } from "tailwind-variants";
|
|
7
|
+
import { type ButtonTheme, buttonThemeToStyleVars } from "../../../lib/theme";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Button component based on Figma BaseKit / Interface / Buttons
|
|
11
|
+
*
|
|
12
|
+
* Variants:
|
|
13
|
+
* - primary: Primary filled button using semantic color tokens
|
|
14
|
+
* - primaryOutline: Outlined primary button (for light backgrounds)
|
|
15
|
+
* - secondary: Secondary button using semantic color tokens
|
|
16
|
+
* - charcoal: Dark filled button (for light backgrounds) - legacy
|
|
17
|
+
* - charcoalOutline: Dark outlined button (for light backgrounds)
|
|
18
|
+
* - charcoalOutlineQuiet: Subtle dark outlined button (for light backgrounds)
|
|
19
|
+
* - ivory: Light filled button (for dark backgrounds)
|
|
20
|
+
* - ivoryOutline: Light outlined button (for dark backgrounds)
|
|
21
|
+
* - ivoryOutlineQuiet: Subtle light outlined button (for dark backgrounds)
|
|
22
|
+
* - gray: Gray filled button (for dark backgrounds)
|
|
23
|
+
*
|
|
24
|
+
* Sizes:
|
|
25
|
+
* - lg: Large buttons
|
|
26
|
+
* - default: Medium buttons
|
|
27
|
+
* - sm: Small buttons
|
|
28
|
+
*
|
|
29
|
+
* For icon-only buttons, use the IconButton component instead.
|
|
30
|
+
*
|
|
31
|
+
* Theme Support:
|
|
32
|
+
* Pass a `theme` prop to override default colors via CSS custom properties.
|
|
33
|
+
* Surface tokens (--radius-surface-button, --surface-button-stroke) control
|
|
34
|
+
* border radius and stroke width across all variants.
|
|
35
|
+
*/
|
|
36
|
+
const buttonVariants = tv({
|
|
37
|
+
base: "inline-flex items-center justify-center gap-spacing-8 whitespace-nowrap transition-colors duration-150 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 rounded-surface-button stroke-surface-button border-solid",
|
|
38
|
+
variants: {
|
|
39
|
+
variant: {
|
|
40
|
+
// Primary - uses semantic color tokens
|
|
41
|
+
primary:
|
|
42
|
+
"bg-button-primary-bg text-text-inverted hover:bg-button-primary-bg-hover border-transparent focus-visible:ring-button-primary-bg",
|
|
43
|
+
// Primary Outline - outlined primary (for light backgrounds)
|
|
44
|
+
primaryOutline:
|
|
45
|
+
"bg-transparent text-text-primary border-border-strong hover:bg-alpha-black-5 active:bg-alpha-black-10 focus-visible:ring-button-primary-bg",
|
|
46
|
+
// Secondary - uses semantic color tokens
|
|
47
|
+
secondary:
|
|
48
|
+
"bg-button-secondary-bg text-text-primary border-border-subtle hover:bg-button-secondary-bg-hover focus-visible:ring-button-primary-bg",
|
|
49
|
+
// Charcoal (dark filled) - primary dark (legacy)
|
|
50
|
+
charcoal:
|
|
51
|
+
"bg-gray-1200 text-gray-100 hover:bg-gray-1100 active:bg-gray-1000 border-transparent focus-visible:ring-gray-1000",
|
|
52
|
+
// Charcoal Outline - outlined dark (for light backgrounds)
|
|
53
|
+
charcoalOutline:
|
|
54
|
+
"border-border-strong text-gray-1000 hover:bg-alpha-black-5 active:bg-alpha-black-10 focus-visible:ring-gray-1000",
|
|
55
|
+
// Charcoal Outline Quiet - subtle outlined dark (for light backgrounds)
|
|
56
|
+
charcoalOutlineQuiet:
|
|
57
|
+
"border-border-subtle text-alpha-black-60 hover:border-border-strong hover:text-alpha-black-80 active:bg-alpha-black-5 focus-visible:ring-gray-1000",
|
|
58
|
+
// Ivory (light filled) - primary light (for dark backgrounds)
|
|
59
|
+
ivory:
|
|
60
|
+
"bg-gray-50 text-gray-1000 hover:bg-gray-100 active:bg-gray-200 border-transparent focus-visible:ring-gray-50 focus-visible:ring-offset-gray-1000",
|
|
61
|
+
// Ivory Outline - outlined light (for dark backgrounds)
|
|
62
|
+
ivoryOutline:
|
|
63
|
+
"border-gray-50 text-gray-50 hover:bg-alpha-white-10 active:bg-alpha-white-20 focus-visible:ring-gray-50 focus-visible:ring-offset-gray-1000",
|
|
64
|
+
// Ivory Outline Quiet - subtle light outline (for dark backgrounds)
|
|
65
|
+
ivoryOutlineQuiet:
|
|
66
|
+
"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",
|
|
67
|
+
// Gray - gray filled button (for dark backgrounds)
|
|
68
|
+
gray: "bg-gray-800 text-gray-100 hover:bg-gray-700 active:bg-gray-600 border-transparent focus-visible:ring-gray-700 focus-visible:ring-offset-gray-1000",
|
|
69
|
+
// Themed - uses CSS custom properties for styling
|
|
70
|
+
themed:
|
|
71
|
+
"[background:var(--btn-bg)] [color:var(--btn-text)] [border-color:var(--btn-border-color,transparent)] hover:[background:var(--btn-bg-hover,var(--btn-bg))] active:[background:var(--btn-bg-active,var(--btn-bg-hover,var(--btn-bg)))]",
|
|
72
|
+
},
|
|
73
|
+
size: {
|
|
74
|
+
// Large button - uses primitive spacing tokens
|
|
75
|
+
lg: "px-spacing-24 py-spacing-12 typography-brand-large-button-large h-spacing-48",
|
|
76
|
+
// Medium button (default) - uses primitive spacing tokens
|
|
77
|
+
default:
|
|
78
|
+
"px-spacing-20 py-spacing-10 typography-brand-medium-button-medium h-spacing-40",
|
|
79
|
+
// Small button - uses primitive spacing tokens
|
|
80
|
+
sm: "px-spacing-16 py-spacing-8 typography-brand-small-button-small h-spacing-32",
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
defaultVariants: {
|
|
84
|
+
variant: "primary",
|
|
85
|
+
size: "default",
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
export type HTMLButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement>;
|
|
90
|
+
export type ButtonProps = BaseButtonProps &
|
|
91
|
+
VariantProps<typeof buttonVariants> &
|
|
92
|
+
HTMLButtonProps & {
|
|
93
|
+
/**
|
|
94
|
+
* Theme overrides for button styling via CSS custom properties
|
|
95
|
+
*/
|
|
96
|
+
theme?: ButtonTheme;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Check if a ButtonTheme has any actual values set
|
|
101
|
+
*/
|
|
102
|
+
function hasThemeValues(theme: ButtonTheme | undefined): boolean {
|
|
103
|
+
if (!theme) return false;
|
|
104
|
+
return Object.values(theme).some((v) => v !== undefined && v !== null);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
108
|
+
(
|
|
109
|
+
{ className, variant, size, render, nativeButton, theme, style, ...props },
|
|
110
|
+
ref,
|
|
111
|
+
) => {
|
|
112
|
+
// When render prop is provided, default nativeButton to false to suppress warnings
|
|
113
|
+
const isNativeButton = nativeButton ?? render === undefined;
|
|
114
|
+
|
|
115
|
+
// If theme has actual values, use "themed" variant to enable CSS custom property styling
|
|
116
|
+
const hasTheme = hasThemeValues(theme);
|
|
117
|
+
const effectiveVariant = hasTheme ? "themed" : variant;
|
|
118
|
+
const themeStyles = buttonThemeToStyleVars(theme);
|
|
119
|
+
const combinedStyles = hasTheme ? { ...themeStyles, ...style } : style;
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<BaseButton
|
|
123
|
+
className={buttonVariants({
|
|
124
|
+
variant: effectiveVariant,
|
|
125
|
+
size,
|
|
126
|
+
class: className,
|
|
127
|
+
})}
|
|
128
|
+
ref={ref}
|
|
129
|
+
render={render}
|
|
130
|
+
nativeButton={isNativeButton}
|
|
131
|
+
style={combinedStyles}
|
|
132
|
+
{...props}
|
|
133
|
+
/>
|
|
134
|
+
);
|
|
135
|
+
},
|
|
136
|
+
);
|
|
137
|
+
Button.displayName = "Button";
|
|
138
|
+
|
|
139
|
+
export { Button, buttonVariants };
|