@handamade/psi-react 0.26.0 → 0.27.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/docs/Text.md ADDED
@@ -0,0 +1,42 @@
1
+ <!-- Generated by emit-docs.ts — do not edit. -->
2
+ # Text
3
+
4
+ A line or paragraph of text at one of Psi's text styles (D109). It gives a pattern tree an element per line, so a headline and its explanation do not run together. Margin 0: a Stack's gap does the spacing.
5
+
6
+ ## Props
7
+
8
+ | Prop | Type | Default | Required | Description |
9
+ |---|---|---|---|---|
10
+ | `children` | `ReactNode` | — | no | The text. |
11
+ | `as` | `"p" \| "span" \| "div" \| "h2" \| "h3" \| "h4"` | p | no | The element. A heading's level follows the document outline, its size the design, so `as` and `size` are separate. |
12
+ | `size` | `12 \| 14 \| 16 \| 18` | 14 | no | Font size in px; each size carries its own line height. |
13
+ | `weight` | `"regular" \| "medium"` | regular | no | |
14
+ | `tone` | `"primary" \| "secondary"` | primary | no | `secondary` for supporting text. |
15
+ | `ref` | `Ref<HTMLElement>` | — | no | Forwarded ref to the element. |
16
+ | `className` | `string` | — | no | Additional CSS class name(s) merged onto the component's root element. |
17
+
18
+ ## Keyboard & assistive tech
19
+
20
+ | Keys | Behavior |
21
+ |---|---|
22
+ | Tab | Not focusable. |
23
+
24
+ Renders the element `as` names (p by default). Use h2–h4 for a heading at its outline level, whatever its `size`.
25
+
26
+ ## Theming
27
+
28
+ Override `--psi-text-*` custom properties at any scope.
29
+
30
+
31
+
32
+ ## Rules
33
+
34
+ - One accent per visual group; everything else neutral or ghost.
35
+ - danger only for actions with real consequences.
36
+ - Sizes are px numbers (24|32|40|48), never S/M/L.
37
+ - Typography tokens are --psi-text-{size}-{lineHeight}-{weight}.
38
+ - Override component tokens (--psi-{component}-*), not semantic tokens, for one-off theming.
39
+ - --psi-button-font overrides button typography across all sizes (documented D34 override; ember → mono).
40
+ - Wrap labeled form controls in Field — label association, description/error line, aria-describedby and aria-invalid come wired; don't hand-roll label+message rows.
41
+ - Field is for a control with a *visible* label. A toolbar filter control named by aria-label or placeholder takes neither a Field nor a hand-rolled label row — compare filter-toolbar (no labels) with table-pagination ("Rows per page" visible, so Field). Filters with visible labels submitted together by a Find button are filter-form: a Toolbar as="form" align="start" of Fields with one-line labels, where a field's error line is the Field's own and hangs below its control (D87, D92).
42
+ - Use Dialog for blocking modal flows — title/footer slots, dismissible gate; danger stays on the footer Buttons, one accent per group.
package/docs/Tooltip.md CHANGED
@@ -8,7 +8,7 @@ Short hover/focus hint attached to a single trigger element.
8
8
  | Prop | Type | Default | Required | Description |
9
9
  |---|---|---|---|---|
10
10
  | `content` | `ReactNode` | — | yes | Tooltip content. |
11
- | `children` | `ReactElement<Record<string, unknown>, string \| JSXElementConstructor<any>>` | — | yes | The trigger element (must accept ref, onMouseEnter/Leave, onFocus/Blur). |
11
+ | `children` | `ReactElement<Record<string, unknown>, string \| JSXElementConstructor<any>>` | — | yes | The trigger element (must accept onFocus/Blur). |
12
12
  | `placement` | `"top" \| "bottom" \| "left" \| "right"` | top | no | Preferred placement. |
13
13
 
14
14
  ## Keyboard & assistive tech
package/llms.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # @handamade/psi-react
2
2
 
3
- 50 React 19 components consuming @handamade/psi-tokens. Zero runtime deps, CSS Modules, ref-as-prop.
3
+ 52 React 19 components consuming @handamade/psi-tokens. Zero runtime deps, CSS Modules, ref-as-prop.
4
4
 
5
5
  ## Machine-readable artifacts
6
6
  - dist/manifest.json: full component/prop inventory with types and defaults
@@ -30,6 +30,10 @@
30
30
  ## Panel
31
31
  - <Panel padding={16|24}>: the elevated surface panel (D51) — bg-secondary, faint hairline border, radius-12, space-24 padding (16 for compact). Binds the shared --psi-surface-* recipe Dialog's panel also uses; brands retune both by overriding --psi-surface-*. Not a Card: no media slot, no hover lift.
