@nationaldesignstudio/react 0.2.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 (97) hide show
  1. package/dist/component-registry.md +1310 -127
  2. package/dist/components/atoms/background/background.d.ts +13 -27
  3. package/dist/components/atoms/button/button.d.ts +64 -72
  4. package/dist/components/atoms/button/button.figma.d.ts +1 -0
  5. package/dist/components/atoms/button/icon-button.d.ts +62 -110
  6. package/dist/components/atoms/input/input-group.d.ts +278 -0
  7. package/dist/components/atoms/input/input.d.ts +121 -0
  8. package/dist/components/atoms/popover/popover.d.ts +195 -0
  9. package/dist/components/atoms/select/select.d.ts +131 -0
  10. package/dist/components/atoms/tooltip/tooltip.d.ts +161 -0
  11. package/dist/components/organisms/card/card.d.ts +3 -3
  12. package/dist/components/sections/hero/hero.d.ts +2 -2
  13. package/dist/components/sections/prose/prose.d.ts +3 -3
  14. package/dist/components/sections/river/river.d.ts +1 -1
  15. package/dist/components/sections/tout/tout.d.ts +4 -4
  16. package/dist/components/shared/floating-arrow.d.ts +34 -0
  17. package/dist/index.d.ts +12 -0
  18. package/dist/index.js +13935 -7622
  19. package/dist/index.js.map +1 -1
  20. package/dist/lib/form-control.d.ts +106 -0
  21. package/dist/tokens.css +4725 -19065
  22. package/package.json +2 -1
  23. package/src/components/atoms/accordion/accordion.stories.tsx +1 -1
  24. package/src/components/atoms/accordion/accordion.tsx +2 -2
  25. package/src/components/atoms/background/background.tsx +71 -109
  26. package/src/components/atoms/button/button.figma.tsx +37 -0
  27. package/src/components/atoms/button/button.stories.tsx +253 -115
  28. package/src/components/atoms/button/button.test.tsx +289 -5
  29. package/src/components/atoms/button/button.tsx +40 -101
  30. package/src/components/atoms/button/button.visual.test.tsx +28 -32
  31. package/src/components/atoms/button/icon-button.stories.tsx +44 -101
  32. package/src/components/atoms/button/icon-button.test.tsx +26 -94
  33. package/src/components/atoms/button/icon-button.tsx +81 -224
  34. package/src/components/atoms/input/index.ts +17 -0
  35. package/src/components/atoms/input/input-group.stories.tsx +646 -0
  36. package/src/components/atoms/input/input-group.test.tsx +362 -0
  37. package/src/components/atoms/input/input-group.tsx +409 -0
  38. package/src/components/atoms/input/input.stories.tsx +228 -0
  39. package/src/components/atoms/input/input.test.tsx +167 -0
  40. package/src/components/atoms/input/input.tsx +104 -0
  41. package/src/components/atoms/pager-control/pager-control.stories.tsx +6 -8
  42. package/src/components/atoms/pager-control/pager-control.tsx +12 -12
  43. package/src/components/atoms/popover/index.ts +30 -0
  44. package/src/components/atoms/popover/popover.stories.tsx +531 -0
  45. package/src/components/atoms/popover/popover.test.tsx +486 -0
  46. package/src/components/atoms/popover/popover.tsx +488 -0
  47. package/src/components/atoms/select/index.ts +18 -0
  48. package/src/components/atoms/select/select.stories.tsx +455 -0
  49. package/src/components/atoms/select/select.tsx +324 -0
  50. package/src/components/atoms/tooltip/index.ts +24 -0
  51. package/src/components/atoms/tooltip/tooltip.stories.tsx +348 -0
  52. package/src/components/atoms/tooltip/tooltip.test.tsx +363 -0
  53. package/src/components/atoms/tooltip/tooltip.tsx +347 -0
  54. package/src/components/dev-tools/dev-toolbar/dev-toolbar.stories.tsx +8 -17
  55. package/src/components/dev-tools/dev-toolbar/dev-toolbar.tsx +3 -3
  56. package/src/components/foundation/typography/typography.stories.tsx +401 -0
  57. package/src/components/organisms/card/card.stories.tsx +19 -19
  58. package/src/components/organisms/card/card.test.tsx +1 -1
  59. package/src/components/organisms/card/card.tsx +3 -3
  60. package/src/components/organisms/card/card.visual.test.tsx +11 -11
  61. package/src/components/organisms/navbar/navbar.tsx +2 -2
  62. package/src/components/organisms/navbar/navbar.visual.test.tsx +2 -2
  63. package/src/components/organisms/us-gov-banner/us-gov-banner.tsx +2 -2
  64. package/src/components/sections/banner/banner.stories.tsx +1 -5
  65. package/src/components/sections/banner/banner.test.tsx +2 -2
  66. package/src/components/sections/banner/banner.tsx +6 -6
  67. package/src/components/sections/card-grid/card-grid.tsx +5 -5
  68. package/src/components/sections/faq-section/faq-section.tsx +2 -2
  69. package/src/components/sections/hero/hero.stories.tsx +7 -7
  70. package/src/components/sections/hero/hero.test.tsx +5 -5
  71. package/src/components/sections/hero/hero.tsx +10 -11
  72. package/src/components/sections/prose/prose.test.tsx +2 -2
  73. package/src/components/sections/prose/prose.tsx +6 -7
  74. package/src/components/sections/river/river.stories.tsx +8 -8
  75. package/src/components/sections/river/river.test.tsx +4 -4
  76. package/src/components/sections/river/river.tsx +8 -16
  77. package/src/components/sections/tout/tout.stories.tsx +7 -31
  78. package/src/components/sections/tout/tout.test.tsx +1 -1
  79. package/src/components/sections/tout/tout.tsx +11 -11
  80. package/src/components/sections/two-column-section/two-column-section.tsx +7 -9
  81. package/src/components/shared/floating-arrow.tsx +78 -0
  82. package/src/components/shared/index.ts +5 -0
  83. package/src/index.ts +98 -0
  84. package/src/lib/form-control.ts +71 -0
  85. package/src/stories/grid-system.stories.tsx +309 -0
  86. package/src/stories/{Introduction.mdx → introduction.mdx} +29 -15
  87. package/src/stories/{ThemeProvider.stories.tsx → theme-provider.stories.tsx} +8 -22
  88. package/src/stories/{TokenShowcase.stories.tsx → token-showcase.stories.tsx} +1 -20
  89. package/src/stories/token-showcase.tsx +777 -0
  90. package/src/styles.css +3 -0
  91. package/src/tests/token-resolution.test.tsx +298 -0
  92. package/src/theme/hooks.ts +1 -1
  93. package/src/theme/index.ts +1 -1
  94. package/src/theme/theme-provider.test.tsx +270 -0
  95. package/src/theme/{ThemeProvider.tsx → theme-provider.tsx} +18 -2
  96. package/src/stories/GridSystem.stories.tsx +0 -84
  97. package/src/stories/TokenShowcase.tsx +0 -1429
