@recursica/mantine-adapter 0.43.0 → 0.45.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 +42 -3
- 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 +1457 -1398
- package/dist/mantine-adapter.js.map +1 -1
- package/package.json +3 -3
- package/src/components/Breadcrumb/Breadcrumb.module.css +2 -0
- package/src/components/Breadcrumb/Breadcrumb.stories.tsx +0 -6
- package/src/components/Breadcrumb/Breadcrumb.tsx +16 -0
- package/src/components/Dropdown/DROPDOWN_IMPLEMENTATION_NOTES.md +2 -0
- package/src/components/Dropdown/Dropdown.module.css +61 -4
- package/src/components/Dropdown/Dropdown.stories.tsx +21 -0
- package/src/components/Dropdown/Dropdown.tsx +17 -0
- package/src/components/Dropdown/USAGE.md +1 -0
- package/src/components/Link/IMPLEMENTATION_NOTES.md +1 -1
- package/src/components/Link/Link.module.css +4 -0
- package/src/components/Menu/Menu.stories.tsx +9 -1
- package/src/components/Table/TABLE_IMPLEMENTATION_NOTES.md +67 -0
- package/src/components/Table/Table.icons.tsx +40 -0
- package/src/components/Table/Table.stories.tsx +93 -0
- package/src/components/Table/Table.tsx +46 -8
- package/src/components/Table/USAGE.md +35 -0
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.45.0",
|
|
17
17
|
"type": "module",
|
|
18
18
|
"main": "./dist/mantine-adapter.cjs",
|
|
19
19
|
"module": "./dist/mantine-adapter.js",
|
|
@@ -94,8 +94,8 @@
|
|
|
94
94
|
"vitest": "^3.2.4"
|
|
95
95
|
},
|
|
96
96
|
"dependencies": {
|
|
97
|
-
"@recursica/adapter-common": "
|
|
98
|
-
"@recursica/official-release": "
|
|
97
|
+
"@recursica/adapter-common": "^0.20.0",
|
|
98
|
+
"@recursica/official-release": "^2.8.0"
|
|
99
99
|
},
|
|
100
100
|
"peerDependencies": {
|
|
101
101
|
"@mantine/core": "^8.0.0",
|
|
@@ -56,12 +56,6 @@ export const Default: Story = {
|
|
|
56
56
|
},
|
|
57
57
|
};
|
|
58
58
|
|
|
59
|
-
export const StaticExample: Story = {
|
|
60
|
-
args: {
|
|
61
|
-
items: ["Store", "Electronics", "Computers", "Laptops"],
|
|
62
|
-
},
|
|
63
|
-
};
|
|
64
|
-
|
|
65
59
|
export const CustomSeparator: Story = {
|
|
66
60
|
args: {
|
|
67
61
|
items: ["Root", "Branch", "Leaf"],
|
|
@@ -35,6 +35,21 @@ export const Breadcrumb = forwardRef<HTMLDivElement, BreadcrumbProps>(
|
|
|
35
35
|
| undefined,
|
|
36
36
|
);
|
|
37
37
|
|
|
38
|
+
// Mantine's Breadcrumbs, unlike MUI's, doesn't hide the separator from screen
|
|
39
|
+
// readers by default — it's a plain text node. Match MUI's built-in aria-hidden,
|
|
40
|
+
// letting a caller override it via attributes.separator["aria-hidden"] if needed.
|
|
41
|
+
const callerAttributes = (sanitizedProps as Record<string, unknown>)
|
|
42
|
+
.attributes as
|
|
43
|
+
| Partial<Record<string, Record<string, unknown>>>
|
|
44
|
+
| undefined;
|
|
45
|
+
const mergedAttributes = {
|
|
46
|
+
...callerAttributes,
|
|
47
|
+
separator: {
|
|
48
|
+
"aria-hidden": true,
|
|
49
|
+
...callerAttributes?.separator,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
38
53
|
const classNameProp = (sanitizedProps as Record<string, unknown>)
|
|
39
54
|
.className as string | undefined;
|
|
40
55
|
const finalClass = classNameProp
|
|
@@ -47,6 +62,7 @@ export const Breadcrumb = forwardRef<HTMLDivElement, BreadcrumbProps>(
|
|
|
47
62
|
{...(sanitizedProps as unknown as MantineBreadcrumbsProps)}
|
|
48
63
|
className={finalClass}
|
|
49
64
|
classNames={mergedClassNames}
|
|
65
|
+
attributes={mergedAttributes}
|
|
50
66
|
/>
|
|
51
67
|
);
|
|
52
68
|
},
|
|
@@ -5,3 +5,5 @@ The `Dropdown` component is mapped explicitly to Mantine's `<Select>` following
|
|
|
5
5
|
1. **Naked Primitive Mapping:** Mantine's `Select` natively executes macro-label generation. To decouple it, we explicitly disable internal labels (`label={undefined}`) and inject it purely inside our generic `FormControlWrapper`.
|
|
6
6
|
2. **Strict Dropdown Design Tokens:** The adapter implements strictly sandboxed styling utilizing only `--recursica_ui-kit_components_dropdown_...` variables. It explicitly does NOT inherit general `text-field` tokens despite geometric similarities, ensuring dropdown menus can be themed independently.
|
|
7
7
|
3. **Dropdown Appendages:** To correctly map Mantine's detached Popover `.dropdown` and list `.option` items, we targeted focus and geometric bindings appending standard padding structures matched to the dropdown height overrides dynamically into our `Dropdown.module.css`.
|
|
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
|
+
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`.
|
|
@@ -169,6 +169,39 @@
|
|
|
169
169
|
);
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
/* Mantine's own clear button (rendered via `clearButtonProps`, see Dropdown.tsx) is a bare
|
|
173
|
+
`CloseButton` by default — its own module CSS hardcodes a gray icon color and a circular gray
|
|
174
|
+
hover background, neither of which is Recursica-token-driven. The double class selector (same
|
|
175
|
+
specificity-boosting trick as Chip.module.css's `.root.root`) beats that default without
|
|
176
|
+
needing `!important`. Same icon-size/trailing-icon tokens as the rest of this section, same
|
|
177
|
+
focus-visible ring technique as Chip's own `.removeIcon` — the established inline icon-button
|
|
178
|
+
pattern elsewhere in this adapter. */
|
|
179
|
+
.clearButton.clearButton {
|
|
180
|
+
width: var(--recursica_ui-kit_components_dropdown_properties_icon-size);
|
|
181
|
+
height: var(--recursica_ui-kit_components_dropdown_properties_icon-size);
|
|
182
|
+
min-width: var(--recursica_ui-kit_components_dropdown_properties_icon-size);
|
|
183
|
+
min-height: var(--recursica_ui-kit_components_dropdown_properties_icon-size);
|
|
184
|
+
border-radius: 50%;
|
|
185
|
+
background: transparent;
|
|
186
|
+
color: var(
|
|
187
|
+
--recursica_ui-kit_components_dropdown_properties_colors_trailing-icon
|
|
188
|
+
);
|
|
189
|
+
outline: none;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.clearButton.clearButton:hover {
|
|
193
|
+
background: transparent;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.clearButton.clearButton:focus-visible {
|
|
197
|
+
box-shadow:
|
|
198
|
+
0 0 0 var(--recursica_brand_states_focus_border-size)
|
|
199
|
+
var(--recursica_brand_states_focus_color),
|
|
200
|
+
0 0 var(--recursica_brand_states_focus_blur)
|
|
201
|
+
var(--recursica_brand_states_focus_margin)
|
|
202
|
+
var(--recursica_brand_states_focus_color);
|
|
203
|
+
}
|
|
204
|
+
|
|
172
205
|
/* -------------------------------------
|
|
173
206
|
STATE CASCADE ARCHITECTURE
|
|
174
207
|
-------------------------------------- */
|
|
@@ -254,6 +287,17 @@
|
|
|
254
287
|
--recursica_ui-kit_components_dropdown_properties_border-radius
|
|
255
288
|
);
|
|
256
289
|
overflow: hidden;
|
|
290
|
+
|
|
291
|
+
/* The visible top/bottom gap before/after the option list was previously left to Mantine's own
|
|
292
|
+
untokenized `--combobox-padding` default (4px) — wire it to the same dropdown vertical-padding
|
|
293
|
+
token the closed control itself uses instead (mirrors the mui-adapter fix to the equivalent
|
|
294
|
+
gap there, which came from MUI's own untokenized 8px `.MuiList-padding` default). */
|
|
295
|
+
padding-top: var(
|
|
296
|
+
--recursica_ui-kit_components_dropdown_properties_vertical-padding
|
|
297
|
+
);
|
|
298
|
+
padding-bottom: var(
|
|
299
|
+
--recursica_ui-kit_components_dropdown_properties_vertical-padding
|
|
300
|
+
);
|
|
257
301
|
}
|
|
258
302
|
|
|
259
303
|
.option {
|
|
@@ -277,10 +321,8 @@
|
|
|
277
321
|
cursor: pointer;
|
|
278
322
|
}
|
|
279
323
|
|
|
280
|
-
/* No per-option
|
|
281
|
-
|
|
282
|
-
.option[data-selected="true"],
|
|
283
|
-
.option[data-combobox-selected="true"],
|
|
324
|
+
/* No per-option hovered token exists in the schema; use the generic overlay tint (same technique
|
|
325
|
+
as Table's row hover) to highlight a hovered-but-not-selected option. */
|
|
284
326
|
.option[data-hovered="true"],
|
|
285
327
|
.option:hover {
|
|
286
328
|
background-color: color-mix(
|
|
@@ -290,3 +332,18 @@
|
|
|
290
332
|
transparent
|
|
291
333
|
);
|
|
292
334
|
}
|
|
335
|
+
|
|
336
|
+
/* No dedicated dropdown-option "selected" token exists in the schema either, but the menu-item
|
|
337
|
+
component's selected-state colors are the closest real token family for the same concept (a
|
|
338
|
+
selected row in a list) — reused here rather than the neutral hover tint above, which is what
|
|
339
|
+
previously made the selected option read as grey instead of Recursica's brand color. Matches
|
|
340
|
+
the mui-adapter fix for the same gap. */
|
|
341
|
+
.option[data-selected="true"],
|
|
342
|
+
.option[data-combobox-selected="true"] {
|
|
343
|
+
background-color: var(
|
|
344
|
+
--recursica_ui-kit_components_menu-item_variants_selection-states_selected_properties_colors_background-color
|
|
345
|
+
);
|
|
346
|
+
color: var(
|
|
347
|
+
--recursica_ui-kit_components_menu-item_variants_selection-states_selected_properties_colors_text-color
|
|
348
|
+
);
|
|
349
|
+
}
|
|
@@ -65,6 +65,27 @@ export const SearchableClearable: Story = {
|
|
|
65
65
|
},
|
|
66
66
|
};
|
|
67
67
|
|
|
68
|
+
export const WithLeadingIcon: Story = {
|
|
69
|
+
args: {
|
|
70
|
+
label: "Destination",
|
|
71
|
+
leftSection: (
|
|
72
|
+
<svg
|
|
73
|
+
width="24"
|
|
74
|
+
height="24"
|
|
75
|
+
viewBox="0 0 24 24"
|
|
76
|
+
fill="none"
|
|
77
|
+
stroke="currentColor"
|
|
78
|
+
strokeWidth="2"
|
|
79
|
+
strokeLinecap="round"
|
|
80
|
+
strokeLinejoin="round"
|
|
81
|
+
>
|
|
82
|
+
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path>
|
|
83
|
+
<circle cx="12" cy="10" r="3"></circle>
|
|
84
|
+
</svg>
|
|
85
|
+
),
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
68
89
|
export const StaticError: Story = {
|
|
69
90
|
args: {
|
|
70
91
|
error: "You must choose a valid destination.",
|
|
@@ -68,6 +68,7 @@ export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
|
|
|
68
68
|
value,
|
|
69
69
|
defaultValue,
|
|
70
70
|
data,
|
|
71
|
+
clearButtonProps,
|
|
71
72
|
...rest
|
|
72
73
|
} = props;
|
|
73
74
|
const sanitizedProps = omitUnsupportedProps(
|
|
@@ -76,6 +77,21 @@ export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
|
|
|
76
77
|
);
|
|
77
78
|
const restRecord = sanitizedProps as Record<string, unknown>;
|
|
78
79
|
|
|
80
|
+
// Mantine's own clear button (rendered when `clearable` + a value are both present) otherwise
|
|
81
|
+
// renders unstyled — bare `CloseButton` defaults, no Recursica icon-button treatment. Merge in
|
|
82
|
+
// our own class (see `.clearButton` in Dropdown.module.css) alongside anything the caller
|
|
83
|
+
// already passed, same merge shape as `mergeClassNames` but for a single `className` string
|
|
84
|
+
// rather than a per-slot classNames map.
|
|
85
|
+
const consumerClearButtonProps = clearButtonProps as
|
|
86
|
+
| Record<string, unknown>
|
|
87
|
+
| undefined;
|
|
88
|
+
const mergedClearButtonProps = {
|
|
89
|
+
...consumerClearButtonProps,
|
|
90
|
+
className: consumerClearButtonProps?.className
|
|
91
|
+
? `${styles.clearButton} ${consumerClearButtonProps.className as string}`
|
|
92
|
+
: styles.clearButton,
|
|
93
|
+
};
|
|
94
|
+
|
|
79
95
|
// Securely map core native blocks down ensuring nested CSS modules map precisely
|
|
80
96
|
const mergedClassNames = mergeClassNames(
|
|
81
97
|
{
|
|
@@ -139,6 +155,7 @@ export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
|
|
|
139
155
|
value={value}
|
|
140
156
|
defaultValue={defaultValue}
|
|
141
157
|
data={(data as unknown as MantineSelectProps["data"]) || []}
|
|
158
|
+
clearButtonProps={mergedClearButtonProps}
|
|
142
159
|
label={undefined}
|
|
143
160
|
description={undefined}
|
|
144
161
|
error={undefined}
|
|
@@ -45,3 +45,4 @@ All Recursica components in the `@recursica/mantine-adapter` package adhere stri
|
|
|
45
45
|
## 4. Notes
|
|
46
46
|
|
|
47
47
|
- `Dropdown` is styled independently from `TextField`; even though they look similar, they are themed using separate design tokens.
|
|
48
|
+
- Pass `leftSection` for a leading icon, and `clearable` (with a value present) to show a clear button — both render using the dropdown's own icon-color tokens.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## States in Design Tokens
|
|
4
4
|
|
|
5
|
-
The design tokens (`recursica_variables_scoped.css`) provide the base link styling directly on `--recursica_ui-kit_components_link_properties_*` (no distinct `default` state), plus a `visited` variant that overrides only `colors_text-color`/`colors_icon-color`.
|
|
5
|
+
The design tokens (`recursica_variables_scoped.css`) provide the base link styling directly on `--recursica_ui-kit_components_link_properties_*` (no distinct `default` state), plus a `visited` variant that overrides only `colors_text-color`/`colors_icon-color`. Hover is not a per-component `link_variants_states_hover_*` token; it's driven by the brand-level `--recursica_brand_states_link_decoration` token (resolves to `underline`), applied on `.root:hover`. Earlier this token was exported as an invalid quoted string (`"underline"`), which silently failed and was mistaken for the token not existing at all — now fixed upstream to a bare keyword. There are no tokens for `active` or `focus` either; the component relies on the browser's default focus outline for accessibility unless overridden by a global reset.
|
|
6
6
|
|
|
7
7
|
## Overriding Mantine's underline Prop
|
|
8
8
|
|
|
@@ -199,7 +199,9 @@ export const Default: Story = {
|
|
|
199
199
|
position: "bottom-start",
|
|
200
200
|
withArrow: false,
|
|
201
201
|
offset: 5,
|
|
202
|
-
|
|
202
|
+
// Rendered open by default so this story is diffable against the MUI adapter
|
|
203
|
+
// without an interaction step.
|
|
204
|
+
opened: true,
|
|
203
205
|
},
|
|
204
206
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
205
207
|
render: ({ withLayer, layer, ...args }: MenuStoryArgs) => {
|
|
@@ -230,6 +232,9 @@ export const Default: Story = {
|
|
|
230
232
|
export const WithDisabledItems: Story = {
|
|
231
233
|
args: {
|
|
232
234
|
position: "bottom-start",
|
|
235
|
+
// Rendered open by default so this story is diffable against the MUI adapter
|
|
236
|
+
// without an interaction step.
|
|
237
|
+
opened: true,
|
|
233
238
|
},
|
|
234
239
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
235
240
|
render: ({ withLayer, layer, ...args }: MenuStoryArgs) => {
|
|
@@ -258,6 +263,9 @@ export const WithSubmenus: Story = {
|
|
|
258
263
|
args: {
|
|
259
264
|
position: "bottom-start",
|
|
260
265
|
width: 200,
|
|
266
|
+
// Rendered open by default so this story is diffable against the MUI adapter
|
|
267
|
+
// without an interaction step.
|
|
268
|
+
opened: true,
|
|
261
269
|
},
|
|
262
270
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
263
271
|
render: ({ withLayer, layer, ...args }: MenuStoryArgs) => {
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Table — Implementation Notes (Internal)
|
|
2
|
+
|
|
3
|
+
## Background
|
|
4
|
+
|
|
5
|
+
Table.module.css already had full CSS for header/cell/footer sorted, disabled, currency, and
|
|
6
|
+
selected states, keyed off `data-sorted`/`data-disabled`/`data-currency`/`data-selected`
|
|
7
|
+
attributes — but nothing in Table.tsx ever set those attributes, so the states were unreachable
|
|
8
|
+
and their tokens showed as "unused" in the token analyzer. This pass adds the props that
|
|
9
|
+
actually set them; it does not add any new CSS selectors for those states.
|
|
10
|
+
|
|
11
|
+
## Why not the 4-component split from the Forge reference implementation
|
|
12
|
+
|
|
13
|
+
Forge's Mantine Table reference (reviewed 2026-08-22) splits `Table`/`TableHeader`/`TableCell`/
|
|
14
|
+
`TableFooter` into four separate top-level components, each computing CSS custom properties in
|
|
15
|
+
JS and injecting them via inline `style`. We deliberately did not port that:
|
|
16
|
+
|
|
17
|
+
- It bypasses `filterStylingProps`/`overStyled` and computes styling values in TSX instead of
|
|
18
|
+
referencing tokens directly in the CSS module — both are hard "no" per
|
|
19
|
+
`adapter-common/docs/COMPONENT_DEV_GUIDE.md` (§1 "No custom properties set from TSX for
|
|
20
|
+
styling", §3.1 `overStyled`/`filterStylingProps`).
|
|
21
|
+
- It uses a `layer`-scoped token naming convention (`properties_colors_layer-0_...`) that
|
|
22
|
+
doesn't exist in this repo's tokens and isn't the sanctioned way to handle layers here (layer
|
|
23
|
+
is set only by wrapping in `<Layer>`, never a component prop — see the canonical guide's
|
|
24
|
+
"Layers" section).
|
|
25
|
+
- Per direction from Matt (2026-08-22): keep the existing dot-notation composition
|
|
26
|
+
(`Table.Th`/`Table.Td`/`Table.Tr`/`Table.Tfoot`/etc., mirroring Mantine's own API) rather than
|
|
27
|
+
introducing new component names — "align with the underlying UI-kit's components, but style
|
|
28
|
+
with Recursica."
|
|
29
|
+
|
|
30
|
+
Instead, `Table.Th`/`Table.Td`/`Table.Tr` gained real Recursica props (`sorted`, `disabled`,
|
|
31
|
+
`variant`, `selected`) that set `data-*` attributes on themselves, activating the CSS that was
|
|
32
|
+
already there.
|
|
33
|
+
|
|
34
|
+
## Footer cells reuse `Table.Td`, not a new component
|
|
35
|
+
|
|
36
|
+
There's no separate "footer cell" component. A `Table.Td` nested inside `Table.Tfoot` picks up
|
|
37
|
+
the footer-specific CSS automatically via the existing `.root tfoot td` structural selector in
|
|
38
|
+
Table.module.css — `variant="currency"` and `disabled` work identically whether the `Table.Td`
|
|
39
|
+
is in `Table.Tbody` or `Table.Tfoot`.
|
|
40
|
+
|
|
41
|
+
## Sort icon is rendered by `Table.Th` itself, not composed by the consumer
|
|
42
|
+
|
|
43
|
+
Mantine's `Table` has no sortable-header primitive (unlike MUI's `TableSortLabel`), so
|
|
44
|
+
`Table.Th` renders its own chevron (`Table.icons.tsx`, plain inline SVGs — no icon package
|
|
45
|
+
dependency, same convention as `DatePicker.icons.tsx`) when `sorted` is `"asc"`/`"desc"`, sized
|
|
46
|
+
via the existing `.sortIcon` CSS rule. Also sets `aria-sort` for accessibility (Mantine doesn't
|
|
47
|
+
set this itself). This is intentionally asymmetric with `mui-adapter`, where sorting is composed
|
|
48
|
+
via `Table.SortLabel` instead — see that adapter's own implementation notes.
|
|
49
|
+
|
|
50
|
+
## Selectable rows (checkboxes)
|
|
51
|
+
|
|
52
|
+
Neither Mantine's `Table` nor `@mui/material`'s `Table` has a built-in checkbox-driven row
|
|
53
|
+
selection feature (confirmed 2026-08-22 — MUI's own docs treat it as a compose-it-yourself
|
|
54
|
+
recipe with a plain `Checkbox`; the only MUI product with built-in selection is
|
|
55
|
+
`@mui/x-data-grid`, not installed here). `Table.Tr`'s `selected` prop only controls the
|
|
56
|
+
selected-row background/token state; wiring an actual checkbox column is a separate,
|
|
57
|
+
not-yet-scoped ask.
|
|
58
|
+
|
|
59
|
+
## Deliberately not changed
|
|
60
|
+
|
|
61
|
+
- **No wrapper divs** — Forge wraps the table in two divs to get a scrollable bordered
|
|
62
|
+
container. The canonical guide forbids wrapper divs; `Table.ScrollContainer` (already wraps
|
|
63
|
+
Mantine's own `Table.ScrollContainer`) is the sanctioned way to get a scrolling table.
|
|
64
|
+
- **Hover compositing, striping direction/row parity, row-padding semantics** — Forge diverges
|
|
65
|
+
from our current CSS on these (see the 2026-08-22 review), but none of them are confirmed
|
|
66
|
+
bugs vs. intentional design choices already baked into Figma's tokens. Left as-is; flag to
|
|
67
|
+
design if a change is wanted.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sort-direction indicators for `Table.Th`. Sized/colored entirely via the `.sortIcon` rule in
|
|
5
|
+
* Table.module.css (currentColor + the header's own text-color token) — no width/height/color
|
|
6
|
+
* hardcoded here on purpose, same convention as DatePicker.icons.tsx.
|
|
7
|
+
*/
|
|
8
|
+
export function ChevronUpIcon(props: React.SVGProps<SVGSVGElement>) {
|
|
9
|
+
return (
|
|
10
|
+
<svg
|
|
11
|
+
viewBox="0 0 24 24"
|
|
12
|
+
fill="none"
|
|
13
|
+
stroke="currentColor"
|
|
14
|
+
strokeWidth="2"
|
|
15
|
+
strokeLinecap="round"
|
|
16
|
+
strokeLinejoin="round"
|
|
17
|
+
aria-hidden
|
|
18
|
+
{...props}
|
|
19
|
+
>
|
|
20
|
+
<polyline points="18 15 12 9 6 15" />
|
|
21
|
+
</svg>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function ChevronDownIcon(props: React.SVGProps<SVGSVGElement>) {
|
|
26
|
+
return (
|
|
27
|
+
<svg
|
|
28
|
+
viewBox="0 0 24 24"
|
|
29
|
+
fill="none"
|
|
30
|
+
stroke="currentColor"
|
|
31
|
+
strokeWidth="2"
|
|
32
|
+
strokeLinecap="round"
|
|
33
|
+
strokeLinejoin="round"
|
|
34
|
+
aria-hidden
|
|
35
|
+
{...props}
|
|
36
|
+
>
|
|
37
|
+
<polyline points="6 9 12 15 18 9" />
|
|
38
|
+
</svg>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -45,3 +45,96 @@ export const Default: Story = {
|
|
|
45
45
|
);
|
|
46
46
|
},
|
|
47
47
|
};
|
|
48
|
+
|
|
49
|
+
export const SortedColumn: Story = {
|
|
50
|
+
render: () => {
|
|
51
|
+
const sorted = [...elements].sort((a, b) => a.mass - b.mass);
|
|
52
|
+
const rows = sorted.map((element) => (
|
|
53
|
+
<Table.Tr key={element.name}>
|
|
54
|
+
<Table.Td>{element.position}</Table.Td>
|
|
55
|
+
<Table.Td>{element.name}</Table.Td>
|
|
56
|
+
<Table.Td>{element.symbol}</Table.Td>
|
|
57
|
+
<Table.Td>{element.mass}</Table.Td>
|
|
58
|
+
</Table.Tr>
|
|
59
|
+
));
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<Table>
|
|
63
|
+
<Table.Thead>
|
|
64
|
+
<Table.Tr>
|
|
65
|
+
<Table.Th>Element position</Table.Th>
|
|
66
|
+
<Table.Th>Element name</Table.Th>
|
|
67
|
+
<Table.Th>Symbol</Table.Th>
|
|
68
|
+
<Table.Th sorted="asc">Atomic mass</Table.Th>
|
|
69
|
+
</Table.Tr>
|
|
70
|
+
</Table.Thead>
|
|
71
|
+
<Table.Tbody>{rows}</Table.Tbody>
|
|
72
|
+
</Table>
|
|
73
|
+
);
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const SelectedAndDisabledRows: Story = {
|
|
78
|
+
render: () => (
|
|
79
|
+
<Table>
|
|
80
|
+
<Table.Thead>
|
|
81
|
+
<Table.Tr>
|
|
82
|
+
<Table.Th>Element position</Table.Th>
|
|
83
|
+
<Table.Th>Element name</Table.Th>
|
|
84
|
+
<Table.Th>Symbol</Table.Th>
|
|
85
|
+
<Table.Th>Atomic mass</Table.Th>
|
|
86
|
+
</Table.Tr>
|
|
87
|
+
</Table.Thead>
|
|
88
|
+
<Table.Tbody>
|
|
89
|
+
{elements.map((element, index) => (
|
|
90
|
+
<Table.Tr
|
|
91
|
+
key={element.name}
|
|
92
|
+
selected={index === 0}
|
|
93
|
+
disabled={index === elements.length - 1}
|
|
94
|
+
>
|
|
95
|
+
<Table.Td>{element.position}</Table.Td>
|
|
96
|
+
<Table.Td>{element.name}</Table.Td>
|
|
97
|
+
<Table.Td>{element.symbol}</Table.Td>
|
|
98
|
+
<Table.Td>{element.mass}</Table.Td>
|
|
99
|
+
</Table.Tr>
|
|
100
|
+
))}
|
|
101
|
+
</Table.Tbody>
|
|
102
|
+
</Table>
|
|
103
|
+
),
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const CurrencyColumnWithFooter: Story = {
|
|
107
|
+
render: () => {
|
|
108
|
+
const prices = [
|
|
109
|
+
{ item: "Widget", price: 19.99 },
|
|
110
|
+
{ item: "Gadget", price: 49.5 },
|
|
111
|
+
{ item: "Gizmo", price: 9.25 },
|
|
112
|
+
];
|
|
113
|
+
const total = prices.reduce((sum, row) => sum + row.price, 0);
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<Table>
|
|
117
|
+
<Table.Thead>
|
|
118
|
+
<Table.Tr>
|
|
119
|
+
<Table.Th>Item</Table.Th>
|
|
120
|
+
<Table.Th>Price</Table.Th>
|
|
121
|
+
</Table.Tr>
|
|
122
|
+
</Table.Thead>
|
|
123
|
+
<Table.Tbody>
|
|
124
|
+
{prices.map((row) => (
|
|
125
|
+
<Table.Tr key={row.item}>
|
|
126
|
+
<Table.Td>{row.item}</Table.Td>
|
|
127
|
+
<Table.Td variant="currency">${row.price.toFixed(2)}</Table.Td>
|
|
128
|
+
</Table.Tr>
|
|
129
|
+
))}
|
|
130
|
+
</Table.Tbody>
|
|
131
|
+
<Table.Tfoot>
|
|
132
|
+
<Table.Tr>
|
|
133
|
+
<Table.Td>Total</Table.Td>
|
|
134
|
+
<Table.Td variant="currency">${total.toFixed(2)}</Table.Td>
|
|
135
|
+
</Table.Tr>
|
|
136
|
+
</Table.Tfoot>
|
|
137
|
+
</Table>
|
|
138
|
+
);
|
|
139
|
+
},
|
|
140
|
+
};
|
|
@@ -15,8 +15,14 @@ import {
|
|
|
15
15
|
filterStylingProps,
|
|
16
16
|
type RecursicaOverStyled,
|
|
17
17
|
} from "../../utils/filterStylingProps";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
type RecursicaTableProps,
|
|
20
|
+
type RecursicaTableRowProps,
|
|
21
|
+
type RecursicaTableHeaderCellProps,
|
|
22
|
+
type RecursicaTableCellProps,
|
|
23
|
+
} from "@recursica/adapter-common";
|
|
19
24
|
import styles from "./Table.module.css";
|
|
25
|
+
import { ChevronUpIcon, ChevronDownIcon } from "./Table.icons";
|
|
20
26
|
|
|
21
27
|
export type TableProps = RecursicaOverStyled<
|
|
22
28
|
MantineTableProps & RecursicaTableProps
|
|
@@ -80,45 +86,77 @@ export const TableTbody = forwardRef<HTMLTableSectionElement, TableTbodyProps>(
|
|
|
80
86
|
);
|
|
81
87
|
TableTbody.displayName = "TableTbody";
|
|
82
88
|
|
|
83
|
-
export type TableTrProps = RecursicaOverStyled<
|
|
89
|
+
export type TableTrProps = RecursicaOverStyled<
|
|
90
|
+
Omit<MantineTableTrProps, "children"> & RecursicaTableRowProps
|
|
91
|
+
>;
|
|
84
92
|
|
|
85
93
|
export const TableTr = forwardRef<HTMLTableRowElement, TableTrProps>(
|
|
86
|
-
function TableTr(
|
|
94
|
+
function TableTr(
|
|
95
|
+
{ overStyled = false, selected = false, disabled = false, ...rest },
|
|
96
|
+
ref,
|
|
97
|
+
) {
|
|
87
98
|
const sanitizedProps = filterStylingProps(rest, overStyled);
|
|
88
99
|
return (
|
|
89
100
|
<MantineTable.Tr
|
|
90
101
|
ref={ref}
|
|
91
102
|
{...(sanitizedProps as unknown as MantineTableTrProps)}
|
|
103
|
+
data-selected={selected ? "true" : undefined}
|
|
104
|
+
data-disabled={disabled ? "true" : undefined}
|
|
92
105
|
/>
|
|
93
106
|
);
|
|
94
107
|
},
|
|
95
108
|
);
|
|
96
109
|
TableTr.displayName = "TableTr";
|
|
97
110
|
|
|
98
|
-
export type TableThProps = RecursicaOverStyled<
|
|
111
|
+
export type TableThProps = RecursicaOverStyled<
|
|
112
|
+
Omit<MantineTableThProps, "children"> & RecursicaTableHeaderCellProps
|
|
113
|
+
>;
|
|
99
114
|
|
|
100
115
|
export const TableTh = forwardRef<HTMLTableCellElement, TableThProps>(
|
|
101
|
-
function TableTh(
|
|
116
|
+
function TableTh(
|
|
117
|
+
{ overStyled = false, sorted = false, disabled = false, children, ...rest },
|
|
118
|
+
ref,
|
|
119
|
+
) {
|
|
102
120
|
const sanitizedProps = filterStylingProps(rest, overStyled);
|
|
103
121
|
return (
|
|
104
122
|
<MantineTable.Th
|
|
105
123
|
ref={ref}
|
|
106
124
|
{...(sanitizedProps as unknown as MantineTableThProps)}
|
|
107
|
-
|
|
125
|
+
data-sorted={sorted ? "true" : undefined}
|
|
126
|
+
data-disabled={disabled ? "true" : undefined}
|
|
127
|
+
aria-sort={
|
|
128
|
+
sorted === "asc"
|
|
129
|
+
? "ascending"
|
|
130
|
+
: sorted === "desc"
|
|
131
|
+
? "descending"
|
|
132
|
+
: undefined
|
|
133
|
+
}
|
|
134
|
+
>
|
|
135
|
+
{children}
|
|
136
|
+
{sorted === "asc" && <ChevronUpIcon className={styles.sortIcon} />}
|
|
137
|
+
{sorted === "desc" && <ChevronDownIcon className={styles.sortIcon} />}
|
|
138
|
+
</MantineTable.Th>
|
|
108
139
|
);
|
|
109
140
|
},
|
|
110
141
|
);
|
|
111
142
|
TableTh.displayName = "TableTh";
|
|
112
143
|
|
|
113
|
-
export type TableTdProps = RecursicaOverStyled<
|
|
144
|
+
export type TableTdProps = RecursicaOverStyled<
|
|
145
|
+
Omit<MantineTableTdProps, "children"> & RecursicaTableCellProps
|
|
146
|
+
>;
|
|
114
147
|
|
|
115
148
|
export const TableTd = forwardRef<HTMLTableCellElement, TableTdProps>(
|
|
116
|
-
function TableTd(
|
|
149
|
+
function TableTd(
|
|
150
|
+
{ overStyled = false, disabled = false, variant = "default", ...rest },
|
|
151
|
+
ref,
|
|
152
|
+
) {
|
|
117
153
|
const sanitizedProps = filterStylingProps(rest, overStyled);
|
|
118
154
|
return (
|
|
119
155
|
<MantineTable.Td
|
|
120
156
|
ref={ref}
|
|
121
157
|
{...(sanitizedProps as unknown as MantineTableTdProps)}
|
|
158
|
+
data-disabled={disabled ? "true" : undefined}
|
|
159
|
+
data-currency={variant === "currency" ? "true" : undefined}
|
|
122
160
|
/>
|
|
123
161
|
);
|
|
124
162
|
},
|
|
@@ -39,3 +39,38 @@ export default function Demo() {
|
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
---
|
|
42
|
+
|
|
43
|
+
## 3. Row and Cell States
|
|
44
|
+
|
|
45
|
+
- **`Table.Tr`**: `selected` applies the selected-row background; `disabled` dims the row and applies the disabled cell colors to every cell in it.
|
|
46
|
+
- **`Table.Th`**: `sorted="asc" | "desc"` applies the sorted header style and renders the matching chevron icon; omit it (or pass `false`) for the unsorted style. `disabled` dims the header cell.
|
|
47
|
+
- **`Table.Td`**: `variant="currency"` applies the currency text style (for numeric/monetary columns); `disabled` dims the cell.
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
<Table>
|
|
51
|
+
<Table.Thead>
|
|
52
|
+
<Table.Tr>
|
|
53
|
+
<Table.Th>Name</Table.Th>
|
|
54
|
+
<Table.Th sorted="asc">Balance</Table.Th>
|
|
55
|
+
</Table.Tr>
|
|
56
|
+
</Table.Thead>
|
|
57
|
+
<Table.Tbody>
|
|
58
|
+
<Table.Tr selected>
|
|
59
|
+
<Table.Td>Jane Doe</Table.Td>
|
|
60
|
+
<Table.Td variant="currency">$1,204.50</Table.Td>
|
|
61
|
+
</Table.Tr>
|
|
62
|
+
<Table.Tr disabled>
|
|
63
|
+
<Table.Td>Inactive Account</Table.Td>
|
|
64
|
+
<Table.Td variant="currency">$0.00</Table.Td>
|
|
65
|
+
</Table.Tr>
|
|
66
|
+
</Table.Tbody>
|
|
67
|
+
<Table.Tfoot>
|
|
68
|
+
<Table.Tr>
|
|
69
|
+
<Table.Td>Total</Table.Td>
|
|
70
|
+
<Table.Td variant="currency">$1,204.50</Table.Td>
|
|
71
|
+
</Table.Tr>
|
|
72
|
+
</Table.Tfoot>
|
|
73
|
+
</Table>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|