@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.
- package/CHANGELOG.md +36 -0
- package/dist/index.d.ts +189 -28
- package/dist/mui-adapter.cjs +85 -85
- package/dist/mui-adapter.cjs.map +1 -1
- package/dist/mui-adapter.css +1 -1
- package/dist/mui-adapter.js +29694 -24393
- package/dist/mui-adapter.js.map +1 -1
- package/llms.txt +1 -0
- package/package.json +1 -1
- package/src/GlobalExemptions.modules.css +0 -6
- package/src/components/Accordion/Accordion.module.css +0 -8
- package/src/components/AssistiveElement/AssistiveElement.tsx +1 -1
- package/src/components/Autocomplete/Autocomplete.module.css +0 -8
- package/src/components/Avatar/Avatar.module.css +0 -8
- package/src/components/Button/BUTTON_IMPLEMENTATION_NOTES.md +12 -0
- package/src/components/Button/Button.module.css +6 -18
- package/src/components/Checkbox/Checkbox.tsx +4 -1
- package/src/components/Chip/Chip.module.css +0 -27
- package/src/components/DatePicker/DATEPICKER_IMPLEMENTATION_NOTES.md +29 -0
- package/src/components/DatePicker/DatePicker.icons.tsx +35 -0
- package/src/components/DatePicker/DatePicker.module.css +451 -42
- package/src/components/DatePicker/DatePicker.stories.tsx +23 -25
- package/src/components/DatePicker/DatePicker.tsx +229 -47
- package/src/components/DatePicker/USAGE.md +14 -1
- package/src/components/Dropdown/Dropdown.module.css +0 -7
- package/src/components/FileInput/FileInput.module.css +0 -21
- package/src/components/FileInput/FileInput.tsx +6 -0
- package/src/components/FileUpload/FileUpload.module.css +0 -11
- package/src/components/HoverCard/HoverCard.module.css +1 -7
- package/src/components/Label/Label.module.css +0 -6
- package/src/components/Link/Link.module.css +0 -13
- package/src/components/Menu/Menu.module.css +0 -5
- package/src/components/Modal/Modal.module.css +0 -11
- package/src/components/NumberInput/NumberInput.module.css +0 -7
- package/src/components/Pagination/Pagination.module.css +0 -81
- package/src/components/Popover/IMPLEMENTATION_NOTES.md +22 -0
- package/src/components/Popover/Popover.module.css +118 -0
- package/src/components/Popover/Popover.stories.tsx +133 -0
- package/src/components/Popover/Popover.tsx +275 -0
- package/src/components/Popover/USAGE.md +69 -0
- package/src/components/Popover/index.ts +1 -0
- package/src/components/SegmentedControl/IMPLEMENTATION_NOTES.md +6 -0
- package/src/components/SegmentedControl/SegmentedControl.module.css +32 -11
- package/src/components/SegmentedControl/SegmentedControl.tsx +18 -4
- package/src/components/Slider/IMPLEMENTATION_NOTES.md +29 -0
- package/src/components/Slider/Slider.module.css +80 -17
- package/src/components/Slider/Slider.stories.tsx +1 -1
- package/src/components/Slider/Slider.tsx +36 -1
- package/src/components/Stepper/IMPLEMENTATION_NOTES.md +54 -0
- package/src/components/Stepper/Stepper.module.css +139 -106
- package/src/components/Stepper/Stepper.tsx +76 -10
- package/src/components/Stepper/USAGE.md +4 -0
- package/src/components/Tabs/IMPLEMENTATION_NOTES.md +12 -0
- package/src/components/Tabs/Tabs.module.css +107 -24
- package/src/components/Tabs/Tabs.tsx +1 -0
- package/src/components/TextArea/TextArea.module.css +20 -11
- package/src/components/TextArea/TextArea.tsx +12 -23
- package/src/components/TextField/TextField.module.css +0 -8
- package/src/components/TimePicker/TimePicker.module.css +0 -16
- package/src/components/Timeline/IMPLEMENTATION_NOTES.md +24 -3
- package/src/components/Timeline/Timeline.module.css +55 -73
- package/src/components/Timeline/Timeline.tsx +23 -27
- package/src/components/Timeline/TimelineItem.tsx +38 -35
- package/src/components/Toast/Toast.module.css +0 -7
- package/src/components/Tooltip/Tooltip.module.css +0 -9
- package/src/components/TransferList/TRANSFERLIST_IMPLEMENTATION_NOTES.md +140 -0
- package/src/components/TransferList/TransferList.module.css +174 -38
- package/src/components/TransferList/TransferList.stories.tsx +110 -6
- package/src/components/TransferList/TransferList.tsx +417 -8
- package/src/components/TransferList/USAGE.md +37 -6
- package/src/components/index.ts +1 -0
- 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
|
-
|
|
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
|
);
|