@recursica/mantine-adapter 0.50.6 → 0.52.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 +22 -0
- package/dist/index.d.ts +26 -26
- package/dist/mantine-adapter.cjs +2 -2
- package/dist/mantine-adapter.cjs.map +1 -1
- package/dist/mantine-adapter.css +1 -1
- package/dist/mantine-adapter.js +779 -773
- package/dist/mantine-adapter.js.map +1 -1
- package/package.json +2 -2
- package/src/Introduction.stories.tsx +15 -15
- package/src/OverStyling.tsx +3 -3
- package/src/components/Accordion/ACCORDION_IMPLEMENTATION_NOTES.md +10 -0
- package/src/components/Accordion/Accordion.module.css +5 -1
- package/src/components/AutoComplete/AUTOCOMPLETE_IMPLEMENTATION_NOTES.md +6 -1
- package/src/components/AutoComplete/AutoComplete.module.css +4 -1
- package/src/components/Button/Button.module.css +4 -1
- package/src/components/Button/IMPLEMENTATION_NOTES.md +12 -0
- package/src/components/Card/USAGE.md +1 -1
- package/src/components/Dropdown/DROPDOWN_IMPLEMENTATION_NOTES.md +1 -1
- package/src/components/Dropdown/Dropdown.module.css +4 -1
- package/src/components/Heading/HEADING_IMPLEMENTATION_NOTES.md +18 -0
- package/src/components/Heading/Heading.module.css +13 -0
- package/src/components/{Title/Title.stories.tsx → Heading/Heading.stories.tsx} +13 -13
- package/src/components/Heading/Heading.test.tsx +1 -0
- package/src/components/Heading/Heading.tsx +52 -0
- package/src/components/{Title → Heading}/USAGE.md +5 -5
- package/src/components/Modal/MODAL_IMPLEMENTATION_NOTES.md +6 -1
- package/src/components/Modal/Modal.module.css +4 -1
- package/src/components/Panel/PANEL_IMPLEMENTATION_NOTES.md +10 -0
- package/src/components/Panel/Panel.module.css +4 -1
- package/src/components/Text/TEXT_IMPLEMENTATION_NOTES.md +12 -0
- package/src/components/Text/Text.module.css +13 -0
- package/src/components/Text/Text.stories.tsx +1 -1
- package/src/components/Text/Text.tsx +5 -4
- package/src/components/index.ts +1 -1
- package/src/index.ts +3 -3
- package/src/components/Title/Title.test.tsx +0 -1
- package/src/components/Title/Title.tsx +0 -52
package/package.json
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"url": "git+https://github.com/borderux/recursica.git",
|
|
14
14
|
"directory": "packages/mantine-adapter"
|
|
15
15
|
},
|
|
16
|
-
"version": "0.
|
|
16
|
+
"version": "0.52.0",
|
|
17
17
|
"type": "module",
|
|
18
18
|
"main": "./dist/mantine-adapter.cjs",
|
|
19
19
|
"module": "./dist/mantine-adapter.js",
|
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
"vitest": "^3.2.4"
|
|
96
96
|
},
|
|
97
97
|
"dependencies": {
|
|
98
|
-
"@recursica/adapter-common": "^0.
|
|
98
|
+
"@recursica/adapter-common": "^0.26.0",
|
|
99
99
|
"@recursica/official-release": "^2.8.0"
|
|
100
100
|
},
|
|
101
101
|
"peerDependencies": {
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
Container,
|
|
7
7
|
Stack,
|
|
8
8
|
Group,
|
|
9
|
-
|
|
9
|
+
Heading,
|
|
10
10
|
Text,
|
|
11
11
|
Link,
|
|
12
12
|
Button,
|
|
@@ -21,9 +21,9 @@ function IntroductionContent() {
|
|
|
21
21
|
<Group gap="rec-md" mb="rec-xl" align="flex-start" wrap="nowrap">
|
|
22
22
|
<MantineLogo width={80} height={80} />
|
|
23
23
|
<Stack gap={0}>
|
|
24
|
-
<
|
|
24
|
+
<Heading order={1} m={0}>
|
|
25
25
|
Recursica Design System (Mantine Adapter)
|
|
26
|
-
</
|
|
26
|
+
</Heading>
|
|
27
27
|
<Link href="https://mantine.dev" target="_blank" rel="noreferrer">
|
|
28
28
|
mantine.dev
|
|
29
29
|
</Link>
|
|
@@ -36,9 +36,9 @@ function IntroductionContent() {
|
|
|
36
36
|
</Text>
|
|
37
37
|
|
|
38
38
|
<Stack mb="rec-xl">
|
|
39
|
-
<
|
|
39
|
+
<Heading order={2} mb="rec-md">
|
|
40
40
|
Looking for another UI Kit?
|
|
41
|
-
</
|
|
41
|
+
</Heading>
|
|
42
42
|
<Text mb="rec-md">
|
|
43
43
|
Are you using a different UI Kit (like Material UI)? Recursica
|
|
44
44
|
provides multiple adapters for different frameworks.
|
|
@@ -56,9 +56,9 @@ function IntroductionContent() {
|
|
|
56
56
|
</Stack>
|
|
57
57
|
|
|
58
58
|
<Stack mb="rec-xl">
|
|
59
|
-
<
|
|
59
|
+
<Heading order={2} mb="rec-md">
|
|
60
60
|
Installation
|
|
61
|
-
</
|
|
61
|
+
</Heading>
|
|
62
62
|
<Text mb="rec-md">
|
|
63
63
|
To install the Mantine adapter in your project, run:
|
|
64
64
|
</Text>
|
|
@@ -78,9 +78,9 @@ function IntroductionContent() {
|
|
|
78
78
|
</Stack>
|
|
79
79
|
|
|
80
80
|
<Stack mb="rec-xl">
|
|
81
|
-
<
|
|
81
|
+
<Heading order={2} mb="rec-md">
|
|
82
82
|
Tokens
|
|
83
|
-
</
|
|
83
|
+
</Heading>
|
|
84
84
|
<Text mb="rec-md">
|
|
85
85
|
Raw design tokens (colors, sizes, font weights, opacities, etc.) that
|
|
86
86
|
feed the theme and components. These are the primitive values defined
|
|
@@ -89,9 +89,9 @@ function IntroductionContent() {
|
|
|
89
89
|
</Stack>
|
|
90
90
|
|
|
91
91
|
<Stack mb="rec-xl">
|
|
92
|
-
<
|
|
92
|
+
<Heading order={2} mb="rec-md">
|
|
93
93
|
Theme
|
|
94
|
-
</
|
|
94
|
+
</Heading>
|
|
95
95
|
<Text mb="rec-md">
|
|
96
96
|
Brand and theme layer built on top of tokens. Typography types,
|
|
97
97
|
dimensions, and layout grids are defined here. Theme uses the tokens
|
|
@@ -101,9 +101,9 @@ function IntroductionContent() {
|
|
|
101
101
|
</Stack>
|
|
102
102
|
|
|
103
103
|
<Stack mb="rec-xl">
|
|
104
|
-
<
|
|
104
|
+
<Heading order={2} mb="rec-md">
|
|
105
105
|
Configuring Recursica
|
|
106
|
-
</
|
|
106
|
+
</Heading>
|
|
107
107
|
<Text mb="rec-md">
|
|
108
108
|
To modify the Recursica configuration (tokens, brand, theme), go to{" "}
|
|
109
109
|
<Link href={FORGE_URL} target="_blank" rel="noopener noreferrer">
|
|
@@ -114,9 +114,9 @@ function IntroductionContent() {
|
|
|
114
114
|
</Stack>
|
|
115
115
|
|
|
116
116
|
<Stack mb="rec-xl">
|
|
117
|
-
<
|
|
117
|
+
<Heading order={2} mb="rec-md">
|
|
118
118
|
Documentation
|
|
119
|
-
</
|
|
119
|
+
</Heading>
|
|
120
120
|
<Text mb="rec-md">
|
|
121
121
|
For full documentation, guides, and API reference, visit{" "}
|
|
122
122
|
<Link href={DOCS_URL} target="_blank" rel="noopener noreferrer">
|
package/src/OverStyling.tsx
CHANGED
|
@@ -3,7 +3,7 @@ import { TypographyStylesProvider } from "@mantine/core";
|
|
|
3
3
|
import {
|
|
4
4
|
Container,
|
|
5
5
|
Card,
|
|
6
|
-
|
|
6
|
+
Heading,
|
|
7
7
|
Text as RawText,
|
|
8
8
|
Group,
|
|
9
9
|
Stack,
|
|
@@ -36,9 +36,9 @@ export const OverStylingInfo = () => {
|
|
|
36
36
|
my="rec-xl"
|
|
37
37
|
/>
|
|
38
38
|
|
|
39
|
-
<
|
|
39
|
+
<Heading order={3} mb="rec-md">
|
|
40
40
|
Try It Yourself
|
|
41
|
-
</
|
|
41
|
+
</Heading>
|
|
42
42
|
|
|
43
43
|
<Group mb="rec-xl">
|
|
44
44
|
<Switch
|
|
@@ -60,3 +60,13 @@ and silently override the `leftIcon`-derived element (spread order would let it
|
|
|
60
60
|
to set that slot is through `leftIcon`. Mantine's separate, per-control `chevron` override
|
|
61
61
|
is _not_ omitted — nothing here computes it, so it still passes through safely if a caller
|
|
62
62
|
wants to override the cascaded container-level chevron for a single item.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## `.label` descender clipping (Matt Massey, 2026-08-28)
|
|
67
|
+
|
|
68
|
+
`.label` used plain `overflow: hidden` to make `text-overflow: ellipsis` work, which also clips
|
|
69
|
+
descenders (e.g. the "g" in a long title) whenever `text_line-height` is tighter than the font's
|
|
70
|
+
natural ascent+descent. Switched to `overflow: clip; overflow-clip-margin: 0.35em;` — same
|
|
71
|
+
truncation, but ink can bleed slightly past the line box before it's actually clipped. Project-wide
|
|
72
|
+
fix; see Chip's `CHIP_IMPLEMENTATION_NOTES.md` for the original discovery.
|
|
@@ -211,7 +211,11 @@
|
|
|
211
211
|
text-align: left;
|
|
212
212
|
color: inherit;
|
|
213
213
|
padding: 0; /* Clear Mantine inner padding */
|
|
214
|
-
overflow: hidden
|
|
214
|
+
overflow: clip; /* HARDCODE: `hidden` clipped descenders (e.g. "g") whenever the text_line-height
|
|
215
|
+
token is tighter than the font's natural ascent+descent; `overflow-clip-margin` gives ink a
|
|
216
|
+
small bleed allowance while still clipping genuinely overflowing text — same fix project-wide,
|
|
217
|
+
see Chip/FileInput's IMPLEMENTATION_NOTES.md for the original discovery. */
|
|
218
|
+
overflow-clip-margin: 0.35em;
|
|
215
219
|
white-space: nowrap;
|
|
216
220
|
text-overflow: ellipsis;
|
|
217
221
|
}
|
|
@@ -34,4 +34,9 @@ rendering with our own value comparison.
|
|
|
34
34
|
|
|
35
35
|
## `wrapItemText`
|
|
36
36
|
|
|
37
|
-
`label`/`supportingText` default to single-line truncation with an ellipsis (`.optionText > *` — `overflow:
|
|
37
|
+
`label`/`supportingText` default to single-line truncation with an ellipsis (`.optionText > *` — `overflow: clip; overflow-clip-margin: 0.35em; text-overflow: ellipsis; white-space: nowrap`). Passing `wrapItemText` adds `.optionTextWrap` alongside `.optionText`, which re-enables wrapping (`white-space: normal; overflow-wrap: anywhere`) for both children — same later-cascade-wins mechanism (`.optionTextWrap > *` declared after `.optionText > *`, equal specificity) as the rest of this file's overrides. `renderRichOption`/`renderRichOptionContent` (shared util, both adapters) take `wrapItemText` as a third parameter and combine the two class names when it's true.
|
|
38
|
+
|
|
39
|
+
`.optionText > *` used plain `overflow: hidden` until 2026-08-28 (Matt Massey) — it clipped
|
|
40
|
+
descenders (e.g. "g") whenever `text_line-height` is tighter than the font's natural
|
|
41
|
+
ascent+descent. `overflow-clip-margin` gives ink a small bleed allowance while still clipping
|
|
42
|
+
genuinely overflowing text; same project-wide fix as Chip's `CHIP_IMPLEMENTATION_NOTES.md`.
|
|
@@ -304,7 +304,10 @@
|
|
|
304
304
|
|
|
305
305
|
/* Default: label/supportingText each truncate to a single line with an ellipsis. */
|
|
306
306
|
.optionText > * {
|
|
307
|
-
overflow: hidden
|
|
307
|
+
overflow: clip; /* HARDCODE: `hidden` clips descenders (e.g. "g") when text_line-height is
|
|
308
|
+
tighter than the font's natural ascent+descent; overflow-clip-margin gives ink a small bleed
|
|
309
|
+
allowance while still clipping genuinely overflowing text (see Chip's IMPLEMENTATION_NOTES.md). */
|
|
310
|
+
overflow-clip-margin: 0.35em;
|
|
308
311
|
text-overflow: ellipsis;
|
|
309
312
|
white-space: nowrap;
|
|
310
313
|
}
|
|
@@ -72,7 +72,10 @@
|
|
|
72
72
|
.labelText {
|
|
73
73
|
display: block;
|
|
74
74
|
min-width: 0;
|
|
75
|
-
overflow: hidden
|
|
75
|
+
overflow: clip; /* HARDCODE: `hidden` clips descenders (e.g. "g") when text_line-height is
|
|
76
|
+
tighter than the font's natural ascent+descent; overflow-clip-margin gives ink a small bleed
|
|
77
|
+
allowance while still clipping genuinely overflowing text (see Chip's IMPLEMENTATION_NOTES.md). */
|
|
78
|
+
overflow-clip-margin: 0.35em;
|
|
76
79
|
text-overflow: ellipsis;
|
|
77
80
|
white-space: nowrap;
|
|
78
81
|
font-size: inherit;
|
|
@@ -69,3 +69,15 @@ Mantine's `.mantine-Button-label` flex centering breaks primitive truncation log
|
|
|
69
69
|
**Bug:** with `overStyled` and a custom `className` (surfaced by Tree embedding a `Button` for its expand chevron — see `Tree/IMPLEMENTATION_NOTES.md`), `finalClass` (`` `${styles.root} ${classNameProp}` ``) was set explicitly on `<MantineButton>`, but `{...sanitizedProps}` was spread _after_ it — and `sanitizedProps` still contained the original, unmodified `className` key, since it had only been _read_, never deleted. The later spread would silently overwrite `finalClass` with just the caller's own class. Same bug class as `Dropdown.tsx`/`BareDropdown.tsx` had.
|
|
70
70
|
|
|
71
71
|
**Why it wasn't visible here:** `classNames={{root: mergedClassNames.root, ...}}` is a _separate_ Mantine prop from the plain `className` string, unaffected by the overwrite, and `mergedClassNames.root` always includes `styles.root` independently — so the root element kept its Recursica styling regardless of the bug. mui-adapter's equivalent `Button.tsx` has no such secondary path (`@mui/material` only has a plain `className`), so the identical mistake there was fully visible (chevron rendered in MUI's own default color). Fixed in both regardless, since this is a real latent bug independent of whether it happens to be masked today.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## `.labelText` descender clipping (Matt Massey, 2026-08-28)
|
|
76
|
+
|
|
77
|
+
`.labelText` used plain `overflow: hidden` to make `text-overflow: ellipsis` work, which also
|
|
78
|
+
clips descenders (e.g. the "g" in a long label) whenever `text_line-height` is tighter than the
|
|
79
|
+
font's natural ascent+descent. Switched to `overflow: clip; overflow-clip-margin: 0.35em;` — same
|
|
80
|
+
truncation, but ink can bleed slightly past the line box before it's actually clipped. `.root`'s
|
|
81
|
+
own `overflow: hidden` (the max-width truncation bounding box) is untouched — it has padding
|
|
82
|
+
around the label so it isn't tight against the glyphs the way `.labelText` is. Project-wide fix;
|
|
83
|
+
see Chip's `CHIP_IMPLEMENTATION_NOTES.md` for the original discovery.
|
|
@@ -8,5 +8,5 @@ The `Dropdown` component is mapped explicitly to Mantine's `<Select>` following
|
|
|
8
8
|
4. **Popup Vertical Padding:** The gap above/below the option list inside `.dropdown` is explicitly set from the same `--recursica_..._dropdown_properties_vertical-padding` token the closed control uses — it was previously left to Mantine's own untokenized `--combobox-padding` (4px) default.
|
|
9
9
|
5. **Clear Button Styling:** Mantine's native clear button (`clearButtonProps`, shown when `clearable` + a value are both present) is a bare `CloseButton` with its own hardcoded gray icon/hover styling by default. `Dropdown.tsx` merges in a `.clearButton` class (Dropdown.module.css) that overrides it with the same icon-size/trailing-icon-color/focus-ring tokens as the rest of the right section, using the double-class-selector specificity trick (same as Chip.module.css's `.root.root`) to beat Mantine's own CSS module rule without `!important`.
|
|
10
10
|
6. **Rich Option Content (`leadingIcon`/`supportingText`):** `data` items now accept optional `leadingIcon`/`supportingText` fields, via the shared `RecursicaComboboxItem` type in `@recursica/adapter-common` (see `MANTINE_ADAPTER_RICH_OPTION_DATA.md` at the repo root) — the same type `AutoComplete` and both `mui-adapter` components use, so it's declared once and not redeclared per component/adapter. `label` on that shared type is optional, falling back to `value` (matches Mantine's own runtime default for both `Select` and `Autocomplete`). `Dropdown.tsx` installs a default `renderOption` (`../../utils/renderRichOption.tsx`, shared with `AutoComplete`) that reads `leadingIcon`/`supportingText` straight off Mantine's own parsed `option` — no separate lookup-by-value map is needed, since Mantine's `getParsedComboboxData` passes extra fields on an item through untouched whenever the item already has both `value` and `label`. Because `label` is now optional, `data` is first run through adapter-common's `normalizeComboboxData` (backfilling `label` from `value`) before being handed to Mantine, so items missing `label` don't have their `leadingIcon`/`supportingText` silently dropped by that same parser (see `AutoComplete.tsx`'s identical, longer-standing use of this — `Dropdown`'s `label` used to be required, which is why it didn't need this before). A caller-supplied `renderOption` always wins over the default. New CSS classes (`.optionContent`/`.optionIcon`/`.optionText`/`.optionSupportingText`) reuse the menu-item component's icon/supporting-text tokens — no dedicated dropdown-option tokens exist for either, same reasoning as the `.option[data-selected]` reuse above. The icon is only rendered when `leadingIcon` is present (a conditional child, not a hidden reserved slot), so label/supportingText shift left when there's no icon; `.optionContent`'s `align-items: center` keeps the label vertically centered when there's no `supportingText` to stack under it.
|
|
11
|
-
7. **`wrapItemText`:** `label`/`supportingText` default to single-line truncation with an ellipsis (`.optionText > *` — `overflow:
|
|
11
|
+
7. **`wrapItemText`:** `label`/`supportingText` default to single-line truncation with an ellipsis (`.optionText > *` — `overflow: clip; overflow-clip-margin: 0.35em; text-overflow: ellipsis; white-space: nowrap`). Passing `wrapItemText` adds `.optionTextWrap` alongside `.optionText`, re-enabling wrapping (`white-space: normal; overflow-wrap: anywhere`) for both children — later-cascade-wins, equal specificity, same mechanism as this file's other CSS overrides. `renderRichOption`/`renderRichOptionContent` take `wrapItemText` as a third parameter and combine the two class names when it's true. `.optionText > *` used plain `overflow: hidden` until 2026-08-28 (Matt Massey) — it clipped descenders (e.g. "g") whenever `text_line-height` is tighter than the font's natural ascent+descent; `overflow-clip-margin` gives ink a small bleed allowance while still clipping genuinely overflowing text, same project-wide fix as Chip's `CHIP_IMPLEMENTATION_NOTES.md`.
|
|
12
12
|
8. **Selected Option Highlight (bug fix):** The `.option[data-selected="true"], .option[data-combobox-active="true"]` background/text-color rule (and the matching `.optionIcon`/`.optionSupportingText` color rules) used to key off `data-combobox-selected` instead of `data-combobox-active`. Despite the name, `data-combobox-active` is what Mantine sets when an option's value matches the current field value (`OptionsDropdown.mjs`: `active: checked`, consumed by `ComboboxOption.mjs`'s `mod` map); `data-combobox-selected` is an unrelated, transient keyboard-navigation highlight that Mantine sets/clears imperatively via `element.setAttribute` outside React (`use-combobox.mjs`'s `selectOption`/`clearSelectedItem`), not tied to the chosen value at all. The bug: the real selected option only showed the brand background while being arrow-key-navigated, and lost it entirely on a normal open/close (matches Playwright verification — `data-combobox-active="true"` is present and gets the token background after a plain click-select-reopen, with no keyboard involved).
|
|
@@ -408,7 +408,10 @@
|
|
|
408
408
|
|
|
409
409
|
/* Default: label/supportingText each truncate to a single line with an ellipsis. */
|
|
410
410
|
.optionText > * {
|
|
411
|
-
overflow: hidden
|
|
411
|
+
overflow: clip; /* HARDCODE: `hidden` clips descenders (e.g. "g") when text_line-height is
|
|
412
|
+
tighter than the font's natural ascent+descent; overflow-clip-margin gives ink a small bleed
|
|
413
|
+
allowance while still clipping genuinely overflowing text (see Chip's IMPLEMENTATION_NOTES.md). */
|
|
414
|
+
overflow-clip-margin: 0.35em;
|
|
412
415
|
text-overflow: ellipsis;
|
|
413
416
|
white-space: nowrap;
|
|
414
417
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Heading – Implementation Notes
|
|
2
|
+
|
|
3
|
+
## Renamed from `Title` (Matt Massey, 2026-08-28)
|
|
4
|
+
|
|
5
|
+
**Decision:** Renamed the component from `Title` to `Heading` — designers refer to semantic tags
|
|
6
|
+
(`h1`-`h6`, `span`, `p`), and "Title" wasn't part of that vocabulary. `Heading` + `Text` are now
|
|
7
|
+
Recursica's common typography component names across all adapters. No behavior change.
|
|
8
|
+
|
|
9
|
+
## `.root` `text-wrap: balance` (Matt Massey, 2026-08-28)
|
|
10
|
+
|
|
11
|
+
**Decision:** Added `text-wrap: balance` via a new `Heading.module.css` `.root` class, merged onto
|
|
12
|
+
the typography class for every `order` (h1–h6), alongside the caller's own `className`.
|
|
13
|
+
|
|
14
|
+
**Implementation:** UX asked for more evenly balanced multi-line wrapping instead of a ragged last
|
|
15
|
+
line — headings are the primary intended use case for this property. Not a design token — it's a
|
|
16
|
+
layout algorithm choice, so it's hardcoded rather than pulled from `recursica_variables_scoped.css`.
|
|
17
|
+
Chromium/Firefox only balance up to ~6 lines, which comfortably covers heading text. No fallback
|
|
18
|
+
needed — browsers that don't support the value just ignore the declaration.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* HARDCODED VALUES
|
|
3
|
+
* - `.root` `text-wrap: balance` — not a design token; a layout algorithm choice requested by
|
|
4
|
+
* UX (Matt Massey, 2026-08-28) to even out wrapped line lengths. See HEADING_IMPLEMENTATION_NOTES.md.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
.root {
|
|
8
|
+
/* HARDCODE: balances wrapped line lengths instead of leaving a ragged short last line. Most
|
|
9
|
+
effective on short text; browsers cap balancing at ~6 lines, so long paragraphs silently
|
|
10
|
+
fall back to normal wrapping past that point — no fallback needed, unsupported browsers
|
|
11
|
+
just ignore the declaration. */
|
|
12
|
+
text-wrap: balance;
|
|
13
|
+
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
-
import {
|
|
2
|
+
import { Heading } from "./Heading";
|
|
3
3
|
|
|
4
|
-
const meta: Meta<typeof
|
|
5
|
-
title: "UI-Kit/
|
|
6
|
-
component:
|
|
4
|
+
const meta: Meta<typeof Heading> = {
|
|
5
|
+
title: "UI-Kit/Heading",
|
|
6
|
+
component: Heading,
|
|
7
7
|
tags: ["autodocs"],
|
|
8
8
|
parameters: {
|
|
9
9
|
docs: {
|
|
10
10
|
description: {
|
|
11
11
|
component:
|
|
12
|
-
"The semantic `<
|
|
12
|
+
"The semantic `<Heading>` abstraction intrinsically links pure `h1-h6` tag generation with exact Recursica design boundaries to preserve SEO and screen reader trees uniformly globally.",
|
|
13
13
|
},
|
|
14
14
|
},
|
|
15
15
|
},
|
|
@@ -25,26 +25,26 @@ const meta: Meta<typeof Title> = {
|
|
|
25
25
|
|
|
26
26
|
export default meta;
|
|
27
27
|
|
|
28
|
-
type Story = StoryObj<typeof
|
|
28
|
+
type Story = StoryObj<typeof Heading>;
|
|
29
29
|
|
|
30
30
|
export const Default: Story = {
|
|
31
31
|
args: {
|
|
32
32
|
order: 1,
|
|
33
33
|
children: "Semantic H1 Document Boundary",
|
|
34
34
|
},
|
|
35
|
-
render: ({ ...args }) => <
|
|
35
|
+
render: ({ ...args }) => <Heading {...args} />,
|
|
36
36
|
};
|
|
37
37
|
|
|
38
38
|
export const StaticVariations: Story = {
|
|
39
39
|
args: {},
|
|
40
40
|
render: () => (
|
|
41
41
|
<div style={{ display: "flex", flexDirection: "column", gap: "24px" }}>
|
|
42
|
-
<
|
|
43
|
-
<
|
|
44
|
-
<
|
|
45
|
-
<
|
|
46
|
-
<
|
|
47
|
-
<
|
|
42
|
+
<Heading order={1}>H1 Heading</Heading>
|
|
43
|
+
<Heading order={2}>H2 Heading</Heading>
|
|
44
|
+
<Heading order={3}>H3 Heading</Heading>
|
|
45
|
+
<Heading order={4}>H4 Heading</Heading>
|
|
46
|
+
<Heading order={5}>H5 Heading</Heading>
|
|
47
|
+
<Heading order={6}>H6 Heading</Heading>
|
|
48
48
|
</div>
|
|
49
49
|
),
|
|
50
50
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// Placeholder for future Heading component tests. Add tests here if/when needed.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { forwardRef } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Title as MantineTitle,
|
|
4
|
+
type TitleProps as MantineTitleProps,
|
|
5
|
+
} from "@mantine/core";
|
|
6
|
+
import {
|
|
7
|
+
filterStylingProps,
|
|
8
|
+
omitUnsupportedProps,
|
|
9
|
+
type RecursicaOverStyled,
|
|
10
|
+
} from "../../utils/filterStylingProps";
|
|
11
|
+
|
|
12
|
+
import { type RecursicaHeadingProps } from "@recursica/adapter-common";
|
|
13
|
+
import styles from "./Heading.module.css";
|
|
14
|
+
|
|
15
|
+
export type HeadingProps = RecursicaOverStyled<
|
|
16
|
+
Omit<MantineTitleProps, "size"> & RecursicaHeadingProps
|
|
17
|
+
>;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Enforces highly accessible structural markup utilizing semantic `<h1>` through `<h6>` tags securely bound directly to Recursica typographic scales.
|
|
21
|
+
*/
|
|
22
|
+
export const Heading = forwardRef<HTMLHeadingElement, HeadingProps>(
|
|
23
|
+
function Heading({ overStyled = false, order = 1, ...rest }, ref) {
|
|
24
|
+
// Props this component intentionally doesn't support — deleted at runtime so they can't leak
|
|
25
|
+
// through even if a caller forces them via plain JavaScript, bypassing the `Omit<>` above.
|
|
26
|
+
const UNSUPPORTED_PROPS = [
|
|
27
|
+
"size", // Recursica controls Heading sizing via the `order` prop + typography tokens, not Mantine's native `size`.
|
|
28
|
+
] as const satisfies readonly (keyof MantineTitleProps)[];
|
|
29
|
+
|
|
30
|
+
const sanitizedProps = omitUnsupportedProps(
|
|
31
|
+
filterStylingProps(rest, overStyled),
|
|
32
|
+
UNSUPPORTED_PROPS,
|
|
33
|
+
);
|
|
34
|
+
const classNameProp = (sanitizedProps as Record<string, unknown>)
|
|
35
|
+
.className as string | undefined;
|
|
36
|
+
|
|
37
|
+
const typographyClass = `recursica_brand_typography_h${order}`;
|
|
38
|
+
const mergedClassName = [typographyClass, styles.root, classNameProp]
|
|
39
|
+
.filter(Boolean)
|
|
40
|
+
.join(" ");
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<MantineTitle
|
|
44
|
+
ref={ref}
|
|
45
|
+
{...(sanitizedProps as unknown as MantineTitleProps)}
|
|
46
|
+
order={order}
|
|
47
|
+
className={mergedClassName}
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
50
|
+
},
|
|
51
|
+
);
|
|
52
|
+
Heading.displayName = "Heading";
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Heading - Usage Guide
|
|
2
2
|
|
|
3
|
-
This document describes how to integrate and use the `
|
|
3
|
+
This document describes how to integrate and use the `Heading` component in your projects using `@recursica/mantine-adapter`.
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
## 1. Import Reference
|
|
8
8
|
|
|
9
9
|
```tsx
|
|
10
|
-
import {
|
|
10
|
+
import { Heading } from "@recursica/mantine-adapter";
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
---
|
|
@@ -16,10 +16,10 @@ import { Title } from "@recursica/mantine-adapter";
|
|
|
16
16
|
|
|
17
17
|
```tsx
|
|
18
18
|
import React from "react";
|
|
19
|
-
import {
|
|
19
|
+
import { Heading } from "@recursica/mantine-adapter";
|
|
20
20
|
|
|
21
21
|
export default function Demo() {
|
|
22
|
-
return <
|
|
22
|
+
return <Heading order={1}>This is a Heading 1</Heading>;
|
|
23
23
|
}
|
|
24
24
|
```
|
|
25
25
|
|
|
@@ -16,7 +16,12 @@ Mantine internally handles scroll state natively, dynamically showing/hiding a d
|
|
|
16
16
|
|
|
17
17
|
### 3. Title truncation
|
|
18
18
|
|
|
19
|
-
`.title` truncates with an ellipsis (`overflow:
|
|
19
|
+
`.title` truncates with an ellipsis (`overflow: clip; overflow-clip-margin: 0.35em`, `white-space: nowrap`, `text-overflow: ellipsis`) rather than wrapping. It also needs `flex: 1 1 auto; min-width: 0;` since it's a flex child of `.header` alongside the close button — without `min-width: 0`, a flex item won't shrink below its content's intrinsic width, so ellipsis never engages. `.header`'s `display: flex` is likewise explicit rather than relied upon from Mantine's own header class, so the mui-adapter's plain-`<div>` header gets identical layout.
|
|
20
|
+
|
|
21
|
+
`.title` used plain `overflow: hidden` until 2026-08-28 (Matt Massey) — it clipped descenders
|
|
22
|
+
(e.g. "g" in a long title) whenever `text_line-height` is tighter than the font's natural
|
|
23
|
+
ascent+descent. `overflow-clip-margin` gives ink a small bleed allowance while still clipping
|
|
24
|
+
genuinely overflowing text, same project-wide fix as Chip's `CHIP_IMPLEMENTATION_NOTES.md`.
|
|
20
25
|
|
|
21
26
|
### 4. Width was pinned to Mantine's `md` size, not content-driven
|
|
22
27
|
|
|
@@ -78,7 +78,10 @@
|
|
|
78
78
|
.title {
|
|
79
79
|
flex: 1 1 auto; /* HARDCODE: let the title claim the space between the header edge and the close button */
|
|
80
80
|
min-width: 0; /* HARDCODE: required for text-overflow ellipsis to take effect on a flex child */
|
|
81
|
-
overflow: hidden
|
|
81
|
+
overflow: clip; /* HARDCODE: `hidden` clips descenders (e.g. "g") when text_line-height is
|
|
82
|
+
tighter than the font's natural ascent+descent; overflow-clip-margin gives ink a small bleed
|
|
83
|
+
allowance while still clipping genuinely overflowing text (see Chip's IMPLEMENTATION_NOTES.md). */
|
|
84
|
+
overflow-clip-margin: 0.35em;
|
|
82
85
|
white-space: nowrap;
|
|
83
86
|
text-overflow: ellipsis;
|
|
84
87
|
|
|
@@ -104,3 +104,13 @@ Mantine's Drawer content does not set `border-style` natively. Without this, the
|
|
|
104
104
|
- **Scrollable** — Internal scrollbar enabled when content exceeds the panel height. Header and footer CTAs remain pinned.
|
|
105
105
|
|
|
106
106
|
Mantine's Drawer handles this automatically — the `body` section scrolls when content overflows, while the `header` remains fixed. The `Panel.Footer` uses `margin-top: auto` to stay at the bottom.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## `.titleTruncate` descender clipping (Matt Massey, 2026-08-28)
|
|
111
|
+
|
|
112
|
+
`.titleTruncate` used plain `overflow: hidden` to make `text-overflow: ellipsis` work, which also
|
|
113
|
+
clips descenders (e.g. the "g" in a long title) whenever `text_line-height` is tighter than the
|
|
114
|
+
font's natural ascent+descent. Switched to `overflow: clip; overflow-clip-margin: 0.35em;` — same
|
|
115
|
+
truncation, but ink can bleed slightly past the line box before it's actually clipped.
|
|
116
|
+
Project-wide fix; see Chip's `CHIP_IMPLEMENTATION_NOTES.md` for the original discovery.
|
|
@@ -96,7 +96,10 @@
|
|
|
96
96
|
.titleTruncate {
|
|
97
97
|
composes: title;
|
|
98
98
|
white-space: nowrap;
|
|
99
|
-
overflow: hidden
|
|
99
|
+
overflow: clip; /* HARDCODE: `hidden` clips descenders (e.g. "g") when text_line-height is
|
|
100
|
+
tighter than the font's natural ascent+descent; overflow-clip-margin gives ink a small bleed
|
|
101
|
+
allowance while still clipping genuinely overflowing text (see Chip's IMPLEMENTATION_NOTES.md). */
|
|
102
|
+
overflow-clip-margin: 0.35em;
|
|
100
103
|
text-overflow: ellipsis;
|
|
101
104
|
display: block;
|
|
102
105
|
flex: 1;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Text – Implementation Notes
|
|
2
|
+
|
|
3
|
+
## `.root` `text-wrap: balance` (Matt Massey, 2026-08-28)
|
|
4
|
+
|
|
5
|
+
**Decision:** Added `text-wrap: balance` via a new `Text.module.css` `.root` class, merged onto the
|
|
6
|
+
typography class alongside the caller's own `className`.
|
|
7
|
+
|
|
8
|
+
**Implementation:** UX asked for more evenly balanced multi-line wrapping instead of a ragged last
|
|
9
|
+
line. Not a design token — it's a layout algorithm choice, so it's hardcoded rather than pulled
|
|
10
|
+
from `recursica_variables_scoped.css`. Chromium/Firefox only balance up to ~6 lines; longer
|
|
11
|
+
paragraphs silently fall back to normal wrapping past that point. No fallback needed — browsers
|
|
12
|
+
that don't support the value just ignore the declaration.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* HARDCODED VALUES
|
|
3
|
+
* - `.root` `text-wrap: balance` — not a design token; a layout algorithm choice requested by
|
|
4
|
+
* UX (Matt Massey, 2026-08-28) to even out wrapped line lengths. See TEXT_IMPLEMENTATION_NOTES.md.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
.root {
|
|
8
|
+
/* HARDCODE: balances wrapped line lengths instead of leaving a ragged short last line. Most
|
|
9
|
+
effective on short text; browsers cap balancing at ~6 lines, so long paragraphs silently
|
|
10
|
+
fall back to normal wrapping past that point — no fallback needed, unsupported browsers
|
|
11
|
+
just ignore the declaration. */
|
|
12
|
+
text-wrap: balance;
|
|
13
|
+
}
|
|
@@ -9,7 +9,7 @@ const meta: Meta<typeof Text> = {
|
|
|
9
9
|
docs: {
|
|
10
10
|
description: {
|
|
11
11
|
component:
|
|
12
|
-
"The standard `<Text>` component controls common body sizing scales and implicit paragraphs governed by the active theme layer. For semantic headings (`h1` through `h6`), use `<
|
|
12
|
+
"The standard `<Text>` component controls common body sizing scales and implicit paragraphs governed by the active theme layer. For semantic headings (`h1` through `h6`), use `<Heading>` instead.",
|
|
13
13
|
},
|
|
14
14
|
},
|
|
15
15
|
},
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from "../../utils/filterStylingProps";
|
|
11
11
|
|
|
12
12
|
import { type RecursicaTextProps } from "@recursica/adapter-common";
|
|
13
|
+
import styles from "./Text.module.css";
|
|
13
14
|
|
|
14
15
|
export type TextProps = RecursicaOverStyled<
|
|
15
16
|
Omit<MantineTextProps, "variant"> & RecursicaTextProps
|
|
@@ -24,9 +25,9 @@ const _Text = forwardRef<HTMLDivElement, TextProps>(function Text(
|
|
|
24
25
|
.className as string | undefined;
|
|
25
26
|
|
|
26
27
|
const typographyClass = `recursica_brand_typography_${variant}`;
|
|
27
|
-
const mergedClassName = classNameProp
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
const mergedClassName = [typographyClass, styles.root, classNameProp]
|
|
29
|
+
.filter(Boolean)
|
|
30
|
+
.join(" ");
|
|
30
31
|
|
|
31
32
|
return (
|
|
32
33
|
<MantineText
|
|
@@ -40,7 +41,7 @@ _Text.displayName = "Text";
|
|
|
40
41
|
|
|
41
42
|
/**
|
|
42
43
|
* A generalized typographical wrapper limiting text properties to bounded Recursica UI-kit tokens inherently.
|
|
43
|
-
* Do not use for semantic headings; use the explicit `<
|
|
44
|
+
* Do not use for semantic headings; use the explicit `<Heading>` component for `<h1>` - `<h6>`.
|
|
44
45
|
*
|
|
45
46
|
* Supports polymorphism via the `component` prop or `renderRoot` for custom element rendering.
|
|
46
47
|
* @example
|
package/src/components/index.ts
CHANGED
|
@@ -18,6 +18,7 @@ export * from "./Flex/Flex";
|
|
|
18
18
|
export * from "./FormControlLayout/FormControlLayout";
|
|
19
19
|
export * from "./Grid/Grid";
|
|
20
20
|
export * from "./Group/Group";
|
|
21
|
+
export * from "./Heading/Heading";
|
|
21
22
|
export * from "./HoverCard/HoverCard";
|
|
22
23
|
export * from "./Link/Link";
|
|
23
24
|
export * from "./Loader/Loader";
|
|
@@ -45,7 +46,6 @@ export * from "./TextField/TextField";
|
|
|
45
46
|
export * from "./TimePicker/TimePicker";
|
|
46
47
|
export * from "./Timeline/Timeline";
|
|
47
48
|
export * from "./Timeline/TimelineItem";
|
|
48
|
-
export * from "./Title/Title";
|
|
49
49
|
export * from "./Toast";
|
|
50
50
|
export * from "./Tooltip/Tooltip";
|
|
51
51
|
export * from "./TransferList/TransferList";
|
package/src/index.ts
CHANGED
|
@@ -72,6 +72,9 @@ export const FileUpload = wrapComponent(
|
|
|
72
72
|
export const FormControlLayout = wrapComponent(
|
|
73
73
|
rawComponents.FormControlLayout,
|
|
74
74
|
) as typeof rawComponents.FormControlLayout;
|
|
75
|
+
export const Heading = wrapComponent(
|
|
76
|
+
rawComponents.Heading,
|
|
77
|
+
) as typeof rawComponents.Heading;
|
|
75
78
|
export const HoverCard = wrapComponent(
|
|
76
79
|
rawComponents.HoverCard,
|
|
77
80
|
) as typeof rawComponents.HoverCard;
|
|
@@ -147,9 +150,6 @@ export const Timeline = wrapComponent(
|
|
|
147
150
|
export const TimelineItem = wrapComponent(
|
|
148
151
|
rawComponents.TimelineItem,
|
|
149
152
|
) as typeof rawComponents.TimelineItem;
|
|
150
|
-
export const Title = wrapComponent(
|
|
151
|
-
rawComponents.Title,
|
|
152
|
-
) as typeof rawComponents.Title;
|
|
153
153
|
export const Toast = wrapComponent(
|
|
154
154
|
rawComponents.Toast,
|
|
155
155
|
) as typeof rawComponents.Toast;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
// Placeholder for future Title component tests. Add tests here if/when needed.
|