@nationaldesignstudio/react 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/component-registry.md +1310 -127
  2. package/dist/components/atoms/button/button.d.ts +55 -47
  3. package/dist/components/atoms/button/button.figma.d.ts +1 -0
  4. package/dist/components/atoms/input/input.d.ts +24 -24
  5. package/dist/components/atoms/popover/popover.d.ts +195 -0
  6. package/dist/components/atoms/select/select.d.ts +24 -24
  7. package/dist/components/atoms/tooltip/tooltip.d.ts +161 -0
  8. package/dist/components/organisms/card/card.d.ts +1 -1
  9. package/dist/components/sections/hero/hero.d.ts +2 -2
  10. package/dist/components/sections/tout/tout.d.ts +3 -3
  11. package/dist/components/shared/floating-arrow.d.ts +34 -0
  12. package/dist/index.d.ts +8 -0
  13. package/dist/index.js +11602 -8499
  14. package/dist/index.js.map +1 -1
  15. package/dist/lib/form-control.d.ts +25 -24
  16. package/dist/tokens.css +4797 -3940
  17. package/package.json +2 -1
  18. package/src/components/atoms/accordion/accordion.stories.tsx +1 -1
  19. package/src/components/atoms/accordion/accordion.tsx +2 -2
  20. package/src/components/atoms/button/button.figma.tsx +37 -0
  21. package/src/components/atoms/button/button.stories.tsx +236 -140
  22. package/src/components/atoms/button/button.test.tsx +289 -5
  23. package/src/components/atoms/button/button.tsx +37 -33
  24. package/src/components/atoms/button/button.visual.test.tsx +26 -76
  25. package/src/components/atoms/button/icon-button.stories.tsx +44 -101
  26. package/src/components/atoms/button/icon-button.test.tsx +26 -94
  27. package/src/components/atoms/button/icon-button.tsx +3 -3
  28. package/src/components/atoms/input/input-group.stories.tsx +4 -8
  29. package/src/components/atoms/input/input-group.test.tsx +14 -28
  30. package/src/components/atoms/input/input-group.tsx +57 -32
  31. package/src/components/atoms/input/input.stories.tsx +14 -18
  32. package/src/components/atoms/input/input.test.tsx +4 -20
  33. package/src/components/atoms/input/input.tsx +16 -9
  34. package/src/components/atoms/pager-control/pager-control.stories.tsx +6 -8
  35. package/src/components/atoms/pager-control/pager-control.tsx +12 -12
  36. package/src/components/atoms/popover/index.ts +30 -0
  37. package/src/components/atoms/popover/popover.stories.tsx +531 -0
  38. package/src/components/atoms/popover/popover.test.tsx +486 -0
  39. package/src/components/atoms/popover/popover.tsx +488 -0
  40. package/src/components/atoms/select/select.tsx +12 -8
  41. package/src/components/atoms/tooltip/index.ts +24 -0
  42. package/src/components/atoms/tooltip/tooltip.stories.tsx +348 -0
  43. package/src/components/atoms/tooltip/tooltip.test.tsx +363 -0
  44. package/src/components/atoms/tooltip/tooltip.tsx +347 -0
  45. package/src/components/dev-tools/dev-toolbar/dev-toolbar.stories.tsx +8 -13
  46. package/src/components/dev-tools/dev-toolbar/dev-toolbar.tsx +3 -3
  47. package/src/components/organisms/card/card.stories.tsx +19 -19
  48. package/src/components/organisms/card/card.tsx +1 -1
  49. package/src/components/organisms/card/card.visual.test.tsx +11 -11
  50. package/src/components/organisms/navbar/navbar.visual.test.tsx +2 -2
  51. package/src/components/organisms/us-gov-banner/us-gov-banner.tsx +2 -2
  52. package/src/components/sections/banner/banner.stories.tsx +1 -5
  53. package/src/components/sections/banner/banner.test.tsx +2 -2
  54. package/src/components/sections/banner/banner.tsx +6 -6
  55. package/src/components/sections/card-grid/card-grid.tsx +4 -4
  56. package/src/components/sections/hero/hero.stories.tsx +7 -7
  57. package/src/components/sections/hero/hero.tsx +10 -11
  58. package/src/components/sections/prose/prose.tsx +2 -2
  59. package/src/components/sections/river/river.test.tsx +3 -3
  60. package/src/components/sections/river/river.tsx +6 -12
  61. package/src/components/sections/tout/tout.stories.tsx +7 -31
  62. package/src/components/sections/tout/tout.tsx +9 -9
  63. package/src/components/sections/two-column-section/two-column-section.tsx +7 -9
  64. package/src/components/shared/floating-arrow.tsx +78 -0
  65. package/src/components/shared/index.ts +5 -0
  66. package/src/index.ts +57 -0
  67. package/src/lib/form-control.ts +8 -6
  68. package/src/stories/grid-system.stories.tsx +309 -0
  69. package/src/stories/{ThemeProvider.stories.tsx → theme-provider.stories.tsx} +7 -19
  70. package/src/stories/{TokenShowcase.stories.tsx → token-showcase.stories.tsx} +1 -1
  71. package/src/stories/{TokenShowcase.tsx → token-showcase.tsx} +34 -34
  72. package/src/styles.css +3 -3
  73. package/src/tests/token-resolution.test.tsx +6 -9
  74. package/src/theme/hooks.ts +1 -1
  75. package/src/theme/index.ts +1 -1
  76. package/src/theme/theme-provider.test.tsx +270 -0
  77. package/src/theme/{ThemeProvider.tsx → theme-provider.tsx} +18 -2
  78. package/src/stories/GridSystem.stories.tsx +0 -84
  79. /package/src/stories/{Introduction.mdx → introduction.mdx} +0 -0
