@recursica/mantine-adapter 0.17.0 → 0.19.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 +16 -0
- package/dist/mantine-adapter.cjs +1 -1
- package/dist/mantine-adapter.cjs.map +1 -1
- package/dist/mantine-adapter.css +1 -1
- package/dist/mantine-adapter.js +1642 -1357
- package/dist/mantine-adapter.js.map +1 -1
- package/dist/src/OverStyling.d.ts +1 -0
- package/dist/src/Version.d.ts +1 -0
- package/dist/src/components/Modal/Modal.d.ts +39 -2
- package/dist/src/components/Modal/index.d.ts +1 -0
- package/dist/src/components/Popover/Popover.d.ts +55 -0
- package/dist/src/components/Popover/index.d.ts +1 -0
- package/dist/src/components/Timeline/Timeline.d.ts +14 -2
- package/dist/src/components/Timeline/TimelineItem.d.ts +25 -0
- package/dist/src/components/Toast/Toast.d.ts +21 -2
- package/dist/src/components/Toast/index.d.ts +1 -0
- package/dist/src/components/index.d.ts +2 -1
- package/package.json +4 -3
- package/src/Introduction.stories.tsx +180 -0
- package/src/{OverStyling.stories.tsx → OverStyling.tsx} +1 -11
- package/src/{Version.stories.tsx → Version.tsx} +1 -13
- package/src/components/Modal/MODAL_IMPLEMENTATION_NOTES.md +15 -0
- package/src/components/Modal/Modal.module.css +127 -0
- package/src/components/Modal/Modal.stories.tsx +66 -4
- package/src/components/Modal/Modal.tsx +218 -3
- package/src/components/Modal/index.ts +1 -0
- package/src/components/Popover/IMPLEMENTATION_NOTES.md +77 -0
- package/src/components/Popover/Popover.module.css +84 -0
- package/src/components/Popover/Popover.stories.tsx +133 -0
- package/src/components/Popover/Popover.tsx +156 -0
- package/src/components/Popover/index.ts +1 -0
- package/src/components/Timeline/TIMELINE_IMPLEMENTATION_NOTES.md +13 -0
- package/src/components/Timeline/Timeline.module.css +361 -0
- package/src/components/Timeline/Timeline.stories.tsx +114 -3
- package/src/components/Timeline/Timeline.tsx +81 -4
- package/src/components/Timeline/TimelineItem.tsx +102 -0
- package/src/components/Toast/TOAST_IMPLEMENTATION_NOTES.md +39 -0
- package/src/components/Toast/Toast.module.css +109 -0
- package/src/components/Toast/Toast.stories.tsx +57 -7
- package/src/components/Toast/Toast.tsx +90 -4
- package/src/components/Toast/index.ts +1 -0
- package/src/components/index.ts +2 -1
|
@@ -1,17 +1,79 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
1
2
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
-
import { Modal } from "./Modal";
|
|
3
|
-
import {
|
|
3
|
+
import { Modal, type ModalProps } from "./Modal";
|
|
4
|
+
import { Button } from "../Button";
|
|
4
5
|
|
|
5
6
|
const meta: Meta<typeof Modal> = {
|
|
6
|
-
title: "UI-Kit
|
|
7
|
+
title: "UI-Kit/Modal",
|
|
7
8
|
component: Modal,
|
|
8
9
|
tags: ["autodocs"],
|
|
10
|
+
argTypes: {
|
|
11
|
+
opened: { table: { disable: true } },
|
|
12
|
+
onClose: { table: { disable: true } },
|
|
13
|
+
defaultChecked: { table: { disable: true } },
|
|
14
|
+
},
|
|
9
15
|
};
|
|
10
16
|
|
|
11
17
|
export default meta;
|
|
12
18
|
|
|
13
19
|
type Story = StoryObj<typeof Modal>;
|
|
14
20
|
|
|
21
|
+
const DefaultWrapper = (args: ModalProps) => {
|
|
22
|
+
const [opened, setOpened] = useState(false);
|
|
23
|
+
return (
|
|
24
|
+
<>
|
|
25
|
+
<Modal {...args} opened={opened} onClose={() => setOpened(false)}>
|
|
26
|
+
Please log in to continue accessing this feature.
|
|
27
|
+
<Modal.Footer>
|
|
28
|
+
<Button variant="outline" onClick={() => setOpened(false)}>
|
|
29
|
+
Cancel
|
|
30
|
+
</Button>
|
|
31
|
+
<Button onClick={() => setOpened(false)}>Confirm</Button>
|
|
32
|
+
</Modal.Footer>
|
|
33
|
+
</Modal>
|
|
34
|
+
<Button onClick={() => setOpened(true)}>Open Modal</Button>
|
|
35
|
+
</>
|
|
36
|
+
);
|
|
37
|
+
};
|
|
38
|
+
|
|
15
39
|
export const Default: Story = {
|
|
16
|
-
|
|
40
|
+
args: {
|
|
41
|
+
title: "Authentication Required",
|
|
42
|
+
},
|
|
43
|
+
render: (args) => <DefaultWrapper {...args} />,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const ScrollingWrapper = (args: ModalProps) => {
|
|
47
|
+
const [opened, setOpened] = useState(false);
|
|
48
|
+
return (
|
|
49
|
+
<>
|
|
50
|
+
<Modal {...args} opened={opened} onClose={() => setOpened(false)}>
|
|
51
|
+
<p>
|
|
52
|
+
This modal demonstrates the dynamically injected scroll dividers. When
|
|
53
|
+
this body content overflows, the borders between the Header and Footer
|
|
54
|
+
automatically appear to define the scrolling boundary.
|
|
55
|
+
</p>
|
|
56
|
+
{Array(20)
|
|
57
|
+
.fill(0)
|
|
58
|
+
.map((_, i) => (
|
|
59
|
+
<p key={i}>Scrolling content block {i + 1}...</p>
|
|
60
|
+
))}
|
|
61
|
+
{/* The new Footer abstraction */}
|
|
62
|
+
<Modal.Footer>
|
|
63
|
+
<Button variant="outline" onClick={() => setOpened(false)}>
|
|
64
|
+
Decline
|
|
65
|
+
</Button>
|
|
66
|
+
<Button onClick={() => setOpened(false)}>Accept Terms</Button>
|
|
67
|
+
</Modal.Footer>
|
|
68
|
+
</Modal>
|
|
69
|
+
<Button onClick={() => setOpened(true)}>Open Scrolling Modal</Button>
|
|
70
|
+
</>
|
|
71
|
+
);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const ScrollingContent: Story = {
|
|
75
|
+
args: {
|
|
76
|
+
title: "Terms and Conditions",
|
|
77
|
+
},
|
|
78
|
+
render: (args) => <ScrollingWrapper {...args} />,
|
|
17
79
|
};
|
|
@@ -1,7 +1,222 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
import {
|
|
3
|
+
Modal as MantineModal,
|
|
4
|
+
type ModalProps as MantineModalProps,
|
|
5
|
+
} from "@mantine/core";
|
|
6
|
+
import {
|
|
7
|
+
filterStylingProps,
|
|
8
|
+
type RecursicaOverStyled,
|
|
9
|
+
} from "../../utils/filterStylingProps";
|
|
10
|
+
import styles from "./Modal.module.css";
|
|
2
11
|
|
|
3
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Properties for the strictly-tokenized Modal component.
|
|
14
|
+
* Native Mantine abstract properties like `size`, `radius`, and `shadow` have been stripped out
|
|
15
|
+
* because they directly conflict with the non-negotiable pixel boundaries defined in the Recursica UI Kit.
|
|
16
|
+
*/
|
|
17
|
+
export type ModalProps = RecursicaOverStyled<
|
|
18
|
+
Omit<MantineModalProps, "size" | "radius" | "shadow">
|
|
19
|
+
>;
|
|
4
20
|
|
|
5
|
-
|
|
6
|
-
|
|
21
|
+
/**
|
|
22
|
+
* The `Modal` component displays a window overlaid on the primary viewport.
|
|
23
|
+
*
|
|
24
|
+
* **Recursica Abstract:**
|
|
25
|
+
* This component acts as a strict structural wrapper around Mantine's `<Modal>`.
|
|
26
|
+
* It automatically enforces Figma UI Kit geometries (`max-width: 960px`, `min-width: 304px`)
|
|
27
|
+
* and handles internal body scrolling explicitly via CSS module overrides.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```tsx
|
|
31
|
+
* <Modal opened={opened} onClose={close} title="Hello World">
|
|
32
|
+
* Modal Content
|
|
33
|
+
* </Modal>
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
const ModalInner = React.forwardRef<HTMLDivElement, ModalProps>(function Modal(
|
|
37
|
+
{
|
|
38
|
+
overStyled = false,
|
|
39
|
+
children,
|
|
40
|
+
title,
|
|
41
|
+
withCloseButton = true,
|
|
42
|
+
overlayProps,
|
|
43
|
+
withOverlay = true,
|
|
44
|
+
closeButtonProps,
|
|
45
|
+
...rest
|
|
46
|
+
},
|
|
47
|
+
ref,
|
|
48
|
+
) {
|
|
49
|
+
const sanitizedProps = filterStylingProps(rest, overStyled);
|
|
50
|
+
|
|
51
|
+
const mergedClassNames: Partial<Record<string, string>> = {
|
|
52
|
+
root: styles.root,
|
|
53
|
+
inner: styles.inner,
|
|
54
|
+
content: styles.content,
|
|
55
|
+
header: styles.header,
|
|
56
|
+
title: styles.title,
|
|
57
|
+
close: styles.close,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const classNamesProp = (sanitizedProps as Record<string, unknown>).classNames;
|
|
61
|
+
if (
|
|
62
|
+
classNamesProp &&
|
|
63
|
+
typeof classNamesProp === "object" &&
|
|
64
|
+
!Array.isArray(classNamesProp)
|
|
65
|
+
) {
|
|
66
|
+
const o = classNamesProp as Record<string, string>;
|
|
67
|
+
Object.keys(o).forEach((key) => {
|
|
68
|
+
if (mergedClassNames[key]) {
|
|
69
|
+
mergedClassNames[key] = `${mergedClassNames[key]} ${o[key]}`;
|
|
70
|
+
} else {
|
|
71
|
+
mergedClassNames[key] = o[key];
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<MantineModal.Root
|
|
78
|
+
ref={ref}
|
|
79
|
+
classNames={mergedClassNames}
|
|
80
|
+
{...(sanitizedProps as unknown as Omit<
|
|
81
|
+
MantineModalProps,
|
|
82
|
+
"size" | "radius" | "shadow"
|
|
83
|
+
>)}
|
|
84
|
+
>
|
|
85
|
+
{withOverlay && <MantineModal.Overlay {...overlayProps} />}
|
|
86
|
+
<MantineModal.Content>
|
|
87
|
+
{(title || withCloseButton) && (
|
|
88
|
+
<MantineModal.Header>
|
|
89
|
+
{title && <MantineModal.Title>{title}</MantineModal.Title>}
|
|
90
|
+
{withCloseButton && (
|
|
91
|
+
<MantineModal.CloseButton {...closeButtonProps} />
|
|
92
|
+
)}
|
|
93
|
+
</MantineModal.Header>
|
|
94
|
+
)}
|
|
95
|
+
<ModalBody>{children}</ModalBody>
|
|
96
|
+
</MantineModal.Content>
|
|
97
|
+
</MantineModal.Root>
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
ModalInner.displayName = "Modal";
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The `Modal.Footer` provides a perfectly padded container for modal actions.
|
|
105
|
+
*
|
|
106
|
+
* **Recursica Abstract:**
|
|
107
|
+
* By default, this container aligns its children to the right (`justify-content: flex-end`)
|
|
108
|
+
* and applies the standard Figma button-gap spacing.
|
|
109
|
+
*
|
|
110
|
+
* > [!IMPORTANT]
|
|
111
|
+
* > **Button Hierarchy:** Recursica design language explicitly requires that the
|
|
112
|
+
* > primary action button MUST be the right-most element, with the secondary
|
|
113
|
+
* > (outline variant) action placed immediately to the left of it.
|
|
114
|
+
*/
|
|
115
|
+
const ModalFooter = React.forwardRef<
|
|
116
|
+
HTMLDivElement,
|
|
117
|
+
React.ComponentPropsWithoutRef<"div">
|
|
118
|
+
>(function ModalFooter({ className, ...rest }, ref) {
|
|
119
|
+
return (
|
|
120
|
+
<div
|
|
121
|
+
ref={ref}
|
|
122
|
+
className={`${styles.footer} ${className || ""}`}
|
|
123
|
+
{...rest}
|
|
124
|
+
/>
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
ModalFooter.displayName = "Modal.Footer";
|
|
128
|
+
|
|
129
|
+
const ModalBody = React.forwardRef<
|
|
130
|
+
HTMLDivElement,
|
|
131
|
+
React.ComponentPropsWithoutRef<typeof MantineModal.Body>
|
|
132
|
+
>(function ModalBody({ className, onScroll, children, ...rest }, ref) {
|
|
133
|
+
const internalRef = React.useRef<HTMLDivElement>(null);
|
|
134
|
+
const [scrolledTop, setScrolledTop] = React.useState(false);
|
|
135
|
+
const [scrolledBottom, setScrolledBottom] = React.useState(false);
|
|
136
|
+
|
|
137
|
+
const checkScroll = React.useCallback(() => {
|
|
138
|
+
if (internalRef.current) {
|
|
139
|
+
const { scrollTop, scrollHeight, clientHeight } = internalRef.current;
|
|
140
|
+
setScrolledTop(scrollTop > 0);
|
|
141
|
+
setScrolledBottom(Math.ceil(scrollTop + clientHeight) < scrollHeight);
|
|
142
|
+
}
|
|
143
|
+
}, []);
|
|
144
|
+
|
|
145
|
+
// Re-check scroll on mount and window resize
|
|
146
|
+
React.useEffect(() => {
|
|
147
|
+
checkScroll();
|
|
148
|
+
window.addEventListener("resize", checkScroll);
|
|
149
|
+
return () => window.removeEventListener("resize", checkScroll);
|
|
150
|
+
}, [checkScroll, children]);
|
|
151
|
+
|
|
152
|
+
const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
|
153
|
+
checkScroll();
|
|
154
|
+
onScroll?.(e);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// Intercept children to pull Modal.Footer out of the scrolling container
|
|
158
|
+
let footer: React.ReactNode = null;
|
|
159
|
+
const bodyChildren: React.ReactNode[] = [];
|
|
160
|
+
|
|
161
|
+
React.Children.forEach(children, (child) => {
|
|
162
|
+
if (
|
|
163
|
+
React.isValidElement(child) &&
|
|
164
|
+
(child.type === ModalFooter ||
|
|
165
|
+
(child.type as React.ComponentType)?.displayName === "Modal.Footer")
|
|
166
|
+
) {
|
|
167
|
+
footer = child;
|
|
168
|
+
} else {
|
|
169
|
+
bodyChildren.push(child);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<MantineModal.Body
|
|
175
|
+
{...rest}
|
|
176
|
+
ref={(node) => {
|
|
177
|
+
if (typeof ref === "function") ref(node);
|
|
178
|
+
else if (ref)
|
|
179
|
+
(ref as React.MutableRefObject<HTMLDivElement | null>).current = node;
|
|
180
|
+
}}
|
|
181
|
+
className={`${styles.bodyWrapper} ${className || ""}`}
|
|
182
|
+
>
|
|
183
|
+
<div
|
|
184
|
+
ref={internalRef}
|
|
185
|
+
onScroll={handleScroll}
|
|
186
|
+
data-scrolled-top={scrolledTop || undefined}
|
|
187
|
+
data-scrolled-bottom={scrolledBottom || undefined}
|
|
188
|
+
className={styles.scrollArea}
|
|
189
|
+
>
|
|
190
|
+
{bodyChildren}
|
|
191
|
+
</div>
|
|
192
|
+
{footer}
|
|
193
|
+
</MantineModal.Body>
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
ModalBody.displayName = "Modal.Body";
|
|
197
|
+
|
|
198
|
+
interface ModalComponent
|
|
199
|
+
extends React.ForwardRefExoticComponent<
|
|
200
|
+
ModalProps & React.RefAttributes<HTMLDivElement>
|
|
201
|
+
> {
|
|
202
|
+
Root: typeof MantineModal.Root;
|
|
203
|
+
Overlay: typeof MantineModal.Overlay;
|
|
204
|
+
Content: typeof MantineModal.Content;
|
|
205
|
+
Header: typeof MantineModal.Header;
|
|
206
|
+
Title: typeof MantineModal.Title;
|
|
207
|
+
CloseButton: typeof MantineModal.CloseButton;
|
|
208
|
+
Body: typeof ModalBody;
|
|
209
|
+
Footer: typeof ModalFooter;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export const Modal = ModalInner as ModalComponent & {
|
|
213
|
+
Footer: typeof ModalFooter;
|
|
7
214
|
};
|
|
215
|
+
Modal.Root = MantineModal.Root;
|
|
216
|
+
Modal.Overlay = MantineModal.Overlay;
|
|
217
|
+
Modal.Content = MantineModal.Content;
|
|
218
|
+
Modal.Header = MantineModal.Header;
|
|
219
|
+
Modal.Title = MantineModal.Title;
|
|
220
|
+
Modal.CloseButton = MantineModal.CloseButton;
|
|
221
|
+
Modal.Body = ModalBody;
|
|
222
|
+
Modal.Footer = ModalFooter;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./Modal";
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Popover – Implementation Notes
|
|
2
|
+
|
|
3
|
+
Decisions and design tweaks strictly tailored for the UI Kit's Popover wrapped against `@mantine/core`. This is a living document that tracks _why_ specific logic decisions exist.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Composable API Preservation (1:1 Mapping)
|
|
8
|
+
|
|
9
|
+
**Decision:** We maintain the exact library composition API structure (`<Popover>`, `<Popover.Target>`, `<Popover.Dropdown>`) as a 1:1 React component mapping.
|
|
10
|
+
|
|
11
|
+
**Implementation:** Mantine's Popover internally manages click detection, open/close state, Floating UI positioning, and portal rendering. By preserving the exact sub-component tree, Recursica safely inherits all of these behaviors without reimplementation.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 2. Popover.Target Pass-Through
|
|
16
|
+
|
|
17
|
+
**Decision:** `Popover.Target` is a transparent pass-through with no styling applied.
|
|
18
|
+
|
|
19
|
+
**Implementation:** The target wrapper exists solely to manage Mantine's ref forwarding and event binding for the trigger element. No `filterStylingProps` or CSS module classes are applied — the trigger's appearance is entirely controlled by whatever component the integrator places inside it (e.g., `<Button>`). This is identical to the `Menu.Target` and `HoverCard.Target` pattern.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 3. Token Namespace: `hover-card-popover`
|
|
24
|
+
|
|
25
|
+
**Decision:** The CSS module exclusively uses variables from the `--recursica_ui-kit_components_hover-card-popover_*` namespace.
|
|
26
|
+
|
|
27
|
+
**Implementation:** The Recursica token system defines a single shared namespace (`hover-card-popover`) for this component and HoverCard. It covers geometry (border-radius, border-size, padding, min/max-width), typography (content-text\_\*), elevation, and layer-aware colors (background, border-color, content).
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 4. Hardcoded Values
|
|
32
|
+
|
|
33
|
+
**Decision:** Two hardcoded values exist in the component.
|
|
34
|
+
|
|
35
|
+
### `border-style: solid` (CSS module)
|
|
36
|
+
|
|
37
|
+
Mantine renders the dropdown using its `Paper` component, which does not set `border-style` natively. Without this hardcoded value, the border-width and border-color tokens would have no visible effect. This is the same pattern used in the Menu component's dropdown.
|
|
38
|
+
|
|
39
|
+
### `arrowSize` defaulted to `16` (Popover.tsx)
|
|
40
|
+
|
|
41
|
+
Mantine's `arrowSize` prop is a JavaScript number used for inline style calculations: it sets `width`, `height`, and a positioning offset (`-arrowSize/2`) directly on the arrow `<div>` element. These inline styles **cannot** be overridden via CSS without `!important`, and the positioning offset has no CSS equivalent. This means the beak size cannot be fully CSS-driven — it is one of the rare cases where a design token value must be mirrored as a JS prop.
|
|
42
|
+
|
|
43
|
+
The default value `16` matches the Recursica `beak-size` token (`--recursica_ui-kit_components_hover-card-popover_properties_beak-size: 16px`). Developers can override `arrowSize` if needed, but should be aware this is a design system concern. If the token value changes, the default in `Popover.tsx` must also be updated.
|
|
44
|
+
|
|
45
|
+
This is documented as an open issue in `docs/COMPONENT_ISSUES.md`.
|
|
46
|
+
|
|
47
|
+
**Note:** Mantine calls this the "arrow"; Recursica calls it the "beak". The Recursica prop `withBeak` (defaulting to `true`) maps to Mantine's `withArrow`. Both `withBeak` and `withArrow` are accepted; `withBeak` takes precedence.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## 5. Minimal CSS Override Philosophy
|
|
52
|
+
|
|
53
|
+
**Decision:** The CSS module only overrides visual design tokens (colors, typography, spacing, borders, shadows). All structural layout properties are deferred to Mantine's native behavior.
|
|
54
|
+
|
|
55
|
+
**Implementation:** Popover is an overlay component where Mantine's native Floating UI positioning and Paper layout are already correct. We avoid setting:
|
|
56
|
+
|
|
57
|
+
- `overflow` on the dropdown (Mantine handles scroll behavior natively)
|
|
58
|
+
- `display`, `position`, `z-index` (Floating UI controls these)
|
|
59
|
+
- `pointer-events` (Mantine manages hover detection across target and dropdown)
|
|
60
|
+
|
|
61
|
+
**Rationale:** Default to Mantine's native behavior and only override what the Recursica token system explicitly needs to control.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## 6. ClassNames Merging on Root
|
|
66
|
+
|
|
67
|
+
**Decision:** The root `Popover` component binds CSS module classes via `classNames` on the Mantine root.
|
|
68
|
+
|
|
69
|
+
**Implementation:** The root component receives `classNames={{ dropdown: styles.dropdown, arrow: styles.arrow }}` and merges any consumer-provided `classNames` when `overStyled` is true. This ensures our token-driven styles are applied to the dropdown panel without wrapper divs, and the consumer's classes are additive.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## 7. Default Position Override
|
|
74
|
+
|
|
75
|
+
**Decision:** Recursica defaults `position` to `"top"`. Mantine defaults to `"bottom"`.
|
|
76
|
+
|
|
77
|
+
**Implementation:** The `position="top"` default is set on the Mantine root element before the prop spread, so developer-provided `position` values still take precedence. This aligns with Recursica's design intent for overlay components to appear above their trigger by default.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/* HARDCODED VALUES:
|
|
2
|
+
- border-style: solid. Structural rendering rule for the dropdown border (Mantine uses Paper
|
|
3
|
+
which may not set border-style natively). Same pattern as Menu and HoverCard.
|
|
4
|
+
- arrowSize defaults to 16 in Popover.tsx. Mantine uses arrowSize for inline width/height
|
|
5
|
+
and positioning (-arrowSize/2) calculations that cannot be CSS-driven. The default matches
|
|
6
|
+
the Recursica beak-size token (16px). See COMPONENT_ISSUES.md for details.
|
|
7
|
+
- All structural layout (display, position, overflow) is deferred to Mantine's native
|
|
8
|
+
behavior. We only override visual design tokens (colors, typography, spacing, borders).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/* ======================================
|
|
12
|
+
DROPDOWN CONTAINER
|
|
13
|
+
====================================== */
|
|
14
|
+
|
|
15
|
+
.dropdown {
|
|
16
|
+
background-color: var(
|
|
17
|
+
--recursica_ui-kit_components_hover-card-popover_colors_background
|
|
18
|
+
);
|
|
19
|
+
border-style: solid; /* HARDCODE: Mantine Paper does not set border-style natively */
|
|
20
|
+
border-width: var(
|
|
21
|
+
--recursica_ui-kit_components_hover-card-popover_properties_border-size
|
|
22
|
+
);
|
|
23
|
+
border-color: var(
|
|
24
|
+
--recursica_ui-kit_components_hover-card-popover_colors_border-color
|
|
25
|
+
);
|
|
26
|
+
border-radius: var(
|
|
27
|
+
--recursica_ui-kit_components_hover-card-popover_properties_border-radius
|
|
28
|
+
);
|
|
29
|
+
box-shadow: var(
|
|
30
|
+
--recursica_ui-kit_components_hover-card-popover_properties_elevation
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
min-width: var(
|
|
34
|
+
--recursica_ui-kit_components_hover-card-popover_properties_min-width
|
|
35
|
+
);
|
|
36
|
+
max-width: var(
|
|
37
|
+
--recursica_ui-kit_components_hover-card-popover_properties_max-width
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
padding: var(
|
|
41
|
+
--recursica_ui-kit_components_hover-card-popover_properties_vertical-padding
|
|
42
|
+
)
|
|
43
|
+
var(
|
|
44
|
+
--recursica_ui-kit_components_hover-card-popover_properties_horizontal-padding
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
/* Typography */
|
|
48
|
+
font-family: var(
|
|
49
|
+
--recursica_ui-kit_components_hover-card-popover_properties_content-text_font-family
|
|
50
|
+
);
|
|
51
|
+
font-size: var(
|
|
52
|
+
--recursica_ui-kit_components_hover-card-popover_properties_content-text_font-size
|
|
53
|
+
);
|
|
54
|
+
font-style: var(
|
|
55
|
+
--recursica_ui-kit_components_hover-card-popover_properties_content-text_font-style
|
|
56
|
+
);
|
|
57
|
+
font-weight: var(
|
|
58
|
+
--recursica_ui-kit_components_hover-card-popover_properties_content-text_font-weight
|
|
59
|
+
);
|
|
60
|
+
letter-spacing: var(
|
|
61
|
+
--recursica_ui-kit_components_hover-card-popover_properties_content-text_letter-spacing
|
|
62
|
+
);
|
|
63
|
+
line-height: var(
|
|
64
|
+
--recursica_ui-kit_components_hover-card-popover_properties_content-text_line-height
|
|
65
|
+
);
|
|
66
|
+
text-decoration: var(
|
|
67
|
+
--recursica_ui-kit_components_hover-card-popover_properties_content-text_text-decoration
|
|
68
|
+
);
|
|
69
|
+
text-transform: var(
|
|
70
|
+
--recursica_ui-kit_components_hover-card-popover_properties_content-text_text-transform
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
color: var(--recursica_ui-kit_components_hover-card-popover_colors_content);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* ======================================
|
|
77
|
+
ARROW / BEAK
|
|
78
|
+
====================================== */
|
|
79
|
+
|
|
80
|
+
.arrow {
|
|
81
|
+
border-color: var(
|
|
82
|
+
--recursica_ui-kit_components_hover-card-popover_colors_border-color
|
|
83
|
+
);
|
|
84
|
+
}
|
|
@@ -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
|
+
import { Stack } from "../Stack/Stack";
|
|
6
|
+
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8
|
+
type PopoverStoryArgs = Record<string, any>;
|
|
9
|
+
|
|
10
|
+
const meta = {
|
|
11
|
+
title: "UI-Kit/Popover",
|
|
12
|
+
component: Popover,
|
|
13
|
+
tags: ["autodocs"],
|
|
14
|
+
parameters: {
|
|
15
|
+
docs: {
|
|
16
|
+
description: {
|
|
17
|
+
component:
|
|
18
|
+
"The `Popover` component is a composable wrapper around Mantine's Popover. It displays a dropdown panel when the user clicks or interacts with a target element.",
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
argTypes: {
|
|
23
|
+
withBeak: {
|
|
24
|
+
control: "boolean",
|
|
25
|
+
description:
|
|
26
|
+
"Whether to display a beak (arrow) pointing from the dropdown to the target.",
|
|
27
|
+
},
|
|
28
|
+
position: {
|
|
29
|
+
control: "select",
|
|
30
|
+
options: [
|
|
31
|
+
"top",
|
|
32
|
+
"top-start",
|
|
33
|
+
"top-end",
|
|
34
|
+
"bottom",
|
|
35
|
+
"bottom-start",
|
|
36
|
+
"bottom-end",
|
|
37
|
+
"left",
|
|
38
|
+
"left-start",
|
|
39
|
+
"left-end",
|
|
40
|
+
"right",
|
|
41
|
+
"right-start",
|
|
42
|
+
"right-end",
|
|
43
|
+
],
|
|
44
|
+
description: "Dropdown position relative to target",
|
|
45
|
+
},
|
|
46
|
+
defaultOpened: {
|
|
47
|
+
control: "boolean",
|
|
48
|
+
description: "Initial opened state",
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
} satisfies Meta<typeof Popover>;
|
|
52
|
+
|
|
53
|
+
export default meta;
|
|
54
|
+
type Story = StoryObj<typeof meta>;
|
|
55
|
+
|
|
56
|
+
export const Default: Story = {
|
|
57
|
+
args: {
|
|
58
|
+
withBeak: true,
|
|
59
|
+
position: "top",
|
|
60
|
+
},
|
|
61
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
62
|
+
render: ({ withLayer, layer, ...args }: PopoverStoryArgs) => {
|
|
63
|
+
return (
|
|
64
|
+
<Popover width={250} {...args}>
|
|
65
|
+
<Popover.Target>
|
|
66
|
+
<Button variant="solid">Toggle Popover</Button>
|
|
67
|
+
</Popover.Target>
|
|
68
|
+
<Popover.Dropdown>
|
|
69
|
+
<Text size="rec-sm">
|
|
70
|
+
This is the popover content. It can contain any elements you want to
|
|
71
|
+
display when the user clicks the target.
|
|
72
|
+
</Text>
|
|
73
|
+
</Popover.Dropdown>
|
|
74
|
+
</Popover>
|
|
75
|
+
);
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const SolidDefault: Story = {
|
|
80
|
+
args: {
|
|
81
|
+
withBeak: true,
|
|
82
|
+
position: "top",
|
|
83
|
+
defaultOpened: true,
|
|
84
|
+
},
|
|
85
|
+
parameters: {
|
|
86
|
+
controls: { disable: true },
|
|
87
|
+
},
|
|
88
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
89
|
+
render: ({ withLayer, layer, ...args }: PopoverStoryArgs) => {
|
|
90
|
+
return (
|
|
91
|
+
<Stack align="center" justify="center" style={{ padding: "100px" }}>
|
|
92
|
+
<Popover width={200} {...args}>
|
|
93
|
+
<Popover.Target>
|
|
94
|
+
<Button variant="solid">Toggle Popover</Button>
|
|
95
|
+
</Popover.Target>
|
|
96
|
+
<Popover.Dropdown>
|
|
97
|
+
<Text size="rec-sm">
|
|
98
|
+
This is a static representation of an opened popover with a beak.
|
|
99
|
+
</Text>
|
|
100
|
+
</Popover.Dropdown>
|
|
101
|
+
</Popover>
|
|
102
|
+
</Stack>
|
|
103
|
+
);
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export const WithoutBeak: Story = {
|
|
108
|
+
args: {
|
|
109
|
+
withBeak: false,
|
|
110
|
+
position: "bottom",
|
|
111
|
+
defaultOpened: true,
|
|
112
|
+
},
|
|
113
|
+
parameters: {
|
|
114
|
+
controls: { disable: true },
|
|
115
|
+
},
|
|
116
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
117
|
+
render: ({ withLayer, layer, ...args }: PopoverStoryArgs) => {
|
|
118
|
+
return (
|
|
119
|
+
<Stack align="center" justify="center" style={{ padding: "100px" }}>
|
|
120
|
+
<Popover width={200} {...args}>
|
|
121
|
+
<Popover.Target>
|
|
122
|
+
<Button variant="outline">Bottom Popover</Button>
|
|
123
|
+
</Popover.Target>
|
|
124
|
+
<Popover.Dropdown>
|
|
125
|
+
<Text size="rec-sm">
|
|
126
|
+
This popover is positioned at the bottom and has no beak.
|
|
127
|
+
</Text>
|
|
128
|
+
</Popover.Dropdown>
|
|
129
|
+
</Popover>
|
|
130
|
+
</Stack>
|
|
131
|
+
);
|
|
132
|
+
},
|
|
133
|
+
};
|