32
32
 
33
+
34
+ ## Stack and Text
35
+ - <Stack gap={4|8|12|16|24|32} align="stretch|start|center|end">: a column with a gap from the space scale (D109), default gap 16 and stretch. A plain <div>, no role, no wrap: Toolbar's vertical counterpart. Use it for the layout a pattern tree cannot get from Panel (which pads but lays out nothing) or from psi-gap-* (which sets gap only). `align="start"` keeps a Button at its own width. Fields in a form: gap 24.
36
+ - <Text as="p|span|div|h2|h3|h4" size={12|14|16|18} weight="regular|medium" tone="primary|secondary">: one line or paragraph at a Psi text style (D109), margin 0, default p / 14 / regular / primary. `as` follows the document outline and `size` the design, so `<Text as="h3" size={16} weight="medium">` is the usual section headline. In a pattern tree, put each line in its own Text: adjacent bare strings render as one run.
33
37
  ## DescriptionList (2)
34
38
  - `<DescriptionList layout="stacked|inline" gap={8|12|16}>` with one `<DescriptionItem term="Date">value</DescriptionItem>` per field (D70). Renders `<dl>`; each item is a `<div><dt/><dd/></div>` group, which is the HTML5 grouping element, so the term/value association survives.
35
39
  - `stacked` (default) puts the term above its value; `inline` is a two-column grid and is what a detail drawer wants. This is the body of the `detail-drawer` pattern — do not hand-roll a `<dl>`.
@@ -114,13 +118,13 @@
114
118
  ## Compositions (recipes for what has no component yet)
115
119
 
116
120
  - `<Textarea maxLength={280}>` is a multi-line text field (D104): the Input recipe on a native `<textarea>`, wired by a Field like Input. `minLength`/`maxLength` are native; with `maxLength` a counter under it shows `{length} / {maxLength}` — not a live region, read with the field through aria-describedby (`showCounter={false}` hides it). No `size`: its height is `rows` (default 3).
117
- - `<RadioGroup value onValueChange><Radio value="a">A</Radio>…</RadioGroup>` (D104): native radios sharing one name, so the browser supplies Tab and the arrow keys. Put it in `<Field group label required error>`: the fieldset and legend name the group and RadioGroup adds no role; standalone it is `role="radiogroup"` and needs `aria-label`. A required or invalid Field makes every radio `required` and `aria-invalid`. `orientation="horizontal"` lays them in a row. Consult dist/patterns.json for `feedback-form`.
121
+ - `<RadioGroup value onValueChange><Radio value="a">A</Radio>…</RadioGroup>` (D104): native radios sharing one name, so the browser supplies Tab and the arrow keys. Put it in `<Field group label required error>`: the fieldset and legend name the group and RadioGroup adds no role; standalone it is `role="radiogroup"` and needs `aria-label`. A required or invalid Field makes every radio `required` and `aria-invalid`; an invalid group Field also makes every `Checkbox` `aria-invalid` (not `required`), and an unchecked, `aria-invalid` box or circle outlines in the field error colour (D108). `orientation="horizontal"` lays them in a row. Consult dist/patterns.json for `feedback-form`.
118
122
  - Form field: wrap the label, control, and message with `<Field label description error required>{control}</Field>` — label association (htmlFor/id), the description/error message line (swaps to error, aria-live="polite"; `announce={false}` drops the live region for an app that routes every announcement through one announcer of its own — the message still describes the control, D81), and aria-describedby/aria-invalid wiring onto a nested Input/Select are automatic (D49). `required` marks the label with an asterisk; `requiredText="(Required)"` states it in a word instead, the label's colour, aria-hidden because the control's `required` is the signal (D84). A control Psi did not write joins a Field with `const field = useFieldControl()` spread onto its focusable element — it returns `id`, `aria-labelledby` (a `<label for>` names only a labelable element, and a custom control is usually a div), `aria-describedby`, `aria-invalid`, `aria-required` and `required` as the Field sets them, and `{}` outside a Field (D84); the element needs a role that admits aria-required, such as combobox. `group` mode renders fieldset/legend for self-labeled controls (Checkbox/Switch). Standalone Input/Select used outside Field still need `aria-invalid` and `aria-describedby` paired manually. Spacing: `psi-gap-24` between fields, `psi-gap-12` for toggle groups, `psi-gap-8` for button rows and label+Tag rows.
119
123
  - Stacks/rows: `.psi-gap-*` utilities set only the gap — they do not create a flex/grid container. Pair them with `display: flex` (column for stacks, row for button groups).
