@recursica/mui-adapter 0.24.0 → 0.26.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 (72) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/index.d.ts +189 -28
  3. package/dist/mui-adapter.cjs +85 -85
  4. package/dist/mui-adapter.cjs.map +1 -1
  5. package/dist/mui-adapter.css +1 -1
  6. package/dist/mui-adapter.js +29694 -24393
  7. package/dist/mui-adapter.js.map +1 -1
  8. package/llms.txt +1 -0
  9. package/package.json +1 -1
  10. package/src/GlobalExemptions.modules.css +0 -6
  11. package/src/components/Accordion/Accordion.module.css +0 -8
  12. package/src/components/AssistiveElement/AssistiveElement.tsx +1 -1
  13. package/src/components/Autocomplete/Autocomplete.module.css +0 -8
  14. package/src/components/Avatar/Avatar.module.css +0 -8
  15. package/src/components/Button/BUTTON_IMPLEMENTATION_NOTES.md +12 -0
  16. package/src/components/Button/Button.module.css +6 -18
  17. package/src/components/Checkbox/Checkbox.tsx +4 -1
  18. package/src/components/Chip/Chip.module.css +0 -27
  19. package/src/components/DatePicker/DATEPICKER_IMPLEMENTATION_NOTES.md +29 -0
  20. package/src/components/DatePicker/DatePicker.icons.tsx +35 -0
  21. package/src/components/DatePicker/DatePicker.module.css +451 -42
  22. package/src/components/DatePicker/DatePicker.stories.tsx +23 -25
  23. package/src/components/DatePicker/DatePicker.tsx +229 -47
  24. package/src/components/DatePicker/USAGE.md +14 -1
  25. package/src/components/Dropdown/Dropdown.module.css +0 -7
  26. package/src/components/FileInput/FileInput.module.css +0 -21
  27. package/src/components/FileInput/FileInput.tsx +6 -0
  28. package/src/components/FileUpload/FileUpload.module.css +0 -11
  29. package/src/components/HoverCard/HoverCard.module.css +1 -7
  30. package/src/components/Label/Label.module.css +0 -6
  31. package/src/components/Link/Link.module.css +0 -13
  32. package/src/components/Menu/Menu.module.css +0 -5
  33. package/src/components/Modal/Modal.module.css +0 -11
  34. package/src/components/NumberInput/NumberInput.module.css +0 -7
  35. package/src/components/Pagination/Pagination.module.css +0 -81
  36. package/src/components/Popover/IMPLEMENTATION_NOTES.md +22 -0
  37. package/src/components/Popover/Popover.module.css +118 -0
  38. package/src/components/Popover/Popover.stories.tsx +133 -0
  39. package/src/components/Popover/Popover.tsx +275 -0
  40. package/src/components/Popover/USAGE.md +69 -0
  41. package/src/components/Popover/index.ts +1 -0
  42. package/src/components/SegmentedControl/IMPLEMENTATION_NOTES.md +6 -0
  43. package/src/components/SegmentedControl/SegmentedControl.module.css +32 -11
  44. package/src/components/SegmentedControl/SegmentedControl.tsx +18 -4
  45. package/src/components/Slider/IMPLEMENTATION_NOTES.md +29 -0
  46. package/src/components/Slider/Slider.module.css +80 -17
  47. package/src/components/Slider/Slider.stories.tsx +1 -1
  48. package/src/components/Slider/Slider.tsx +36 -1
  49. package/src/components/Stepper/IMPLEMENTATION_NOTES.md +54 -0
  50. package/src/components/Stepper/Stepper.module.css +139 -106
  51. package/src/components/Stepper/Stepper.tsx +76 -10
  52. package/src/components/Stepper/USAGE.md +4 -0
  53. package/src/components/Tabs/IMPLEMENTATION_NOTES.md +12 -0
  54. package/src/components/Tabs/Tabs.module.css +107 -24
  55. package/src/components/Tabs/Tabs.tsx +1 -0
  56. package/src/components/TextArea/TextArea.module.css +20 -11
  57. package/src/components/TextArea/TextArea.tsx +12 -23
  58. package/src/components/TextField/TextField.module.css +0 -8
  59. package/src/components/TimePicker/TimePicker.module.css +0 -16
  60. package/src/components/Timeline/IMPLEMENTATION_NOTES.md +24 -3
  61. package/src/components/Timeline/Timeline.module.css +55 -73
  62. package/src/components/Timeline/Timeline.tsx +23 -27
  63. package/src/components/Timeline/TimelineItem.tsx +38 -35
  64. package/src/components/Toast/Toast.module.css +0 -7
  65. package/src/components/Tooltip/Tooltip.module.css +0 -9
  66. package/src/components/TransferList/TRANSFERLIST_IMPLEMENTATION_NOTES.md +140 -0
  67. package/src/components/TransferList/TransferList.module.css +174 -38
  68. package/src/components/TransferList/TransferList.stories.tsx +110 -6
  69. package/src/components/TransferList/TransferList.tsx +417 -8
  70. package/src/components/TransferList/USAGE.md +37 -6
  71. package/src/components/index.ts +1 -0
  72. package/src/index.ts +3 -0
