pixelize-design-library 2.4.2-beta.36 → 2.4.2-beta.37
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/COMPONENT-GOTCHAS.md +135 -0
- package/README.md +5 -0
- package/dist/Components/Common/ErrorMessage.js +2 -2
- package/dist/Components/Drawer/DrawerProps.d.ts +1 -0
- package/dist/Components/InputTextArea/InputTextAreaProps.d.ts +1 -0
- package/dist/Components/Modal/ModalProps.d.ts +1 -0
- package/dist/Components/MultiSelect/MultiSelectProps.d.ts +1 -0
- package/dist/Components/ProfileCard/ProfileCardProps.d.ts +1 -0
- package/dist/Components/RadioButton/RadioButtonProps.d.ts +1 -0
- package/dist/Components/Select/SelectProps.d.ts +1 -0
- package/dist/Components/Table/TableProps.d.ts +1 -0
- package/dist/esm/Components/Common/ErrorMessage.js +2 -2
- package/package.json +3 -3
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Component Gotchas & Decision Guide
|
|
2
|
+
|
|
3
|
+
For anyone — human or AI — consuming `pixelize-design-library` in an app. Read this **before**
|
|
4
|
+
choosing a component or a prop value, not after something looks wrong. Every entry below compiles
|
|
5
|
+
cleanly, accepts what you pass, and produces the wrong result with no warning — TypeScript cannot
|
|
6
|
+
catch any of these, and a unit suite that mocks the library won't either.
|
|
7
|
+
|
|
8
|
+
This file ships inside the published package (`node_modules/pixelize-design-library/COMPONENT-GOTCHAS.md`)
|
|
9
|
+
so it's available without checking out the library's source repo.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Choosing between components
|
|
14
|
+
|
|
15
|
+
### Segmented control look — `SegmentedControl` / `Toggle/TableToggle`
|
|
16
|
+
|
|
17
|
+
Both take `variant?: "default" | "brandSlide" | "underlineSlide"` (default `"default"` — always
|
|
18
|
+
opt-in, never assume a non-default look).
|
|
19
|
+
|
|
20
|
+
| Context | `variant` | Look |
|
|
21
|
+
| --- | --- | --- |
|
|
22
|
+
| Most call sites — settings rows, filters, table view switches | `"default"` | Neutral chip |
|
|
23
|
+
| The primary, hard-to-miss view switcher on a page (not a dense toolbar) | `"brandSlide"` | Solid brand-filled pill slides behind the active segment |
|
|
24
|
+
| A dense toolbar, table header, or anywhere a boxed control feels heavy | `"underlineSlide"` | Thin brand-colored bar glides under the active label, no fill/chip |
|
|
25
|
+
|
|
26
|
+
Pick by density and emphasis, not by taste. When unsure, or migrating an existing call site, leave
|
|
27
|
+
it on `"default"` — it's the only variant guaranteed to match previously-shipped screenshots.
|
|
28
|
+
|
|
29
|
+
### Which "select" component — seven components overlap in name, not purpose
|
|
30
|
+
|
|
31
|
+
**Not picking a value from an options list at all:**
|
|
32
|
+
|
|
33
|
+
| Doing | Component | Why not the others |
|
|
34
|
+
| --- | --- | --- |
|
|
35
|
+
| Triggering a labelled *action* (export, row menu, "...") | `Dropdown` | A menu-button, not a form field — nothing is "selected" and persisted |
|
|
36
|
+
| Typing free-text tags (e.g. email addresses), no options list | `MultiSelect` | Despite the name, there is nothing to pick from — see gotcha below |
|
|
37
|
+
| Choosing which *fields/columns* to include, in a modal, from a checkbox grid | `FieldSelectModal` | Multi-checkbox selection UI, not an inline field control |
|
|
38
|
+
|
|
39
|
+
**Picking one or more values from an options list, inline in a form:**
|
|
40
|
+
|
|
41
|
+
| List length / need | Component | Why |
|
|
42
|
+
| --- | --- | --- |
|
|
43
|
+
| Short, always-visible list, no search needed | `Select` | Native `<select>` wrapper — lightest option, but the popup is OS-painted and can't be themed |
|
|
44
|
+
| New build, want themed/custom-rendered rows (icons, descriptions), no huge list | `SelectV2` | Fully React-rendered popup — **prefer this for new work** over `Select` |
|
|
45
|
+
| Long list needing search + chips + select-all + infinite scroll | `SearchSelect` | The full-featured picker; virtualizes |
|
|
46
|
+
| Search-as-you-type with avatars/colour swatches/a pinned "add new" row | `SelectSearch` | Lighter than `SearchSelect`; reach for it for that specific row-content need |
|
|
47
|
+
|
|
48
|
+
Default when unsure: `SelectV2` for a themed field, `Select` if the native popup is genuinely fine.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Known gotchas (silent failures)
|
|
53
|
+
|
|
54
|
+
### `Drawer` / `Modal` / `ProfileCard` drop any child that isn't a named slot
|
|
55
|
+
|
|
56
|
+
All three match children by exact type (`child.type === XHeader/XBody/XFooter`) and render only
|
|
57
|
+
those matches — nothing else in `children` is ever read. Two consequences: a sibling among the
|
|
58
|
+
slots (a nested `Modal`, `AlertDialog`, a confirm dialog, a portal after the footer) silently never
|
|
59
|
+
mounts; and a slot wrapped in a `<>...</>` fragment is dropped too, because the matching doesn't
|
|
60
|
+
unwrap fragments — a fragment's `.type` is `Fragment`, never the slot component.
|
|
61
|
+
|
|
62
|
+
**Apply:** put a confirm dialog **outside** the `Drawer`/`Modal`/`ProfileCard`, as a sibling in a
|
|
63
|
+
wrapping fragment — never among the header/body/footer slots. When a ternary picks between bodies,
|
|
64
|
+
branch into a variable rendered inside one body slot, rather than returning fragment-wrapped slots
|
|
65
|
+
per branch.
|
|
66
|
+
|
|
67
|
+
### `Select`'s `onChange` never fires for the built-in placeholder
|
|
68
|
+
|
|
69
|
+
`Select` resolves the changed value against its `options` list and calls `onChange` only on a hit.
|
|
70
|
+
The built-in `placeholder` renders as its own empty-value option, which isn't in `options` — so
|
|
71
|
+
picking it resolves to no match and nothing fires. An "Any / All / None" reset built on the
|
|
72
|
+
placeholder alone is a dead control; the value snaps back and no filter clears.
|
|
73
|
+
|
|
74
|
+
**Apply:** fine for a plain pick-one-of-N. For a clearable filter, add an explicit
|
|
75
|
+
`{ id: "", label: "Any" }` entry to `options` instead of relying on the placeholder.
|
|
76
|
+
|
|
77
|
+
### `MultiSelect` is an email-chip input, not a picker
|
|
78
|
+
|
|
79
|
+
Despite the name, `MultiSelect` takes **no `options` prop at all**. Typed text is validated as an
|
|
80
|
+
email address and rejected if it doesn't match — there is no way to choose from a product/entity
|
|
81
|
+
list.
|
|
82
|
+
|
|
83
|
+
**Apply:** use `SearchSelect` with `isMultiple` to pick multiple values from an options list.
|
|
84
|
+
|
|
85
|
+
### `RadioButton` has a closed prop list — no `...rest`, no `name`
|
|
86
|
+
|
|
87
|
+
It forwards only a fixed set of props (`label`, `colorScheme`, `isChecked`, `onChange`,
|
|
88
|
+
`isDisabled`, `size`, `value`, `defaultChecked`, plus label/tooltip props) to the underlying radio
|
|
89
|
+
— a `data-*` hook, `aria-describedby`, or a `ref` cannot reach it.
|
|
90
|
+
|
|
91
|
+
**Apply:** it still joins an external `RadioGroup` correctly (grouping comes from context, no
|
|
92
|
+
`name` prop needed) — don't avoid it just for grouping. Reach for the library's Chakra escape
|
|
93
|
+
hatch only when you need something outside that closed list.
|
|
94
|
+
|
|
95
|
+
### `Table`'s `onRowClick` is dead if every column has a `node` renderer
|
|
96
|
+
|
|
97
|
+
Row-click is gated **per column**, not per row: a column supplying a custom `node` renderer
|
|
98
|
+
swallows the click on that cell. Give every column a `node` and `onRowClick` becomes dead code —
|
|
99
|
+
accepted, typed, never called.
|
|
100
|
+
|
|
101
|
+
**Apply:** leave at least one column without a `node`, or put an explicit control inside a `node`
|
|
102
|
+
renderer instead of relying on row click.
|
|
103
|
+
|
|
104
|
+
### `InputTextArea`'s `width` defaults to the number `500`, and no `id` means no label
|
|
105
|
+
|
|
106
|
+
`width` defaults to `500`, forwarded straight through — `500` isn't a sizing-scale key, so it
|
|
107
|
+
renders a literal `500px`, not a max and not responsive. It only shows up on a narrow screen.
|
|
108
|
+
Separately, `id` feeds both the visible label and the field itself with no fallback — omit it and
|
|
109
|
+
the label is associated with nothing; the field has no accessible name.
|
|
110
|
+
|
|
111
|
+
**Apply:** pass `width="100%"` **and** `id` at every call site.
|
|
112
|
+
|
|
113
|
+
### `error` without `errorMessage` renders the literal word "Error" — and erases `helperText`
|
|
114
|
+
|
|
115
|
+
Every field that shows validation errors (`TextInput`, `InputTextArea`, `Select`, `MultiSelect`,
|
|
116
|
+
`SelectV2`, `SearchSelect`, `SelectSearch`, `Search`, `Checkbox`, the `DatePicker` variants,
|
|
117
|
+
`PhoneNumberInput`) falls back to the literal word **"Error"** when `error` is `true` but
|
|
118
|
+
`errorMessage` is empty. Worse, `helperText` only renders when `!error` — so the guidance
|
|
119
|
+
disappears exactly when it's needed.
|
|
120
|
+
|
|
121
|
+
**Apply:** never pass `error` without `errorMessage`. Gate `error` on a `touched` flag so a field
|
|
122
|
+
whose defaults arrive asynchronously doesn't open already red.
|
|
123
|
+
|
|
124
|
+
### A wrapper presetting a prop must destructure with a default, not rely on JSX prop order
|
|
125
|
+
|
|
126
|
+
Wrapping a library component to preset a prop (e.g. `Table`'s `variant`) as
|
|
127
|
+
`<Table variant="studio" {...props} />` silently reverts to the library's own default the moment a
|
|
128
|
+
caller passes `variant={undefined}` — a later spread always wins in JSX, even with `undefined`,
|
|
129
|
+
and the library component resolves that `undefined` via its own default parameter.
|
|
130
|
+
|
|
131
|
+
**Apply:**
|
|
132
|
+
```tsx
|
|
133
|
+
const AppTable = ({ variant = 'studio', ...props }: TableProps) => <Table variant={variant} {...props} />;
|
|
134
|
+
```
|
|
135
|
+
Applies to any library prop with a default value, not just `Table`'s `variant`.
|
package/README.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
A comprehensive React component library built with TypeScript, providing a rich set of UI components for modern web applications.
|
|
4
4
|
|
|
5
|
+
> **Building with this library (including via an AI assistant)?** Read
|
|
6
|
+
> [COMPONENT-GOTCHAS.md](./COMPONENT-GOTCHAS.md) first — it documents known silent-failure traps
|
|
7
|
+
> and which near-duplicate component to pick for what. It ships with this package, so it's
|
|
8
|
+
> available at `node_modules/pixelize-design-library/COMPONENT-GOTCHAS.md` too.
|
|
9
|
+
|
|
5
10
|
## Features
|
|
6
11
|
|
|
7
12
|
- 🎨 **Modern Design System** - Consistent and beautiful components
|
|
@@ -4,8 +4,8 @@ const jsx_runtime_1 = require("react/jsx-runtime");
|
|
|
4
4
|
const react_1 = require("@chakra-ui/react");
|
|
5
5
|
const lucide_react_1 = require("lucide-react");
|
|
6
6
|
const useCustomTheme_1 = require("../../Theme/useCustomTheme");
|
|
7
|
-
// `id` lets a field point `aria-describedby` here
|
|
8
|
-
//
|
|
7
|
+
// `id` lets a field point `aria-describedby` here; `role="alert"` announces once, on render.
|
|
8
|
+
// Always pass `errorMessage` when `error` is true — omitted, this renders the literal word "Error", and every caller hides `helperText` while `error` is true.
|
|
9
9
|
const ErrorMessage = ({ errorMessage, id }) => {
|
|
10
10
|
const { colors } = (0, useCustomTheme_1.useCustomTheme)();
|
|
11
11
|
return ((0, jsx_runtime_1.jsxs)(react_1.Flex, { id: id, align: "center", color: colors.semanticText.error, fontSize: "0.875rem", role: "alert", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Info, { width: "0.875rem" }), (0, jsx_runtime_1.jsx)(react_1.Text, { ml: "0.188rem", children: errorMessage !== null && errorMessage !== void 0 ? errorMessage : "Error" })] }));
|
|
@@ -3,5 +3,6 @@ export type DrawerProps = Pick<ChakraDrawerProps, "isOpen" | "onClose" | "placem
|
|
|
3
3
|
size?: "xs" | "sm" | "md" | "lg" | "xl" | "full";
|
|
4
4
|
placement?: "top" | "right" | "bottom" | "left";
|
|
5
5
|
vaiant?: "solid" | "outline" | "ghost" | "link";
|
|
6
|
+
/** Only DrawerHeader/DrawerBody/DrawerFooter render; other elements and fragment-wrapped slots are silently dropped. */
|
|
6
7
|
children?: React.ReactNode;
|
|
7
8
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { TextareaProps } from "@chakra-ui/react";
|
|
2
|
+
/** width defaults to 500 → literal "500px", not responsive (pass "100%"); id is required or the label has no accessible association. */
|
|
2
3
|
export type InputTextAreaProps = Pick<TextareaProps, "placeholder" | "value" | "onChange" | "onBlur" | "name" | "id" | "size" | "resize" | "isDisabled" | "isReadOnly" | "isRequired" | "variant" | "autoComplete"> & {
|
|
3
4
|
label?: string;
|
|
4
5
|
error?: boolean;
|
|
@@ -5,6 +5,7 @@ export type ChakraModelProps = Pick<ModalProps, "isOpen" | "onClose" | "finalFoc
|
|
|
5
5
|
overlaybackdropInvert?: string;
|
|
6
6
|
overlaybackdropBlur?: string;
|
|
7
7
|
size: "xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl" | "5xl" | "6xl" | "full";
|
|
8
|
+
/** Only ModalHeader/ModalBody/ModalFooter render; other elements and fragment-wrapped slots are silently dropped. */
|
|
8
9
|
children?: React.ReactNode;
|
|
9
10
|
isLoading?: boolean;
|
|
10
11
|
};
|
|
@@ -3,6 +3,7 @@ export type ProfileCardVariant = "elevated" | "outline" | "filled" | "unstyled";
|
|
|
3
3
|
/** @deprecated Misspelling kept for existing callers — use "outline". */
|
|
4
4
|
export type ProfileCardVariantLegacy = "outlein";
|
|
5
5
|
export type ProfileCardProps = Pick<CardProps, "direction" | "maxW" | "align" | "justify" | "overflow"> & {
|
|
6
|
+
/** Only direct ProfileCardHeader/Body/Footer children render — no wrapping fragment, no other siblings. */
|
|
6
7
|
children: React.ReactNode;
|
|
7
8
|
variant?: ProfileCardVariant | ProfileCardVariantLegacy;
|
|
8
9
|
size?: "sm" | "md" | "lg";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { RadioProps, RadioGroupProps } from '@chakra-ui/react';
|
|
2
|
+
/** Closed prop list, no ...rest — for aria-* or a ref use Chakra's Radio (Primitives barrel) instead. Still joins an external RadioGroup correctly via Chakra's own context. */
|
|
2
3
|
export type ChakraRadioProps = Pick<RadioProps, "size" | "colorScheme" | "isChecked" | "onChange" | "isDisabled" | "value" | "defaultChecked"> & {
|
|
3
4
|
label: string;
|
|
4
5
|
labelFontSize?: string;
|
|
@@ -10,6 +10,7 @@ export type chakraSelectProps = Pick<SelectProps, "placeholder" | "size" | "vari
|
|
|
10
10
|
options: OptionProp[];
|
|
11
11
|
width?: string | number;
|
|
12
12
|
height?: string | number;
|
|
13
|
+
/** Selecting the built-in placeholder (value="") never fires this unless `options` has an explicit `id: ""` entry. */
|
|
13
14
|
onChange: (selectedOption: OptionProp | undefined) => void;
|
|
14
15
|
formControlStyle?: React.CSSProperties;
|
|
15
16
|
isInformation?: boolean;
|
|
@@ -36,6 +36,7 @@ export type TableProps = {
|
|
|
36
36
|
onSelection?: (selected: (string | number)[]) => void;
|
|
37
37
|
isPagination?: boolean;
|
|
38
38
|
selections?: (string | number)[];
|
|
39
|
+
/** Fires per cell, gated on the column: a column with `node` swallows its clicks. */
|
|
39
40
|
onRowClick?: (row: DataObject, header: Record<string | number, string | number>) => void;
|
|
40
41
|
isActionFreeze?: boolean;
|
|
41
42
|
paginationMode?: "client" | "server";
|
|
@@ -2,8 +2,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Flex, Text, } from "@chakra-ui/react";
|
|
3
3
|
import { Info } from "lucide-react";
|
|
4
4
|
import { useCustomTheme } from "../../Theme/useCustomTheme.js";
|
|
5
|
-
// `id` lets a field point `aria-describedby` here
|
|
6
|
-
//
|
|
5
|
+
// `id` lets a field point `aria-describedby` here; `role="alert"` announces once, on render.
|
|
6
|
+
// Always pass `errorMessage` when `error` is true — omitted, this renders the literal word "Error", and every caller hides `helperText` while `error` is true.
|
|
7
7
|
const ErrorMessage = ({ errorMessage, id }) => {
|
|
8
8
|
const { colors } = useCustomTheme();
|
|
9
9
|
return (_jsxs(Flex, { id: id, align: "center", color: colors.semanticText.error, fontSize: "0.875rem", role: "alert", children: [_jsx(Info, { width: "0.875rem" }), _jsx(Text, { ml: "0.188rem", children: errorMessage !== null && errorMessage !== void 0 ? errorMessage : "Error" })] }));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pixelize-design-library",
|
|
3
|
-
"version": "2.4.2-beta.
|
|
3
|
+
"version": "2.4.2-beta.37",
|
|
4
4
|
"description": "React component library for Pixelize apps: themeable Chakra-based components, design tokens and light/dark brand theming.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
"types": "dist/index.d.ts",
|
|
29
29
|
"files": [
|
|
30
30
|
"dist",
|
|
31
|
-
"README.md"
|
|
31
|
+
"README.md",
|
|
32
|
+
"COMPONENT-GOTCHAS.md"
|
|
32
33
|
],
|
|
33
34
|
"dependencies": {
|
|
34
35
|
"@fontsource-variable/inter": "^5.2.8",
|
|
@@ -151,7 +152,6 @@
|
|
|
151
152
|
"jest": "^29.7.0",
|
|
152
153
|
"jest-axe": "^11.0.0",
|
|
153
154
|
"jest-environment-jsdom": "^29.7.0",
|
|
154
|
-
"path": "^0.12.7",
|
|
155
155
|
"postcss": "^8.5.4",
|
|
156
156
|
"prettier": "^3.3.2",
|
|
157
157
|
"prop-types": "^15.8.1",
|