@@ -143,11 +143,7 @@ export const CustomBackground: Story = {
143
143
  <Banner
144
144
  heading="Custom Styled Banner"
145
145
  description="This banner has a custom background color applied via className."
146
- action={
147
- <Button variant="outline" colorScheme="light">
148
- Action
149
- </Button>
150
- }
146
+ action={<Button variant="secondary-outline">Action</Button>}
151
147
  className="bg-gray-1000 text-gray-50 [&_h2]:text-gray-50 [&_p]:text-gray-200"
152
148
  />
153
149
  ),
@@ -156,8 +156,8 @@ describe("Banner", () => {
156
156
 
157
157
  const banner = page.getByTestId("banner");
158
158
  // Mobile padding
159
- await expect.element(banner).toHaveClass(/px-spacing-20/);
160
- await expect.element(banner).toHaveClass(/py-spacing-32/);
159
+ await expect.element(banner).toHaveClass(/px-20/);
160
+ await expect.element(banner).toHaveClass(/py-32/);
161
161
  });
162
162
  });
163
163
 
@@ -21,9 +21,9 @@ const bannerVariants = tv({
21
21
  "col-full",
22
22
  // Responsive padding: mobile -> tablet -> desktop
23
23
  // Uses primitive spacing tokens
24
- "px-spacing-20 py-spacing-32",
25
- "md:p-spacing-56",
26
- "lg:px-spacing-72 lg:pb-spacing-72 lg:pt-0",
24
+ "px-20 py-32",
25
+ "md:p-56",
26
+ "lg:px-72 lg:pb-72 lg:pt-0",
27
27
  ],
28
28
  variants: {
29
29
  colorScheme: {
@@ -95,12 +95,12 @@ const Banner = React.forwardRef<HTMLElement, BannerProps>(
95
95
  <div
96
96
  className={cn(
97
97
  // Uses primitive spacing tokens
98
- "flex flex-col md:flex-row gap-spacing-20 items-start md:items-center md:justify-between",
99
- colorScheme === "dark" && "border-t border-gray-700 py-spacing-36",
98
+ "flex flex-col md:flex-row gap-20 items-start md:items-center md:justify-between",
99
+ colorScheme === "dark" && "border-t border-gray-700 py-36",
100
100
  )}
101
101
  >
102
102
  {/* Copy section */}
103
- <div className="flex flex-col gap-spacing-6 items-start">
103
+ <div className="flex flex-col gap-6 items-start">
104
104
  <h2
105
105
  className={cn(
106
106
  "typography-subheading-small",
@@ -16,9 +16,9 @@ const cardGridVariants = tv({
16
16
  base: [
17
17
  "grid-container",
18
18
  // Small (mobile): 72px y padding
19
- "py-spacing-72",
19
+ "py-72",
20
20
  // Large (desktop): 128px y padding
21
- "lg:py-spacing-128",
21
+ "lg:py-128",
22
22
  ],
23
23
  variants: {
24
24
  variant: {
@@ -85,7 +85,7 @@ const CardGrid = React.forwardRef<HTMLElement, CardGridProps>(
85
85
  "col-full",
86
86
  "typography-h4 text-gray-900",
87
87
  // Gap after title: mobile default, md: 56px, lg: 64px
88
- "mb-spacing-36 md:mb-spacing-56 lg:mb-spacing-64",
88
+ "mb-36 md:mb-56 lg:mb-64",
89
89
  )}
90
90
  >
91
91
  {title}
@@ -96,7 +96,7 @@ const CardGrid = React.forwardRef<HTMLElement, CardGridProps>(
96
96
  className={cn(
97
97
  "col-full",
98
98
  // Mobile: single column
99
- "grid grid-cols-1 gap-spacing-20",
99
+ "grid grid-cols-1 gap-20",
100
100
  // Tablet: 2 columns
101
101
  "md:grid-cols-2",
102
102
  // Desktop: 3 columns for variant A, 2 columns for variant B
@@ -159,7 +159,7 @@ export const WithCloudflareStream: Story = {
159
159
  * Mock banner component for story demonstration
160
160
  */
161
161
  const MockBanner = () => (
162
- <div className="bg-gray-1200 px-spacing-16 py-spacing-8 text-center text-14 text-text-inverted">
162
+ <div className="bg-gray-1200 px-16 py-8 text-center text-14 text-text-inverted">
163
163
  An official website of the United States government
164
164
  </div>
165
165
  );
@@ -168,10 +168,10 @@ const MockBanner = () => (
168
168
  * Mock navigation component for story demonstration
169
169
  */
170
170
  const MockNavigation = () => (
171
- <nav className="flex items-center justify-between px-spacing-56 py-spacing-16">
172
- <div className="flex items-center gap-spacing-24">
171
+ <nav className="flex items-center justify-between px-56 py-16">
172
+ <div className="flex items-center gap-24">
173
173
  <div className="size-48 rounded-full bg-gray-50" />
174
- <div className="flex gap-spacing-24 text-14 text-text-inverted">
174
+ <div className="flex gap-24 text-14 text-text-inverted">
175
175
  <span className="cursor-pointer hover:opacity-80">Link 1</span>
176
176
  <span className="cursor-pointer hover:opacity-80">Link 2</span>
177
177
  </div>
@@ -199,7 +199,7 @@ export const WithTopSlot: Story = {
199
199
  */
200
200
  export const WithRoundedCorners: Story = {
201
201
  render: () => (
202
- <div className="bg-bg-page p-spacing-32">
202
+ <div className="bg-bg-page p-32">
203
203
  <Hero
204
204
  variant="A1"
205
205
  title="Rounded Corners"
@@ -234,7 +234,7 @@ export const WithTopAndRoundedCorners: Story = {
234
234
  </>
235
235
  }
236
236
  >
237
- <p className="mt-spacing-16 max-w-[560px] text-20 text-text-inverted">
237
+ <p className="mt-16 max-w-[560px] text-20 text-text-inverted">
238
238
  A subtitle or description can be added as children
239
239
  </p>
240
240
  </Hero>
@@ -261,7 +261,7 @@ export const UsingSubComponents: Story = {
261
261
  <h1 className="text-64 font-medium text-text-inverted lg:text-128">
262
262
  Custom Layout
263
263
  </h1>
264
- <p className="mt-spacing-16 text-20 text-text-inverted">
264
+ <p className="mt-16 text-20 text-text-inverted">
265
265
  Using HeroHeader and HeroContent sub-components
266
266
  </p>
267
267
  </HeroContent>
@@ -18,8 +18,7 @@ export {
18
18
  type BackgroundVideoProps as HeroBackgroundVideoProps,
19
19
  } from "@/components/atoms/background";
20
20
 
21
- const DEFAULT_TITLE_TYPOGRAPHY =
22
- "text-64 leading-64 tracking-64 md:text-128 md:leading-128 md:tracking-128 lg:text-192 lg:leading-192 lg:tracking-192 font-medium";
21
+ const DEFAULT_TITLE_TYPOGRAPHY = "typography-h1-display";
23
22
 
24
23
  const heroVariants = tv({
25
24
  slots: {
@@ -27,8 +26,8 @@ const heroVariants = tv({
27
26
  top: "relative z-10 w-full",
28
27
  content: [
29
28
  "relative z-10 mx-auto flex w-full max-w-screen-xl flex-1 flex-col",
30
- "p-spacing-20",
31
- "md:p-spacing-56",
29
+ "p-20",
30
+ "md:p-56",
32
31
  ],
33
32
  title: DEFAULT_TITLE_TYPOGRAPHY,
34
33
  },
@@ -36,15 +35,15 @@ const heroVariants = tv({
36
35
  variant: {
37
36
  A1: {
38
37
  root: "min-h-[80vh]",
39
- content: ["justify-end", "lg:p-spacing-72"],
38
+ content: ["justify-end", "lg:p-72"],
40
39
  },
41
40
  A2: {
42
41
  root: "min-h-[80vh]",
43
- content: ["justify-start", "lg:p-spacing-64"],
42
+ content: ["justify-start", "lg:p-64"],
44
43
  },
45
44
  A3: {
46
45
  root: "min-h-[80vh]",
47
- content: ["items-center justify-center", "lg:p-spacing-64"],
46
+ content: ["items-center justify-center", "lg:p-64"],
48
47
  },
49
48
  },
50
49
  colorScheme: {
@@ -80,9 +79,9 @@ const heroHeaderVariants = tv({
80
79
  const heroContentVariants = tv({
81
80
  base: [
82
81
  "relative z-10 mx-auto flex w-full max-w-screen-xl flex-1 flex-col",
83
- "p-spacing-20",
84
- "md:p-spacing-56",
85
- "lg:p-spacing-72",
82
+ "p-20",
83
+ "md:p-56",
84
+ "lg:p-72",
86
85
  ],
87
86
  });
88
87
 
@@ -136,7 +135,7 @@ export interface HeroProps
136
135
  title?: string;
137
136
  /**
138
137
  * Custom typography class for the title using primitive tokens.
139
- * Default: "text-64 leading-64 tracking-64 md:text-128 md:leading-128 md:tracking-128 lg:text-192 lg:leading-192 lg:tracking-192"
138
+ * Default: "typography-h1-display"
140
139
  */
141
140
  titleClassName?: string;
142
141
  /**
@@ -23,7 +23,7 @@ const Prose = React.forwardRef<HTMLDivElement, ProseProps>(
23
23
  className={cn(
24
24
  "flex w-full max-w-[700px] flex-col overflow-hidden",
25
25
  // Responsive gap between sections
26
- "gap-spacing-56 md:gap-spacing-72 xl:gap-spacing-96",
26
+ "gap-56 md:gap-72 xl:gap-96",
27
27
  className,
28
28
  )}
29
29
  {...props}
@@ -67,7 +67,7 @@ const ProseSection = React.forwardRef<HTMLElement, ProseSectionProps>(
67
67
  className={cn(
68
68
  "flex w-full flex-col",
69
69
  // Responsive gap between heading and body
70
- "gap-spacing-24 md:gap-spacing-40",
70
+ "gap-24 md:gap-40",
71
71
  className,
72
72
  )}
73
73
  {...props}
@@ -177,9 +177,9 @@ describe("River", () => {
177
177
 
178
178
  const river = page.getByTestId("river");
179
179
  // Mobile padding
180
- await expect.element(river).toHaveClass(/px-spacing-20/);
181
- await expect.element(river).toHaveClass(/pt-spacing-72/);
182
- await expect.element(river).toHaveClass(/pb-spacing-20/);
180
+ await expect.element(river).toHaveClass(/px-20/);
181
+ await expect.element(river).toHaveClass(/pt-72/);
182
+ await expect.element(river).toHaveClass(/pb-20/);
183
183
  });
184
184
  });
185
185
 
@@ -16,11 +16,11 @@ const riverVariants = tv({
16
16
  base: [
17
17
  "col-full",
18
18
  // Small (mobile): 20px x, 72px top, 20px bottom
19
- "px-spacing-20 pt-spacing-72 pb-spacing-20",
19
+ "px-20 pt-72 pb-20",
20
20
  // Medium (tablet): 56px x, 96px y
21
- "md:px-spacing-56 md:py-spacing-96",
21
+ "md:px-56 md:py-96",
22
22
  // Large (desktop): 72px x, 128px y
23
- "lg:px-spacing-72 lg:py-spacing-128",
23
+ "lg:px-72 lg:py-128",
24
24
  ],
25
25
  variants: {
26
26
  variant: {
@@ -106,17 +106,14 @@ const River = React.forwardRef<HTMLElement, RiverProps>(
106
106
  )}
107
107
  >
108
108
  {/* Text content with 16px gap - uses primitive spacing tokens */}
109
- <div className="flex flex-col gap-spacing-16">
109
+ <div className="flex flex-col gap-16">
110
110
  <h2 className="typography-h4 text-gray-900">{headline}</h2>
111
111
  <p className="typography-body-small text-gray-800">{body}</p>
112
112
  </div>
113
113
 
114
114
  {/* Buttons with 36px gap from text, responsive sizes - uses primitive spacing tokens */}
115
115
  <div
116
- className={cn(
117
- "flex flex-row gap-spacing-16 mt-spacing-36",
118
- "[&>*]:flex-shrink-0",
119
- )}
116
+ className={cn("flex flex-row gap-16 mt-36", "[&>*]:flex-shrink-0")}
120
117
  >
121
118
  {primaryAction}
122
119
  {secondaryAction}
@@ -145,10 +142,7 @@ const River = React.forwardRef<HTMLElement, RiverProps>(
145
142
  >
146
143
  {/* Inner grid container for 24-col layout - uses primitive spacing tokens */}
147
144
  <div
148
- className={cn(
149
- "grid grid-cols-1 gap-spacing-36",
150
- "lg:grid-cols-24 lg:gap-spacing-36",
151
- )}
145
+ className={cn("grid grid-cols-1 gap-36", "lg:grid-cols-24 lg:gap-36")}
152
146
  >
153
147
  {variant === "B" ? (
154
148
  <>
@@ -43,11 +43,7 @@ Playground.args = {
43
43
  headline: "Brand-Large/Headline/Small",
44
44
  body: "A river pattern stacks content in a simple vertical flow: one clear heading, a short block of copy, then the next step. It's ideal for guiding citizens through a process or story, keeping focus moving straight down the page with minimal choices and well-timed calls to action.",
45
45
  primaryAction: <Button>Primary</Button>,
46
- secondaryAction: (
47
- <Button variant="outline" colorScheme="light">
48
- Secondary
49
- </Button>
50
- ),
46
+ secondaryAction: <Button variant="secondary-outline">Secondary</Button>,
51
47
  backgroundMedia: <PlaceholderBackground />,
52
48
  };
53
49
 
@@ -61,11 +57,7 @@ export const Desktop: Story = {
61
57
  headline="Brand-Large/Headline/Small"
62
58
  body="A river pattern stacks content in a simple vertical flow: one clear heading, a short block of copy, then the next step. It's ideal for guiding citizens through a process or story, keeping focus moving straight down the page with minimal choices and well-timed calls to action."
63
59
  primaryAction={<Button>Primary</Button>}
64
- secondaryAction={
65
- <Button variant="outline" colorScheme="light">
66
- Secondary
67
- </Button>
68
- }
60
+ secondaryAction={<Button variant="secondary-outline">Secondary</Button>}
69
61
  backgroundMedia={<PlaceholderBackground />}
70
62
  />
71
63
  ),
@@ -80,11 +72,7 @@ export const Tablet: Story = {
80
72
  headline="Brand-Large/Headline/Small"
81
73
  body="A river pattern stacks content in a simple vertical flow: one clear heading, a short block of copy, then the next step. It's ideal for guiding citizens through a process or story, keeping focus moving straight down the page with minimal choices and well-timed calls to action."
82
74
  primaryAction={<Button>Primary</Button>}
83
- secondaryAction={
84
- <Button variant="outline" colorScheme="light">
85
- Secondary
86
- </Button>
87
- }
75
+ secondaryAction={<Button variant="secondary-outline">Secondary</Button>}
88
76
  backgroundMedia={<PlaceholderBackground />}
89
77
  />
90
78
  ),
@@ -100,7 +88,7 @@ export const Mobile: Story = {
100
88
  body="A river pattern stacks content in a simple vertical flow: one clear heading, a short block of copy, then the next step. It's ideal for guiding citizens through a process or story, keeping focus moving straight down the page with minimal choices and well-timed calls to action."
101
89
  primaryAction={<Button size="sm">Primary</Button>}
102
90
  secondaryAction={
103
- <Button size="sm" variant="outline" colorScheme="light">
91
+ <Button size="sm" variant="secondary-outline">
104
92
  Secondary
105
93
  </Button>
106
94
  }
@@ -125,11 +113,7 @@ export const WithImage: Story = {
125
113
  headline="Work with Purpose"
126
114
  body="Join a team that's building the future of government services. We're looking for passionate individuals who want to make a difference."
127
115
  primaryAction={<Button>View Careers</Button>}
128
- secondaryAction={
129
- <Button variant="outline" colorScheme="light">
130
- Learn More
131
- </Button>
132
- }
116
+ secondaryAction={<Button variant="secondary-outline">Learn More</Button>}
133
117
  backgroundMedia={<ImageBackground />}
134
118
  />
135
119
  ),
@@ -158,11 +142,7 @@ export const WithVideoPlaceholder: Story = {
158
142
  headline="Experience Innovation"
159
143
  body="See how modern technology is transforming the way government serves its citizens."
160
144
  primaryAction={<Button>Watch Video</Button>}
161
- secondaryAction={
162
- <Button variant="outline" colorScheme="light">
163
- Learn More
164
- </Button>
165
- }
145
+ secondaryAction={<Button variant="secondary-outline">Learn More</Button>}
166
146
  backgroundMedia={
167
147
  <div className="absolute inset-0 bg-gray-800 flex items-center justify-center">
168
148
  <span className="text-gray-400 typography-body-small">
@@ -183,11 +163,7 @@ export const WithNdstudioFooter: Story = {
183
163
  headline="Work with Purpose"
184
164
  body="Join a team that's building the future of government services. We're looking for passionate individuals who want to make a difference."
185
165
  primaryAction={<Button>View Careers</Button>}
186
- secondaryAction={
187
- <Button variant="outline" colorScheme="light">
188
- Learn More
189
- </Button>
190
- }
166
+ secondaryAction={<Button variant="secondary-outline">Learn More</Button>}
191
167
  backgroundMedia={<ImageBackground />}
192
168
  footer={<NdstudioFooter />}
193
169
  />
@@ -93,9 +93,9 @@ export interface ToutProps
93
93
  *
94
94
  * This component is self-contained with its own grid.
95
95
  * Grid setup:
96
- * - Desktop (lg): 24 columns, gap-spacing-20, content spans 9 cols
97
- * - Tablet (md): 12 columns, gap-spacing-20, content spans 9 cols
98
- * - Mobile: 4 columns, gap-spacing-20, content spans all 4 cols
96
+ * - Desktop (lg): 24 columns, gap-20, content spans 9 cols
97
+ * - Tablet (md): 12 columns, gap-20, content spans 9 cols
98
+ * - Mobile: 4 columns, gap-20, content spans all 4 cols
99
99
  *
100
100
  * @example
101
101
  * ```tsx
@@ -163,7 +163,7 @@ const Tout = React.forwardRef<HTMLElement, ToutProps>(
163
163
  // Grid setup with responsive columns
164
164
  "grid w-full h-full",
165
165
  // Mobile: 4 columns with gap-20
166
- "grid-cols-4 gap-spacing-20",
166
+ "grid-cols-4 gap-20",
167
167
  // Tablet (md): 12 columns
168
168
  "md:grid-cols-12",
169
169
  // Desktop (lg): 24 columns
@@ -171,9 +171,9 @@ const Tout = React.forwardRef<HTMLElement, ToutProps>(
171
171
  // Max width and centering like grid-container
172
172
  "max-w-[var(--breakpoint-lg)] mx-auto",
173
173
  // Responsive margins matching grid-container - uses primitive spacing tokens
174
- "px-spacing-20 md:px-spacing-56 lg:px-spacing-72",
174
+ "px-20 md:px-56 lg:px-72",
175
175
  // Vertical padding to position content at bottom - uses primitive spacing tokens
176
- "py-spacing-36 md:py-spacing-56 lg:py-spacing-72",
176
+ "py-36 md:py-56 lg:py-72",
177
177
  )}
178
178
  style={{
179
179
  // Grid spacing theme overrides
@@ -193,7 +193,7 @@ const Tout = React.forwardRef<HTMLElement, ToutProps>(
193
193
  "flex flex-col",
194
194
  isCentered ? "justify-start items-center" : "justify-end",
195
195
  // Responsive gap between text and buttons - uses primitive spacing tokens
196
- "gap-spacing-28 md:gap-spacing-36",
196
+ "gap-28 md:gap-36",
197
197
  // Mobile: all 4 cols
198
198
  "col-span-4",
199
199
  // Tablet & Desktop: 9 cols left-aligned, full width centered
@@ -203,7 +203,7 @@ const Tout = React.forwardRef<HTMLElement, ToutProps>(
203
203
  {/* Text content stack - uses primitive spacing tokens */}
204
204
  <div
205
205
  className={cn(
206
- "flex flex-col gap-spacing-16",
206
+ "flex flex-col gap-16",
207
207
  isCentered && "items-center text-center",
208
208
  )}
209
209
  >
@@ -243,7 +243,7 @@ const Tout = React.forwardRef<HTMLElement, ToutProps>(
243
243
  "flex flex-row",
244
244
  isCentered ? "justify-center" : "items-start",
245
245
  // Responsive gap between buttons
246
- "gap-spacing-8 md:gap-spacing-12",
246
+ "gap-8 md:gap-12",
247
247
  "[&>*]:flex-shrink-0",
248
248
  )}
249
249
  >
@@ -16,11 +16,11 @@ const twoColumnSectionVariants = tv({
16
16
  base: [
17
17
  "w-full",
18
18
  // Small (mobile): 20px x, 56px top, 20px bottom
19
- "px-spacing-20 pt-spacing-56 pb-spacing-20",
19
+ "px-20 pt-56 pb-20",
20
20
  // Medium (tablet): 56px x, 56px y
21
- "md:px-spacing-56 md:py-spacing-56",
21
+ "md:px-56 md:py-56",
22
22
  // Large (desktop): 72px x, 72px top, 112px bottom
23
- "lg:px-spacing-72 lg:pt-spacing-72 lg:pb-spacing-112",
23
+ "lg:px-72 lg:pt-72 lg:pb-112",
24
24
  ],
25
25
  variants: {
26
26
  colorScheme: {
@@ -108,13 +108,11 @@ const TwoColumnSection = React.forwardRef<HTMLElement, TwoColumnSectionProps>(
108
108
  {/* Inner container with border-top - uses primitive spacing tokens */}
109
109
  <div
110
110
  className={cn(
111
- "border-t pt-spacing-36",
111
+ "border-t pt-36",
112
112
  colorScheme === "dark" ? "border-gray-700" : "border-gray-300",
113
113
  // Grid layout - uses primitive spacing tokens
114
- "grid grid-cols-1 gap-spacing-56",
115
- layout === "equal"
116
- ? "md:grid-cols-2"
117
- : "lg:grid-cols-24 lg:gap-spacing-56",
114
+ "grid grid-cols-1 gap-56",
115
+ layout === "equal" ? "md:grid-cols-2" : "lg:grid-cols-24 lg:gap-56",
118
116
  )}
119
117
  >
120
118
  {/* Title column */}
@@ -132,7 +130,7 @@ const TwoColumnSection = React.forwardRef<HTMLElement, TwoColumnSectionProps>(
132
130
  {/* Content column - uses primitive spacing tokens */}
133
131
  <div
134
132
  className={cn(
135
- "flex flex-col gap-spacing-56",
133
+ "flex flex-col gap-56",
136
134
  layout !== "equal" && "lg:col-span-15",
137
135
  )}
138
136
  >
@@ -0,0 +1,78 @@
1
+ import { tv } from "tailwind-variants";
2
+ import { cn } from "@/lib/utils";
3
+
4
+ /**
5
+ * Shared arrow variants for floating UI components
6
+ *
7
+ * Used by Tooltip, Popover, and other floating components.
8
+ * Handles positioning based on the side attribute.
9
+ */
10
+ export const floatingArrowVariants = tv({
11
+ base: [
12
+ // Display flex to properly size the arrow container
13
+ "flex",
14
+ // Positioning based on floating side (uses spacing tokens since --spacing: 1px)
15
+ "data-[side=bottom]:-top-7",
16
+ "data-[side=left]:-right-12 data-[side=left]:rotate-90",
17
+ "data-[side=right]:-left-12 data-[side=right]:-rotate-90",
18
+ "data-[side=top]:-bottom-7 data-[side=top]:rotate-180",
19
+ // Animation - follows popup
20
+ "data-[starting-style]:opacity-0",
21
+ "data-[ending-style]:opacity-0",
22
+ ],
23
+ });
24
+
25
+ export interface FloatingArrowSvgProps {
26
+ /** CSS class for the main fill color (e.g., "fill-tooltip-bg" or "fill-overlay-background") */
27
+ fillClassName: string;
28
+ /** CSS class for the border color using fill-* (e.g., "fill-overlay-border") - renders as outline behind main fill */
29
+ borderClassName?: string;
30
+ /** Additional className for the SVG element */
31
+ className?: string;
32
+ }
33
+
34
+ /**
35
+ * FloatingArrowSvg
36
+ *
37
+ * A shared arrow SVG component for floating UI elements.
38
+ * Use with Tooltip, Popover, Dropdown, and other floating components.
39
+ *
40
+ * @example
41
+ * ```tsx
42
+ * // For dark tooltip (no border)
43
+ * <FloatingArrowSvg fillClassName="fill-tooltip-bg" />
44
+ *
45
+ * // For light popover with border
46
+ * <FloatingArrowSvg
47
+ * fillClassName="fill-overlay-background"
48
+ * borderClassName="fill-overlay-border"
49
+ * />
50
+ * ```
51
+ */
52
+ export const FloatingArrowSvg = ({
53
+ fillClassName,
54
+ borderClassName,
55
+ className,
56
+ }: FloatingArrowSvgProps) => (
57
+ <svg
58
+ width="20"
59
+ height="10"
60
+ viewBox="0 0 20 10"
61
+ fill="none"
62
+ className={cn("block", className)}
63
+ aria-hidden="true"
64
+ >
65
+ {/* Main fill shape */}
66
+ <path
67
+ d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z"
68
+ className={fillClassName}
69
+ />
70
+ {/* Border stroke (rendered on top for outline effect) */}
71
+ {borderClassName && (
72
+ <path
73
+ d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z"
74
+ className={borderClassName}
75
+ />
76
+ )}
77
+ </svg>
78
+ );
@@ -0,0 +1,5 @@
1
+ export {
2
+ FloatingArrowSvg,
3
+ type FloatingArrowSvgProps,
4
+ floatingArrowVariants,
5
+ } from "./floating-arrow";
package/src/index.ts CHANGED
@@ -1,3 +1,8 @@
1
+ /**
2
+ * @nationaldesignstudio/react
3
+ * Design system components for React applications
4
+ */
5
+
1
6
  // =============================================================================
2
7
  // Atoms
3
8
  // =============================================================================
@@ -63,6 +68,35 @@ export {
63
68
  PagerControl,
64
69
  pagerControlVariants,
65
70
  } from "./components/atoms/pager-control";
71
+ export type {
72
+ PopoverArrowProps,
73
+ PopoverBackdropProps,
74
+ PopoverCloseProps,
75
+ PopoverDescriptionProps,
76
+ PopoverPopupProps,
77
+ PopoverPortalProps,
78
+ PopoverPositionerProps,
79
+ PopoverProps,
80
+ PopoverRootProps,
81
+ PopoverTitleProps,
82
+ PopoverTriggerProps,
83
+ } from "./components/atoms/popover";
84
+ export {
85
+ Popover,
86
+ PopoverArrow,
87
+ PopoverBackdrop,
88
+ PopoverClose,
89
+ PopoverDescription,
90
+ PopoverParts,
91
+ PopoverPopup,
92
+ PopoverPortal,
93
+ PopoverPositioner,
94
+ PopoverRoot,
95
+ PopoverTitle,
96
+ PopoverTrigger,
97
+ popoverArrowVariants,
98
+ popoverPopupVariants,
99
+ } from "./components/atoms/popover";
66
100
  export type {
67
101
  SelectGroupLabelProps,
68
102
  SelectGroupProps,
@@ -83,6 +117,29 @@ export {
83
117
  selectPopupVariants,
84
118
  selectTriggerVariants,
85
119
  } from "./components/atoms/select";
120
+ export type {
121
+ TooltipArrowProps,
122
+ TooltipPopupProps,
123
+ TooltipPortalProps,
124
+ TooltipPositionerProps,
125
+ TooltipProps,
126
+ TooltipProviderProps,
127
+ TooltipRootProps,
128
+ TooltipTriggerProps,
129
+ } from "./components/atoms/tooltip";
130
+ export {
131
+ Tooltip,
132
+ TooltipArrow,
133
+ TooltipParts,
134
+ TooltipPopup,
135
+ TooltipPortal,
136
+ TooltipPositioner,
137
+ TooltipProvider,
138
+ TooltipRoot,
139
+ TooltipTrigger,
140
+ tooltipArrowVariants,
141
+ tooltipPopupVariants,
142
+ } from "./components/atoms/tooltip";
86
143
  // =============================================================================
87
144
  // Dev Tools
88
145
  // =============================================================================
@@ -19,14 +19,14 @@ export const formControlBase = [
19
19
  "flex w-full items-center",
20
20
  // Typography
21
21
  "text-16 font-medium leading-14",
22
- // Border and radius
23
- "border border-solid border-ui-color-border rounded-radius-6",
22
+ // Border and radius - uses surface ui radius for theming support
23
+ "border border-solid border-ui-color-border rounded-surface-ui-medium",
24
24
  // Background
25
25
  "bg-ui-control-background",
26
26
  // Transitions
27
27
  "transition-[background-color,border-color,box-shadow] duration-150",
28
28
  // Focus state
29
- "outline-none focus-visible:border-ui-accent-base focus-visible:ring-4 focus-visible:ring-ui-color-focus",
29
+ "outline-none focus-visible:border-border-focus focus-visible:ring-4 focus-visible:ring-ui-color-focus",
30
30
  // Hover state (when not focused or disabled)
31
31
  "hover:bg-ui-control-background-hover hover:focus-visible:bg-ui-control-background",
32
32
  // Disabled state
@@ -35,11 +35,13 @@ export const formControlBase = [
35
35
 
36
36
  /**
37
37
  * Size variants shared by form controls
38
+ * Uses spatial tokens for consistent sizing across form controls
38
39
  */
39
40
  export const formControlSizes = {
40
- sm: "h-36 px-12 py-8 text-14",
41
- default: "h-48 px-16 py-10",
42
- lg: "h-56 px-20 py-12 text-18",
41
+ sm: "h-spatial-ui-control-height-small px-spatial-ui-control-padding-x-small py-spatial-ui-control-padding-y-small text-14",
42
+ default:
43
+ "h-spatial-ui-control-height-medium px-spatial-ui-control-padding-x-medium py-spatial-ui-control-padding-y-medium",
44
+ lg: "h-spatial-ui-control-height-large px-spatial-ui-control-padding-x-large py-spatial-ui-control-padding-y-large text-18",
43
45
  } as const;
44
46
 
45
47
  /**