120
124
  - Page-level spacing: the form scale above (24/12/8) governs controls inside a form, not page layout. For layout use `psi-gap-32` between sections of a page, `psi-gap-24` between cards or panels in a grid, `psi-py-32` for a section's vertical padding, and `.psi-container` for the page's max-width and gutters (it is in utilities.css, not components.css). Marketing-scale rhythm is app-level and deliberately not tokenized — the system stops at the container.
121
125
  - Utility classes: families are `psi-gap-*`, `psi-p-*`/`psi-px-*`/`psi-py-*`, `psi-m-*`/`psi-mx-*`/`psi-my-*` (all on the spacing scale), `psi-text-*` and `psi-display-*` (typography), `psi-fg-*` (foreground colour, one per fg token: `psi-fg-primary`, `psi-fg-secondary`, `psi-fg-on-accent`, … — `color: var(--psi-fg-<role>)`) (D94), plus `.psi-tabular`, `.psi-container`, `.psi-media-tint` and `.psi-sr-only` (hides content visually while keeping it in the accessibility tree). Scaled utility families set one property each and create no layout context — pair `psi-gap-*` with `display: flex`; the bare/fixed utilities (`.psi-container`, `.psi-media-tint`, `.psi-sr-only`) set several. Use `psi-m-0` to kill a UA margin rather than an inline style. A utility on a component root beats the component's own value (D94): `<Tabs className="psi-gap-24">` gets a 24px gap. The exact roster is `@handamade/psi-tokens/guidance.json` → `utilities.classes`.
122
126
  - Native pass-through: components spread unlisted props onto their underlying element (Input → `<input>`, Select → `<select>`, Button → `<button>`/`<a>`), so `value`, `onChange`, `type`, `id`, `autoComplete`, `checked` etc. all work even though manifest.json lists only the component-specific props. Exception: Tooltip takes no host props — put handlers/attributes on its trigger child.
123
- - Theming, programmatic: the standard setup is `data-psi-theme` on `<html>`. From JS, set it once at app init: `document.documentElement.dataset.psiTheme = "dark"` (React: in a root-level effect). Theme CSS does NOT paint the page background — paint `background: var(--psi-bg-primary)` at your app root yourself. For a themed subtree, set `data-psi-theme` on a wrapper and paint its background the same way; brand font combos do not reach nested subtrees (see guidance.fonts.scope).
127
+ - Theming, programmatic: the standard setup is `data-psi-theme` on `<html>`. From JS, set it once at app init: `document.documentElement.dataset.psiTheme = "dark"` (React: in a root-level effect). Theme CSS does NOT paint the page background or set its font — at your app root set `background: var(--psi-bg-primary)` and `color: var(--psi-fg-primary)` yourself, and `font: var(--psi-text-14-20-regular)` on `body`, in `@layer psi.base` (D107). Never set a font size on `html`: Psi's scales are rem, so it would resize every control, gap and radius. Components set their own type, but text you place in a Panel, Dialog, Card or tab panel inherits the page's, which is the browser's serif until you set it. Native `button`/`input`/`select`/`textarea` need `font: inherit` in the same layer. For a themed subtree, set `data-psi-theme` on a wrapper and paint its background the same way; brand font combos do not reach nested subtrees (see guidance.fonts.scope).
124
128
  - Size selection: 32 is the default (compact UI); 40 for comfortable forms and marketing pages; 24 for inline/table-row actions; 48 for hero CTAs.
125
129
  - Tags & the accent rule (D40): Tags are passive labels — they do NOT count against "one accent per visual group" (that rule governs buttons/CTAs). Badge mapping: highlight (e.g. a plan badge) → `accent-subtle`; meta → `neutral-subtle`; status → `success`/`warning`/`danger`. On Tag, `subtle` is a boolean prop — `accent-subtle` is spelled `variant="accent" subtle` (Tag's variant union has no `-subtle` entries).
126
130
  - CSS import order: all five DS stylesheets are order-independent — base.css and @handamade/psi-react/styles both open with an explicit `@layer psi.base, psi.theme, psi.components, psi.utilities;` statement. Recommended listing for readability: base → theme → components → utilities → @handamade/psi-react/styles.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@handamade/psi-react",
3
- "version": "0.26.0",
3
+ "version": "0.27.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -47,7 +47,7 @@
47
47
  "tsx": "^4.22.4",
48
48
  "vite": "^6.0.0",
49
49
  "vitest": "^3.2.6",
50
- "@handamade/psi-tokens": "0.26.0"
50
+ "@handamade/psi-tokens": "0.27.0"
51
51
  },
52
52
  "scripts": {
53
53
  "build": "vite build && tsc -p tsconfig.build.json && tsx scripts/emit-styles-dts.ts && tsx scripts/emit-manifest.ts && tsx scripts/emit-patterns.ts && tsx scripts/emit-docs.ts",