@@ -0,0 +1,118 @@
1
+ /* HARDCODED VALUES:
2
+ - border-style: solid. Mui's Tooltip content div does not set border-style natively;
3
+ without it, the border-width/border-color tokens below have no visible effect.
4
+ Same pattern as HoverCard / Tooltip / Menu in this adapter.
5
+ - margin: 0 on the dropdown (per placement, see below). Mui's Tooltip content div ships
6
+ its own hardcoded 14px (or 24px on touch) margin toward the target for every placement,
7
+ stacked on top of the `offset` popper modifier Popover.tsx already applies. That modifier
8
+ alone is what matches Mantine's own gap, so Mui's built-in margin must be zeroed out or
9
+ the visible gap ends up far larger than Mantine's.
10
+ - border-style: solid on the arrow's ::before. Mui's arrow pseudo-element has no border by
11
+ default; without it, the border-color/border-width tokens below have no visible effect.
12
+ - All structural layout (display, position, overflow) is deferred to Mui's native
13
+ Popper/Tooltip behavior. We only override visual design tokens (colors, typography,
14
+ spacing, borders).
15
+ */
16
+
17
+ /* ======================================
18
+ DROPDOWN CONTAINER
19
+ ====================================== */
20
+
21
+ .dropdown {
22
+ background-color: var(
23
+ --recursica_ui-kit_components_hover-card-popover_properties_colors_background-color
24
+ );
25
+ border-style: solid; /* HARDCODE: Mui's Tooltip content div does not set border-style natively */
26
+ border-width: var(
27
+ --recursica_ui-kit_components_hover-card-popover_properties_border-size
28
+ );
29
+ border-color: var(
30
+ --recursica_ui-kit_components_hover-card-popover_properties_colors_border-color
31
+ );
32
+ border-radius: var(
33
+ --recursica_ui-kit_components_hover-card-popover_properties_border-radius
34
+ );
35
+ box-shadow: var(
36
+ --recursica_ui-kit_components_hover-card-popover_properties_elevation
37
+ );
38
+
39
+ min-width: var(
40
+ --recursica_ui-kit_components_hover-card-popover_properties_min-width
41
+ );
42
+ max-width: var(
43
+ --recursica_ui-kit_components_hover-card-popover_properties_max-width
44
+ );
45
+
46
+ padding: var(
47
+ --recursica_ui-kit_components_hover-card-popover_properties_vertical-padding
48
+ )
49
+ var(
50
+ --recursica_ui-kit_components_hover-card-popover_properties_horizontal-padding
51
+ );
52
+
53
+ /* Typography */
54
+ font-family: var(
55
+ --recursica_ui-kit_components_hover-card-popover_properties_content-text_font-family
56
+ );
57
+ font-size: var(
58
+ --recursica_ui-kit_components_hover-card-popover_properties_content-text_font-size
59
+ );
60
+ font-style: var(
61
+ --recursica_ui-kit_components_hover-card-popover_properties_content-text_font-style
62
+ );
63
+ font-weight: var(
64
+ --recursica_ui-kit_components_hover-card-popover_properties_content-text_font-weight
65
+ );
66
+ letter-spacing: var(
67
+ --recursica_ui-kit_components_hover-card-popover_properties_content-text_letter-spacing
68
+ );
69
+ line-height: var(
70
+ --recursica_ui-kit_components_hover-card-popover_properties_content-text_line-height
71
+ );
72
+ text-decoration: var(
73
+ --recursica_ui-kit_components_hover-card-popover_properties_content-text_text-decoration
74
+ );
75
+ text-transform: var(
76
+ --recursica_ui-kit_components_hover-card-popover_properties_content-text_text-transform
77
+ );
78
+
79
+ color: var(
80
+ --recursica_ui-kit_components_hover-card-popover_properties_colors_content
81
+ );
82
+ }
83
+
84
+ /* Mui's Tooltip content div sets its own margin toward the target per placement
85
+ (marginTop/marginBottom/marginLeft/marginRight), duplicating the gap already produced by
86
+ the `offset` popper modifier in Popover.tsx. Zero it out so the modifier is the single
87
+ source of truth for the gap, matching Mantine's gap. Selector specificity matches Mui's own
88
+ placement rule (class + attribute + class) so it wins on source order (this adapter's
89
+ modules are injected after Mui's via injectFirst). */
90
+ :global(.MuiTooltip-popper[data-popper-placement]) .dropdown {
91
+ margin: 0; /* HARDCODE: cancel Mui's built-in per-placement margin, see file header */
92
+ }
93
+
94
+ /* ======================================
95
+ ARROW / BEAK
96
+ ====================================== */
97
+
98
+ /* Mui's arrow is a solid rotated-square shape filled via `currentColor` (no separate
99
+ border), unlike Mantine's bordered-diamond arrow. Fill with the panel's own
100
+ background-color token for the closest visual match Mui's primitive allows. */
101
+ .arrow {
102
+ color: var(
103
+ --recursica_ui-kit_components_hover-card-popover_properties_colors_background-color
104
+ );
105
+ }
106
+
107
+ /* Mui's arrow ::before has no border by default, so it renders as a solid triangle with no
108
+ visible edge against a similarly-colored dropdown body. Mantine's arrow gets its visibility
109
+ the same way — a border using the popover's own border tokens — so replicate that here. */
110
+ .arrow::before {
111
+ border-style: solid; /* HARDCODE: Mui's arrow ::before does not set border-style natively */
112
+ border-width: var(
113
+ --recursica_ui-kit_components_hover-card-popover_properties_border-size
114
+ );
115
+ border-color: var(
116
+ --recursica_ui-kit_components_hover-card-popover_properties_colors_border-color
117
+ );
118
+ }
@@ -0,0 +1,133 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { Popover } from "./Popover";
3
+ import { Button } from "../Button";
4
+ import { Text } from "../Text/Text";
5
+
6
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
+ type PopoverStoryArgs = Record<string, any>;
8
+
9
+ const meta: Meta = {
10
+ title: "UI-Kit/Popover",
11
+ component: Popover,
12
+ tags: ["autodocs"],
13
+ parameters: {
14
+ controls: {
15
+ // Explicitly list only the props integrators should configure — Mui's
16
+ // underlying Tooltip props would otherwise leak into Controls.
17
+ include: ["withBeak", "position", "defaultOpened"],
18
+ },
19
+ docs: {
20
+ description: {
21
+ component:
22
+ "The `Popover` component is a composable wrapper around Mui's Tooltip in click-controlled mode. It displays a dropdown panel when the user clicks a target element.",
23
+ },
24
+ },
25
+ },
26
+ argTypes: {
27
+ withBeak: {
28
+ control: "boolean",
29
+ description:
30
+ "Whether to display a beak (arrow) pointing from the dropdown to the target.",
31
+ },
32
+ position: {
33
+ control: "select",
34
+ options: [
35
+ "top",
36
+ "top-start",
37
+ "top-end",
38
+ "bottom",
39
+ "bottom-start",
40
+ "bottom-end",
41
+ "left",
42
+ "left-start",
43
+ "left-end",
44
+ "right",
45
+ "right-start",
46
+ "right-end",
47
+ ],
48
+ description: "Dropdown position relative to target",
49
+ },
50
+ defaultOpened: {
51
+ control: "boolean",
52
+ description: "Initial opened state",
53
+ },
54
+ },
55
+ };
56
+
57
+ export default meta;
58
+ type Story = StoryObj<PopoverStoryArgs>;
59
+
60
+ export const Default: Story = {
61
+ args: {
62
+ withBeak: true,
63
+ position: "top",
64
+ },
65
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
66
+ render: ({ withLayer, layer, ...args }: PopoverStoryArgs) => {
67
+ return (
68
+ <Popover width={250} {...args}>
69
+ <Popover.Target>
70
+ <Button variant="solid">Toggle Popover</Button>
71
+ </Popover.Target>
72
+ <Popover.Dropdown>
73
+ <Text>
74
+ This is the popover content. It can contain any elements you want to
75
+ display when the user clicks the target.
76
+ </Text>
77
+ </Popover.Dropdown>
78
+ </Popover>
79
+ );
80
+ },
81
+ };
82
+
83
+ export const SolidDefault: Story = {
84
+ args: {
85
+ withBeak: true,
86
+ position: "top",
87
+ defaultOpened: true,
88
+ },
89
+ parameters: {
90
+ layout: "centered",
91
+ controls: { disable: true },
92
+ },
93
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
94
+ render: ({ withLayer, layer, ...args }: PopoverStoryArgs) => {
95
+ return (
96
+ <Popover width={200} {...args}>
97
+ <Popover.Target>
98
+ <Button variant="solid">Toggle Popover</Button>
99
+ </Popover.Target>
100
+ <Popover.Dropdown>
101
+ <Text>
102
+ This is a static representation of an opened popover with a beak.
103
+ </Text>
104
+ </Popover.Dropdown>
105
+ </Popover>
106
+ );
107
+ },
108
+ };
109
+
110
+ export const WithoutBeak: Story = {
111
+ args: {
112
+ withBeak: false,
113
+ position: "bottom",
114
+ defaultOpened: true,
115
+ },
116
+ parameters: {
117
+ layout: "centered",
118
+ controls: { disable: true },
119
+ },
120
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
121
+ render: ({ withLayer, layer, ...args }: PopoverStoryArgs) => {
122
+ return (
123
+ <Popover width={200} {...args}>
124
+ <Popover.Target>
125
+ <Button variant="outline">Bottom Popover</Button>
126
+ </Popover.Target>
127
+ <Popover.Dropdown>
128
+ <Text>This popover is positioned at the bottom and has no beak.</Text>
129
+ </Popover.Dropdown>
130
+ </Popover>
131
+ );
132
+ },
133
+ };
@@ -0,0 +1,275 @@
1
+ import React, {
2
+ cloneElement,
3
+ isValidElement,
4
+ useEffect,
5
+ useRef,
6
+ useState,
7
+ } from "react";
8
+ import {
9
+ Tooltip as MuiTooltip,
10
+ type TooltipProps as MuiTooltipProps,
11
+ } from "@mui/material";
12
+ import {
13
+ filterStylingProps,
14
+ type RecursicaOverStyled,
15
+ } from "../../utils/filterStylingProps";
16
+ import styles from "./Popover.module.css";
17
+
18
+ // ============================================================
19
+ // POPOVER ROOT
20
+ // ============================================================
21
+
22
+ import { type RecursicaPopoverProps } from "@recursica/adapter-common";
23
+
24
+ /**
25
+ * Behavioral props specific to this adapter's click-controlled implementation.
26
+ * `withBeak` comes from the shared `RecursicaPopoverProps` (adapter-common); the
27
+ * rest map to Mantine's own `PopoverProps` surface, reproduced here since Mui has
28
+ * no single library type that already covers them.
29
+ */
30
+ export interface PopoverOwnProps extends RecursicaPopoverProps {
31
+ /** Dropdown position relative to the target */
32
+ position?: MuiTooltipProps["placement"];
33
+ /** Initial opened state (uncontrolled) */
34
+ defaultOpened?: boolean;
35
+ /** Controlled opened state */
36
+ opened?: boolean;
37
+ /** Called whenever the opened state changes */
38
+ onChange?: (opened: boolean) => void;
39
+ /** Distance in px between the dropdown and the target */
40
+ offset?: number;
41
+ /** Fixed width applied to the dropdown panel */
42
+ width?: number | string;
43
+ children?: React.ReactNode;
44
+ }
45
+
46
+ /**
47
+ * Recursica Popover component wrapping Mui's Tooltip in click-controlled mode.
48
+ *
49
+ * Displays a dropdown panel when the user clicks the target element.
50
+ * Uses the composable dot-notation pattern:
51
+ * ```tsx
52
+ * <Popover withBeak>
53
+ * <Popover.Target>
54
+ * <Button>Click me</Button>
55
+ * </Popover.Target>
56
+ * <Popover.Dropdown>
57
+ * Content displayed in popover
58
+ * </Popover.Dropdown>
59
+ * </Popover>
60
+ * ```
61
+ */
62
+ export type PopoverProps = RecursicaOverStyled<
63
+ Omit<
64
+ MuiTooltipProps,
65
+ "title" | "children" | "open" | "onClose" | "onOpen" | "placement"
66
+ > &
67
+ PopoverOwnProps
68
+ >;
69
+
70
+ const PopoverBase = function Popover({
71
+ overStyled = false,
72
+ withBeak = true,
73
+ position = "top",
74
+ defaultOpened = false,
75
+ opened,
76
+ onChange,
77
+ offset = 8,
78
+ width,
79
+ children,
80
+ ...rest
81
+ }: PopoverProps) {
82
+ const sanitizedProps = filterStylingProps(
83
+ rest as Record<string, unknown>,
84
+ overStyled,
85
+ );
86
+
87
+ // Bind CSS module classes to Mui's internal classNames API
88
+ const mergedClassNames: Partial<Record<string, string>> = {
89
+ tooltip: styles.dropdown,
90
+ arrow: styles.arrow,
91
+ };
92
+
93
+ const classesProp = (sanitizedProps as Record<string, unknown>).classes;
94
+ if (
95
+ classesProp &&
96
+ typeof classesProp === "object" &&
97
+ !Array.isArray(classesProp)
98
+ ) {
99
+ const o = classesProp as Record<string, string>;
100
+ Object.keys(o).forEach((key) => {
101
+ mergedClassNames[key] = mergedClassNames[key]
102
+ ? `${mergedClassNames[key]} ${o[key]}`
103
+ : o[key];
104
+ });
105
+ }
106
+
107
+ const [internalOpened, setInternalOpened] = useState(defaultOpened);
108
+ const isControlled = opened !== undefined;
109
+ const currentOpened = isControlled ? (opened as boolean) : internalOpened;
110
+
111
+ const setOpened = (next: boolean) => {
112
+ if (!isControlled) setInternalOpened(next);
113
+ onChange?.(next);
114
+ };
115
+
116
+ // Find Target and Dropdown children
117
+ let targetNode: React.ReactNode = null;
118
+ let dropdownNode: React.ReactNode = null;
119
+
120
+ React.Children.forEach(children, (child) => {
121
+ if (isValidElement(child)) {
122
+ const childElement = child as unknown as {
123
+ type?: { displayName?: string };
124
+ props: { children?: React.ReactNode };
125
+ };
126
+ if (childElement.type?.displayName === "PopoverTarget") {
127
+ targetNode = childElement.props.children;
128
+ } else if (childElement.type?.displayName === "PopoverDropdown") {
129
+ dropdownNode = childElement.props.children;
130
+ }
131
+ }
132
+ });
133
+
134
+ if (!targetNode) {
135
+ throw new Error("Popover requires a <Popover.Target> child.");
136
+ }
137
+ if (!dropdownNode) {
138
+ throw new Error("Popover requires a <Popover.Dropdown> child.");
139
+ }
140
+
141
+ const targetRef = useRef<HTMLElement | null>(null);
142
+ const dropdownRef = useRef<HTMLDivElement | null>(null);
143
+
144
+ // Mui's Tooltip has no native "click outside to close" behavior once its own
145
+ // hover/focus/touch listeners are disabled for click-controlled use, so it's
146
+ // implemented here directly (mirrors Mantine's default closeOnClickOutside).
147
+ useEffect(() => {
148
+ if (!currentOpened) return undefined;
149
+ const handlePointerDown = (event: MouseEvent) => {
150
+ const target = event.target as Node;
151
+ if (targetRef.current?.contains(target)) return;
152
+ if (dropdownRef.current?.contains(target)) return;
153
+ setOpened(false);
154
+ };
155
+ document.addEventListener("mousedown", handlePointerDown);
156
+ return () => document.removeEventListener("mousedown", handlePointerDown);
157
+ // eslint-disable-next-line react-hooks/exhaustive-deps
158
+ }, [currentOpened]);
159
+
160
+ const handleTargetClick = (event: React.MouseEvent) => {
161
+ if (isValidElement(targetNode)) {
162
+ (
163
+ targetNode.props as { onClick?: (e: React.MouseEvent) => void }
164
+ ).onClick?.(event);
165
+ }
166
+ setOpened(!currentOpened);
167
+ };
168
+
169
+ // Mui's Tooltip auto-wires `aria-labelledby`/`aria-label` on the target to describe
170
+ // it via the tooltip content once open — correct for an actual tooltip, but wrong here:
171
+ // it would silently replace the target's own accessible name (e.g. a Button's label)
172
+ // with the popover's body text. Explicitly reset both so the target keeps its own name.
173
+ const targetAriaOverrides = {
174
+ "aria-haspopup": "dialog" as const,
175
+ "aria-expanded": currentOpened,
176
+ "aria-labelledby": undefined,
177
+ "aria-label": undefined,
178
+ };
179
+
180
+ const clonedTarget = isValidElement(targetNode) ? (
181
+ cloneElement(
182
+ targetNode as React.ReactElement,
183
+ {
184
+ onClick: handleTargetClick,
185
+ ref: targetRef,
186
+ ...targetAriaOverrides,
187
+ } as Record<string, unknown>,
188
+ )
189
+ ) : (
190
+ <span
191
+ onClick={handleTargetClick}
192
+ ref={targetRef as unknown as React.Ref<HTMLSpanElement>}
193
+ {...targetAriaOverrides}
194
+ >
195
+ {targetNode}
196
+ </span>
197
+ );
198
+
199
+ return (
200
+ <MuiTooltip
201
+ {...(sanitizedProps as unknown as Omit<
202
+ MuiTooltipProps,
203
+ "title" | "children"
204
+ >)}
205
+ title={<div ref={dropdownRef}>{dropdownNode}</div>}
206
+ open={currentOpened}
207
+ onClose={() => setOpened(false)}
208
+ placement={position}
209
+ arrow={withBeak}
210
+ disableHoverListener
211
+ disableFocusListener
212
+ disableTouchListener
213
+ slotProps={{
214
+ popper: {
215
+ modifiers: [
216
+ {
217
+ name: "offset",
218
+ options: {
219
+ offset: [0, offset],
220
+ },
221
+ },
222
+ ],
223
+ },
224
+ ...(width !== undefined ? { tooltip: { style: { width } } } : {}),
225
+ }}
226
+ classes={mergedClassNames as unknown as MuiTooltipProps["classes"]}
227
+ >
228
+ {clonedTarget}
229
+ </MuiTooltip>
230
+ );
231
+ };
232
+ PopoverBase.displayName = "Popover";
233
+
234
+ // ============================================================
235
+ // POPOVER TARGET
236
+ // ============================================================
237
+
238
+ /**
239
+ * Wrapper for the element that triggers the popover.
240
+ * Requires a single child element; only used as a marker to locate the
241
+ * trigger element, it is never rendered directly (see `PopoverBase`).
242
+ */
243
+ export type PopoverTargetProps = { children?: React.ReactNode };
244
+
245
+ const PopoverTarget = function PopoverTarget({ children }: PopoverTargetProps) {
246
+ return <>{children}</>;
247
+ };
248
+ PopoverTarget.displayName = "PopoverTarget";
249
+
250
+ // ============================================================
251
+ // POPOVER DROPDOWN
252
+ // ============================================================
253
+
254
+ /** The dropdown panel displayed from the popover. */
255
+ export type PopoverDropdownProps = { children?: React.ReactNode };
256
+
257
+ const PopoverDropdown = function PopoverDropdown({
258
+ children,
259
+ }: PopoverDropdownProps) {
260
+ return <>{children}</>;
261
+ };
262
+ PopoverDropdown.displayName = "PopoverDropdown";
263
+
264
+ // ============================================================
265
+ // DOT NOTATION EXPORT
266
+ // ============================================================
267
+
268
+ type PopoverComponent = typeof PopoverBase & {
269
+ Target: typeof PopoverTarget;
270
+ Dropdown: typeof PopoverDropdown;
271
+ };
272
+
273
+ export const Popover = PopoverBase as PopoverComponent;
274
+ Popover.Target = PopoverTarget;
275
+ Popover.Dropdown = PopoverDropdown;
@@ -0,0 +1,69 @@
1
+ # Popover - Usage Guide
2
+
3
+ This document describes how to integrate and use the `Popover` component in your projects using `@recursica/mui-adapter`.
4
+
5
+ ---
6
+
7
+ ## 1. Import Reference
8
+
9
+ ```tsx
10
+ import { Popover } from "@recursica/mui-adapter";
11
+ ```
12
+
13
+ ---
14
+
15
+ ## 2. Basic Example
16
+
17
+ ```tsx
18
+ import React from "react";
19
+ import { Popover, Button, Text } from "@recursica/mui-adapter";
20
+
21
+ export default function Demo() {
22
+ return (
23
+ <Popover position="bottom" withBeak>
24
+ <Popover.Target>
25
+ <Button>Open Popover</Button>
26
+ </Popover.Target>
27
+ <Popover.Dropdown>
28
+ <Text size="rec-sm">This is the popover content.</Text>
29
+ </Popover.Dropdown>
30
+ </Popover>
31
+ );
32
+ }
33
+ ```
34
+
35
+ ---
36
+
37
+ ## 3. Design System Integration
38
+
39
+ All Recursica components in the `@recursica/mui-adapter` package adhere strictly to design system spacing, scaling, and behavior patterns.
40
+
41
+ > [!IMPORTANT]
42
+ >
43
+ > - **Anti-override protection**: Rogue style injections (like inline `style` or arbitrary `className`) are automatically blocked by our prop layer unless `overStyled={true}` is explicitly provided.
44
+ > - **No Direct Layers**: Do not pass a `layer` prop to this component. To place it on a specific visual layer, wrap it in a `<Layer layer={0|1|2|3}>` component natively.
45
+ > - **Variables and Theming**: Styling is entirely determined by local CSS variables defined in `recursica_variables_scoped.css` and mapped in the component's CSS module.
46
+
47
+ ---
48
+
49
+ ## 4. Key Integration Features & Constraints
50
+
51
+ ### Composition
52
+
53
+ `Popover`, `Popover.Target`, and `Popover.Dropdown` are used together: `Popover.Target` wraps the trigger element and applies no styling of its own, while `Popover.Dropdown` renders the styled panel content. Both `Popover.Target` and `Popover.Dropdown` are required — omitting either throws.
54
+
55
+ ### Open/close behavior
56
+
57
+ The dropdown opens when the user clicks the target and closes on an outside click, on Escape, or by clicking the target again. Use `opened`/`onChange` for controlled usage, or `defaultOpened` to set the initial uncontrolled state.
58
+
59
+ ### Beak (Arrow)
60
+
61
+ The Recursica prop `withBeak` (defaulting to `true`) controls whether the pointer beak is shown.
62
+
63
+ ### Position
64
+
65
+ `position` accepts the same 12 placement values as `HoverCard`/`Tooltip` (e.g. `"top"`, `"bottom-start"`, `"right-end"`) and defaults to `"top"`.
66
+
67
+ ### Width
68
+
69
+ An optional `width` prop sets a fixed width on the dropdown panel.
@@ -0,0 +1 @@
1
+ export * from "./Popover";
@@ -0,0 +1,6 @@
1
+ # SegmentedControl Implementation Notes
2
+
3
+ ## Labels rendering uppercase (2026-08-19)
4
+
5
+ - **Root cause:** `--recursica_ui-kit_components_segmented-control-item_variants_selection-states_{unselected,selected}_properties_text_text-transform` resolves to `--recursica_tokens_font_cases_original`, which has no definition in `recursica_variables_scoped.css` (only `_lowercase`/`_titlecase`/`_uppercase` are defined there). The resulting `var()` on `.label` is invalid, and since `text-transform` is an inherited property, the invalid value falls back to the inherited value from `.control` (`.MuiToggleButton-root`) — which carries MUI's own `text-transform: uppercase` button default. Mantine's control has no such native uppercase default, so the same broken token never surfaced there.
6
+ - **Fix:** Reset `text-transform: none` on `.root .control` alongside the other MUI ToggleButton baseline resets (padding/border/etc.) already there, so nothing uppercase is left to inherit. Matches the existing `text-transform: none` MUI-baseline reset pattern in `Button.module.css`. Not a design-token value — it's a structural reset of MUI's own default, same category as the other hardcoded resets already exempted at the top of this file.
@@ -2,16 +2,13 @@
2
2
  - border-style: solid; on container and indicator