@@ -0,0 +1,363 @@
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
+ import {
6
+ Tooltip,
7
+ TooltipArrow,
8
+ TooltipParts,
9
+ TooltipPopup,
10
+ TooltipPortal,
11
+ TooltipPositioner,
12
+ TooltipProvider,
13
+ TooltipTrigger,
14
+ } from "./tooltip";
15
+
16
+ describe("Tooltip", () => {
17
+ describe("Accessibility", () => {
18
+ test("trigger element is accessible", async () => {
19
+ render(
20
+ <Tooltip content="Helpful tip">
21
+ <Button>Hover me</Button>
22
+ </Tooltip>,
23
+ );
24
+ await expect
25
+ .element(page.getByRole("button", { name: "Hover me" }))
26
+ .toBeInTheDocument();
27
+ });
28
+
29
+ test("tooltip has correct role when visible", async () => {
30
+ render(
31
+ <Tooltip content="Helpful tip" defaultOpen>
32
+ <Button>Hover me</Button>
33
+ </Tooltip>,
34
+ );
35
+ await expect
36
+ .element(page.getByRole("tooltip", { name: "Helpful tip" }))
37
+ .toBeInTheDocument();
38
+ });
39
+
40
+ test("trigger element is focusable", async () => {
41
+ render(
42
+ <Tooltip content="Helpful tip">
43
+ <Button>Focus me</Button>
44
+ </Tooltip>,
45
+ );
46
+ await userEvent.keyboard("{Tab}");
47
+ await expect
48
+ .element(page.getByRole("button", { name: "Focus me" }))
49
+ .toHaveFocus();
50
+ });
51
+ });
52
+
53
+ describe("Interactions", () => {
54
+ test("shows tooltip on hover", async () => {
55
+ render(
56
+ <Tooltip content="Hover tooltip">
57
+ <Button>Hover me</Button>
58
+ </Tooltip>,
59
+ );
60
+
61
+ // Tooltip should not be visible initially
62
+ await expect
63
+ .element(page.getByRole("tooltip", { name: "Hover tooltip" }))
64
+ .not.toBeInTheDocument();
65
+
66
+ // Hover over the trigger
67
+ await page.getByRole("button", { name: "Hover me" }).hover();
68
+
69
+ // Tooltip should now be visible
70
+ await expect
71
+ .element(page.getByRole("tooltip", { name: "Hover tooltip" }))
72
+ .toBeInTheDocument();
73
+ });
74
+
75
+ test("shows tooltip on focus", async () => {
76
+ render(
77
+ <Tooltip content="Focus tooltip">
78
+ <Button>Focus me</Button>
79
+ </Tooltip>,
80
+ );
81
+
82
+ // Tooltip should not be visible initially
83
+ await expect
84
+ .element(page.getByRole("tooltip", { name: "Focus tooltip" }))
85
+ .not.toBeInTheDocument();
86
+
87
+ // Focus the trigger
88
+ page.getByRole("button", { name: "Focus me" }).element().focus();
89
+
90
+ // Tooltip should now be visible
91
+ await expect
92
+ .element(page.getByRole("tooltip", { name: "Focus tooltip" }))
93
+ .toBeInTheDocument();
94
+ });
95
+
96
+ test("hides tooltip on blur", async () => {
97
+ render(
98
+ <>
99
+ <Tooltip content="Focus tooltip">
100
+ <Button>Focus me</Button>
101
+ </Tooltip>
102
+ <Button>Other button</Button>
103
+ </>,
104
+ );
105
+
106
+ // Focus the trigger to show tooltip
107
+ page.getByRole("button", { name: "Focus me" }).element().focus();
108
+ await expect
109
+ .element(page.getByRole("tooltip", { name: "Focus tooltip" }))
110
+ .toBeInTheDocument();
111
+
112
+ // Blur by focusing another element
113
+ page.getByRole("button", { name: "Other button" }).element().focus();
114
+
115
+ // Tooltip should be hidden
116
+ await expect
117
+ .element(page.getByRole("tooltip", { name: "Focus tooltip" }))
118
+ .not.toBeInTheDocument();
119
+ });
120
+ });
121
+
122
+ describe("Controlled State", () => {
123
+ test("respects controlled open state", async () => {
124
+ render(
125
+ <Tooltip content="Controlled tooltip" open={true}>
126
+ <Button>Trigger</Button>
127
+ </Tooltip>,
128
+ );
129
+
130
+ // Tooltip should be visible when open=true
131
+ await expect
132
+ .element(page.getByRole("tooltip", { name: "Controlled tooltip" }))
133
+ .toBeInTheDocument();
134
+ });
135
+
136
+ test("respects controlled closed state", async () => {
137
+ render(
138
+ <Tooltip content="Controlled tooltip" open={false}>
139
+ <Button>Trigger</Button>
140
+ </Tooltip>,
141
+ );
142
+
143
+ // Tooltip should not be visible when open=false
144
+ await expect
145
+ .element(page.getByRole("tooltip", { name: "Controlled tooltip" }))
146
+ .not.toBeInTheDocument();
147
+
148
+ // Should not show on hover when controlled
149
+ await page.getByRole("button", { name: "Trigger" }).hover();
150
+ await expect
151
+ .element(page.getByRole("tooltip", { name: "Controlled tooltip" }))
152
+ .not.toBeInTheDocument();
153
+ });
154
+
155
+ test("calls onOpenChange callback", async () => {
156
+ const handleOpenChange = vi.fn();
157
+ render(
158
+ <Tooltip content="Callback tooltip" onOpenChange={handleOpenChange}>
159
+ <Button>Trigger</Button>
160
+ </Tooltip>,
161
+ );
162
+
163
+ // Hover to trigger open
164
+ await page.getByRole("button", { name: "Trigger" }).hover();
165
+
166
+ // Wait for tooltip to appear (ensures callback has been called)
167
+ await expect
168
+ .element(page.getByRole("tooltip", { name: "Callback tooltip" }))
169
+ .toBeInTheDocument();
170
+
171
+ // Callback should have been called with true as first argument
172
+ // (Base UI passes additional context as second argument)
173
+ expect(handleOpenChange).toHaveBeenCalled();
174
+ expect(handleOpenChange.mock.calls[0][0]).toBe(true);
175
+ });
176
+
177
+ test("defaultOpen shows tooltip initially", async () => {
178
+ render(
179
+ <Tooltip content="Default open tooltip" defaultOpen>
180
+ <Button>Trigger</Button>
181
+ </Tooltip>,
182
+ );
183
+
184
+ // Tooltip should be visible initially
185
+ await expect
186
+ .element(page.getByRole("tooltip", { name: "Default open tooltip" }))
187
+ .toBeInTheDocument();
188
+ });
189
+ });
190
+
191
+ describe("Content", () => {
192
+ test("displays text content", async () => {
193
+ render(
194
+ <Tooltip content="Simple text" defaultOpen>
195
+ <Button>Trigger</Button>
196
+ </Tooltip>,
197
+ );
198
+
199
+ await expect.element(page.getByText("Simple text")).toBeInTheDocument();
200
+ });
201
+
202
+ test("displays React node content", async () => {
203
+ render(
204
+ <Tooltip
205
+ content={
206
+ <span data-testid="custom-content">
207
+ <strong>Bold</strong> text
208
+ </span>
209
+ }
210
+ defaultOpen
211
+ >
212
+ <Button>Trigger</Button>
213
+ </Tooltip>,
214
+ );
215
+
216
+ await expect
217
+ .element(page.getByTestId("custom-content"))
218
+ .toBeInTheDocument();
219
+ await expect.element(page.getByText("Bold")).toBeInTheDocument();
220
+ });
221
+ });
222
+
223
+ describe("Arrow", () => {
224
+ test("shows arrow by default", async () => {
225
+ render(
226
+ <Tooltip content="With arrow" defaultOpen>
227
+ <Button>Trigger</Button>
228
+ </Tooltip>,
229
+ );
230
+
231
+ // The arrow is an SVG element rendered by Base UI
232
+ const tooltip = page.getByRole("tooltip");
233
+ await expect.element(tooltip).toBeInTheDocument();
234
+ });
235
+
236
+ test("hides arrow when showArrow is false", async () => {
237
+ render(
238
+ <Tooltip content="Without arrow" defaultOpen showArrow={false}>
239
+ <Button>Trigger</Button>
240
+ </Tooltip>,
241
+ );
242
+
243
+ const tooltip = page.getByRole("tooltip");
244
+ await expect.element(tooltip).toBeInTheDocument();
245
+ });
246
+ });
247
+
248
+ describe("Compound Components", () => {
249
+ test("renders with compound component API", async () => {
250
+ render(
251
+ <TooltipParts defaultOpen>
252
+ <TooltipTrigger>
253
+ <Button>Compound trigger</Button>
254
+ </TooltipTrigger>
255
+ <TooltipPortal>
256
+ <TooltipPositioner>
257
+ <TooltipPopup>
258
+ <TooltipArrow />
259
+ Compound tooltip content
260
+ </TooltipPopup>
261
+ </TooltipPositioner>
262
+ </TooltipPortal>
263
+ </TooltipParts>,
264
+ );
265
+
266
+ await expect
267
+ .element(page.getByRole("button", { name: "Compound trigger" }))
268
+ .toBeInTheDocument();
269
+ await expect
270
+ .element(page.getByText("Compound tooltip content"))
271
+ .toBeInTheDocument();
272
+ });
273
+ });
274
+
275
+ describe("Provider", () => {
276
+ test("TooltipProvider wraps children", async () => {
277
+ render(
278
+ <TooltipProvider>
279
+ <Tooltip content="Provider tooltip" defaultOpen>
280
+ <Button>Trigger</Button>
281
+ </Tooltip>
282
+ </TooltipProvider>,
283
+ );
284
+
285
+ await expect
286
+ .element(page.getByRole("tooltip", { name: "Provider tooltip" }))
287
+ .toBeInTheDocument();
288
+ });
289
+ });
290
+
291
+ describe("Styles", () => {
292
+ test("tooltip popup has dark background", async () => {
293
+ render(
294
+ <Tooltip content="Styled tooltip" defaultOpen>
295
+ <Button>Trigger</Button>
296
+ </Tooltip>,
297
+ );
298
+
299
+ const tooltip = page.getByRole("tooltip");
300
+ await expect.element(tooltip).toBeInTheDocument();
301
+
302
+ const element = tooltip.element();
303
+ const styles = window.getComputedStyle(element);
304
+
305
+ // Tooltip should have a light background (not transparent)
306
+ expect(styles.backgroundColor).not.toBe("rgba(0, 0, 0, 0)");
307
+ expect(styles.backgroundColor).not.toBe("transparent");
308
+ });
309
+
310
+ test("tooltip has border radius", async () => {
311
+ render(
312
+ <Tooltip content="Styled tooltip" defaultOpen>
313
+ <Button>Trigger</Button>
314
+ </Tooltip>,
315
+ );
316
+
317
+ const tooltip = page.getByRole("tooltip");
318
+ await expect.element(tooltip).toBeInTheDocument();
319
+
320
+ const element = tooltip.element();
321
+ const styles = window.getComputedStyle(element);
322
+
323
+ // Should have border radius
324
+ const borderRadius = parseFloat(styles.borderRadius);
325
+ expect(borderRadius).toBeGreaterThan(0);
326
+ });
327
+
328
+ test("tooltip has shadow", async () => {
329
+ render(
330
+ <Tooltip content="Styled tooltip" defaultOpen>
331
+ <Button>Trigger</Button>
332
+ </Tooltip>,
333
+ );
334
+
335
+ const tooltip = page.getByRole("tooltip");
336
+ await expect.element(tooltip).toBeInTheDocument();
337
+
338
+ const element = tooltip.element();
339
+ const styles = window.getComputedStyle(element);
340
+
341
+ // Should have a box shadow
342
+ expect(styles.boxShadow).not.toBe("none");
343
+ });
344
+
345
+ test("tooltip has high z-index", async () => {
346
+ render(
347
+ <Tooltip content="Styled tooltip" defaultOpen>
348
+ <Button>Trigger</Button>
349
+ </Tooltip>,
350
+ );
351
+
352
+ const tooltip = page.getByRole("tooltip");
353
+ await expect.element(tooltip).toBeInTheDocument();
354
+
355
+ const element = tooltip.element();
356
+ const styles = window.getComputedStyle(element);
357
+
358
+ // Should have z-index of 50 or higher
359
+ const zIndex = parseInt(styles.zIndex, 10);
360
+ expect(zIndex).toBeGreaterThanOrEqual(50);
361
+ });
362
+ });
363
+ });
@@ -0,0 +1,347 @@
1
+ "use client";
2
+
3
+ import { Tooltip as BaseTooltip } from "@base-ui-components/react/tooltip";
4
+ import * as React from "react";
5
+ import { tv, type VariantProps } from "tailwind-variants";
6
+ import { cn } from "@/lib/utils";
7
+ import {
8
+ FloatingArrowSvg,
9
+ floatingArrowVariants,
10
+ } from "../../shared/floating-arrow";
11
+
12
+ /**
13
+ * Tooltip popup variants
14
+ *
15
+ * Uses semantic tokens for themeable styling:
16
+ * - color.tooltip.bg - Dark background
17
+ * - color.tooltip.text - Light text
18
+ * - surface.tooltip.radius - Small border radius
19
+ * - spatial.component.tooltip.padding-x/y - Consistent padding
20
+ */
21
+ const tooltipPopupVariants = tv({
22
+ base: [
23
+ // Layout - uses component tooltip tokens
24
+ "px-spatial-component-tooltip-padding-x py-spatial-component-tooltip-padding-y",
25
+ // Background and text - uses tooltip color tokens
26
+ "bg-tooltip-bg text-tooltip-text",
27
+ // Border radius - uses surface tooltip token
28
+ "rounded-surface-tooltip",
29
+ // Typography - uses semantic body text composite
30
+ "typography-body-sm-md font-medium",
31
+ // Shadow for elevation
32
+ "shadow-md",
33
+ // Allow arrow to extend outside popup bounds
34
+ "overflow-visible",
35
+ // Animation
36
+ "origin-[var(--transform-origin)]",
37
+ "transition-[transform,scale,opacity] duration-150",
38
+ "data-[starting-style]:scale-95 data-[starting-style]:opacity-0",
39
+ "data-[ending-style]:scale-95 data-[ending-style]:opacity-0",
40
+ // Ensure it's above other content
41
+ "z-50",
42
+ ],
43
+ variants: {
44
+ variant: {
45
+ default: "",
46
+ // Future variants can be added here (e.g., light, primary)
47
+ },
48
+ },
49
+ defaultVariants: {
50
+ variant: "default",
51
+ },
52
+ });
53
+
54
+ /**
55
+ * Tooltip arrow variants - uses shared floating arrow variants
56
+ */
57
+ const tooltipArrowVariants = floatingArrowVariants;
58
+
59
+ // ============================================================================
60
+ // Tooltip Provider
61
+ // ============================================================================
62
+
63
+ export interface TooltipProviderProps extends BaseTooltip.Provider.Props {
64
+ children: React.ReactNode;
65
+ }
66
+
67
+ /**
68
+ * Tooltip Provider
69
+ *
70
+ * Manages shared delays across multiple tooltips.
71
+ * Wrap your app or a section with this to enable tooltip delay grouping.
72
+ */
73
+ const TooltipProvider = ({ children, ...props }: TooltipProviderProps) => {
74
+ return <BaseTooltip.Provider {...props}>{children}</BaseTooltip.Provider>;
75
+ };
76
+
77
+ // ============================================================================
78
+ // Tooltip Root
79
+ // ============================================================================
80
+
81
+ export interface TooltipRootProps extends BaseTooltip.Root.Props {
82
+ children: React.ReactNode;
83
+ }
84
+
85
+ /**
86
+ * Tooltip Root
87
+ *
88
+ * Groups all tooltip parts. Does not render an element.
89
+ */
90
+ const TooltipRoot = ({ children, ...props }: TooltipRootProps) => {
91
+ return <BaseTooltip.Root {...props}>{children}</BaseTooltip.Root>;
92
+ };
93
+
94
+ // ============================================================================
95
+ // Tooltip Trigger
96
+ // ============================================================================
97
+
98
+ export interface TooltipTriggerProps
99
+ extends React.ComponentProps<typeof BaseTooltip.Trigger> {
100
+ className?: string;
101
+ }
102
+
103
+ /**
104
+ * Tooltip Trigger
105
+ *
106
+ * The element that triggers the tooltip on hover/focus.
107
+ * Renders as the child element with tooltip behavior attached.
108
+ * When children is a single React element, uses `render` prop to avoid wrapper element.
109
+ */
110
+ const TooltipTrigger = React.forwardRef<HTMLButtonElement, TooltipTriggerProps>(
111
+ ({ className, children, ...props }, ref) => {
112
+ // If children is a single React element, use render prop to avoid wrapper
113
+ const isSingleElement = React.isValidElement(children);
114
+
115
+ if (isSingleElement) {
116
+ return (
117
+ <BaseTooltip.Trigger
118
+ ref={ref}
119
+ className={className}
120
+ render={children}
121
+ {...props}
122
+ />
123
+ );
124
+ }
125
+
126
+ return (
127
+ <BaseTooltip.Trigger ref={ref} className={className} {...props}>
128
+ {children}
129
+ </BaseTooltip.Trigger>
130
+ );
131
+ },
132
+ );
133
+ TooltipTrigger.displayName = "TooltipTrigger";
134
+
135
+ // ============================================================================
136
+ // Tooltip Portal
137
+ // ============================================================================
138
+
139
+ export interface TooltipPortalProps extends BaseTooltip.Portal.Props {
140
+ children: React.ReactNode;
141
+ }
142
+
143
+ /**
144
+ * Tooltip Portal
145
+ *
146
+ * Renders the tooltip popup in a portal outside the DOM hierarchy.
147
+ */
148
+ const TooltipPortal = ({ children, ...props }: TooltipPortalProps) => {
149
+ return <BaseTooltip.Portal {...props}>{children}</BaseTooltip.Portal>;
150
+ };
151
+
152
+ // ============================================================================
153
+ // Tooltip Positioner
154
+ // ============================================================================
155
+
156
+ export interface TooltipPositionerProps
157
+ extends Omit<
158
+ React.ComponentProps<typeof BaseTooltip.Positioner>,
159
+ "className"
160
+ > {
161
+ className?: string;
162
+ }
163
+
164
+ /**
165
+ * Tooltip Positioner
166
+ *
167
+ * Positions the tooltip popup relative to the trigger.
168
+ */
169
+ const TooltipPositioner = React.forwardRef<
170
+ HTMLDivElement,
171
+ TooltipPositionerProps
172
+ >(({ className, side = "top", sideOffset = 8, ...props }, ref) => {
173
+ return (
174
+ <BaseTooltip.Positioner
175
+ ref={ref}
176
+ side={side}
177
+ sideOffset={sideOffset}
178
+ className={className}
179
+ {...props}
180
+ />
181
+ );
182
+ });
183
+ TooltipPositioner.displayName = "TooltipPositioner";
184
+
185
+ // ============================================================================
186
+ // Tooltip Popup
187
+ // ============================================================================
188
+
189
+ export interface TooltipPopupProps
190
+ extends Omit<React.ComponentProps<typeof BaseTooltip.Popup>, "className">,
191
+ VariantProps<typeof tooltipPopupVariants> {
192
+ className?: string;
193
+ }
194
+
195
+ /**
196
+ * Tooltip Popup
197
+ *
198
+ * The tooltip content container with styled appearance.
199
+ */
200
+ const TooltipPopup = React.forwardRef<HTMLDivElement, TooltipPopupProps>(
201
+ ({ className, variant, ...props }, ref) => {
202
+ return (
203
+ <BaseTooltip.Popup
204
+ ref={ref}
205
+ role="tooltip"
206
+ className={cn(tooltipPopupVariants({ variant }), className)}
207
+ {...props}
208
+ />
209
+ );
210
+ },
211
+ );
212
+ TooltipPopup.displayName = "TooltipPopup";
213
+
214
+ // ============================================================================
215
+ // Tooltip Arrow
216
+ // ============================================================================
217
+
218
+ export interface TooltipArrowProps
219
+ extends Omit<React.ComponentProps<typeof BaseTooltip.Arrow>, "className"> {
220
+ className?: string;
221
+ }
222
+
223
+ /**
224
+ * Tooltip Arrow
225
+ *
226
+ * Visual pointer element for the tooltip.
227
+ * Uses shared FloatingArrowSvg with tooltip-bg color token.
228
+ */
229
+ const TooltipArrow = React.forwardRef<HTMLDivElement, TooltipArrowProps>(
230
+ ({ className, ...props }, ref) => {
231
+ return (
232
+ <BaseTooltip.Arrow
233
+ ref={ref}
234
+ className={cn(tooltipArrowVariants(), className)}
235
+ {...props}
236
+ >
237
+ <FloatingArrowSvg fillClassName="fill-tooltip-bg" />
238
+ </BaseTooltip.Arrow>
239
+ );
240
+ },
241
+ );
242
+ TooltipArrow.displayName = "TooltipArrow";
243
+
244
+ // ============================================================================
245
+ // Simple Tooltip Component
246
+ // ============================================================================
247
+
248
+ export interface TooltipProps {
249
+ /** The content to show in the tooltip */
250
+ content: React.ReactNode;
251
+ /** The element that triggers the tooltip */
252
+ children: React.ReactNode;
253
+ /** Side of the trigger to show the tooltip */
254
+ side?: "top" | "bottom" | "left" | "right";
255
+ /** Offset from the trigger */
256
+ sideOffset?: number;
257
+ /** Alignment along the side */
258
+ align?: "start" | "center" | "end";
259
+ /** Delay before showing the tooltip (ms) */
260
+ delay?: number;
261
+ /** Delay before hiding the tooltip (ms) */
262
+ closeDelay?: number;
263
+ /** Whether to show an arrow */
264
+ showArrow?: boolean;
265
+ /** Controlled open state */
266
+ open?: boolean;
267
+ /** Default open state */
268
+ defaultOpen?: boolean;
269
+ /** Callback when open state changes */
270
+ onOpenChange?: (open: boolean) => void;
271
+ /** Additional className for the popup */
272
+ className?: string;
273
+ }
274
+
275
+ /**
276
+ * Tooltip
277
+ *
278
+ * A simple, pre-composed tooltip component for common use cases.
279
+ *
280
+ * @example
281
+ * ```tsx
282
+ * <Tooltip content="Save your changes">
283
+ * <Button>Save</Button>
284
+ * </Tooltip>
285
+ * ```
286
+ */
287
+ const Tooltip = ({
288
+ content,
289
+ children,
290
+ side = "top",
291
+ sideOffset = 8,
292
+ align = "center",
293
+ delay,
294
+ closeDelay,
295
+ showArrow = true,
296
+ open,
297
+ defaultOpen,
298
+ onOpenChange,
299
+ className,
300
+ }: TooltipProps) => {
301
+ return (
302
+ <TooltipRoot
303
+ open={open}
304
+ defaultOpen={defaultOpen}
305
+ onOpenChange={onOpenChange}
306
+ >
307
+ <TooltipTrigger delay={delay} closeDelay={closeDelay}>
308
+ {children}
309
+ </TooltipTrigger>
310
+ <TooltipPortal>
311
+ <TooltipPositioner side={side} sideOffset={sideOffset} align={align}>
312
+ <TooltipPopup className={className}>
313
+ {showArrow && <TooltipArrow />}
314
+ {content}
315
+ </TooltipPopup>
316
+ </TooltipPositioner>
317
+ </TooltipPortal>
318
+ </TooltipRoot>
319
+ );
320
+ };
321
+
322
+ // ============================================================================
323
+ // Compound Component Export
324
+ // ============================================================================
325
+
326
+ export const TooltipParts = Object.assign(TooltipRoot, {
327
+ Provider: TooltipProvider,
328
+ Root: TooltipRoot,
329
+ Trigger: TooltipTrigger,
330
+ Portal: TooltipPortal,
331
+ Positioner: TooltipPositioner,
332
+ Popup: TooltipPopup,
333
+ Arrow: TooltipArrow,
334
+ });
335
+
336
+ export {
337
+ Tooltip,
338
+ TooltipProvider,
339
+ TooltipRoot,
340
+ TooltipTrigger,
341
+ TooltipPortal,
342
+ TooltipPositioner,
343
+ TooltipPopup,
344
+ TooltipArrow,
345
+ tooltipPopupVariants,
346
+ tooltipArrowVariants,
347
+ };