3
3
  - background-color: transparent; on label hover (overriding Mantine)
4
4
  - Scope prefix .root to enforce Figma tokens over Mantine's inline calculation without using !important
5
+ - padding/border/border-radius/min-height/min-width: 0 and background-color: transparent on
6
+ .control (MUI's ToggleButton root) so its own baseline button box model does not stack on
7
+ top of .label's token-driven height/border/radius, mirroring Mantine's transparent .control wrapper
8
+ - text-transform: none on .control resets MUI's ToggleButton uppercase default (see comment
9
+ above that rule); needed because the item text-transform token has no valid scoped value
5
10
  */
6
11
 
7
- /* EXEMPTIONS:
8
- - segmented-control-item_properties_item_border-radius is ignored because the item border-radius
9
- is fully governed by the per-selection-state tokens (unselected/selected `properties_border-radius`,
10
- already applied to `.label` and `.control.Mui-selected` below); this generic, state-agnostic radius
11
- token has no distinct consumption site without conflicting with those state-specific overrides.
12
- The Mantine reference adapter exempts this same variable for the same reason. */
13
- /* recursica-ignore: --recursica_ui-kit_components_segmented-control-item_properties_item_border-radius */
14
-
15
12
  .root {
16
13
  background-color: var(
17
14
  --recursica_ui-kit_components_segmented-control_properties_colors_background-color
@@ -38,6 +35,28 @@
38
35
  gap: var(--recursica_ui-kit_components_segmented-control_properties_item-gap);
39
36
  }
40
37
 
38
+ /* MUI's ToggleButton root ships its own padding/border/border-radius/min-height/min-width and a
39
+ text-transform: uppercase button default; reset all of it so it doesn't stack on top of (or leak
40
+ through, via inheritance, into) .label's token-driven box model/typography below. The
41
+ text-transform reset matters because the item's text-transform token
42
+ (segmented-control-item_..._text_text-transform) currently has no valid scoped value to resolve
43
+ to, so without this reset .label's own `text-transform: var(...)` below is invalid and the
44
+ inherited MUI uppercase default would otherwise show through (Mantine has no such native
45
+ default, so it never surfaced there). */
46
+ .root .control {
47
+ padding: 0;
48
+ border: none;
49
+ border-radius: 0;
50
+ min-height: 0;
51
+ min-width: 0;
52
+ background-color: transparent;
53
+ text-transform: none;
54
+ }
55
+
56
+ .root .control:hover {
57
+ background-color: transparent;
58
+ }
59
+
41
60
  .root .label {
42
61
  padding-left: var(
43
62
  --recursica_ui-kit_components_segmented-control-item_properties_item_padding-horizontal
@@ -128,8 +147,10 @@
128
147
  );
129
148
  }
130
149
 
131
- /* Indicator mapping (MUI ToggleButton applies .Mui-selected to the button instead of rendering a sliding indicator) */
132
- .root .control.Mui-selected {
150
+ /* Indicator mapping (MUI ToggleButton applies .Mui-selected to the button instead of rendering a sliding indicator).
151
+ Mui-selected must be wrapped in :global() — otherwise CSS Modules locally hashes it and the
152
+ selector never matches MUI's actual global class (silently dropping the selected state). */
153
+ .root .control:global(.Mui-selected) {
133
154
  background-color: var(
134
155
  --recursica_ui-kit_components_segmented-control-item_variants_selection-states_selected_properties_colors_background-color
135
156
  );
@@ -149,7 +170,7 @@
149
170
  }
150
171
 
151
172
  /* Selected label text color override */
152
- .root .control.Mui-selected .label {
173
+ .root .control:global(.Mui-selected) .label {
153
174
  color: var(
154
175
  --recursica_ui-kit_components_segmented-control-item_variants_selection-states_selected_properties_colors_text-color
155
176
  );