@eocrm/design-system 0.6.21 → 0.6.23
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/AI-PRIMER.md +167 -289
- package/README.md +28 -5
- package/docs/accessible-names.md +22 -0
- package/docs/components/CircularProgress.md +4 -0
- package/docs/components/CurrencySelect.md +1 -1
- package/docs/components/Kbd.md +1 -1
- package/docs/components/Link.md +2 -2
- package/docs/components/MoneyInput.md +1 -1
- package/docs/components/PhoneInput.md +1 -1
- package/docs/components/Progress.md +7 -0
- package/docs/components/Text.md +8 -0
- package/docs/components/Title.md +4 -0
- package/docs/components/index.json +652 -0
- package/docs/i18n.md +74 -0
- package/docs/setup.md +1 -1
- package/docs/typescript.md +9 -0
- package/package.json +2 -2
- package/src/components/Link/Link.tokens.scss +4 -3
- package/src/components/Link/Link.tsx +1 -1
- package/src/components/LiquidEditor/useLiquidAutocomplete.ts +1 -1
- package/src/components/RichText/RichTextAttachment.tsx +1 -1
- package/src/i18n/I18nProvider.tsx +1 -1
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Accessible names
|
|
2
|
+
|
|
3
|
+
An empty label prop means _unset_, never _blank_.
|
|
4
|
+
|
|
5
|
+
Label props in this library treat `''` exactly like omitting it — `aria-label` everywhere, `previousLabel`, `nextLabel`, `confirmLabel`, `dropzoneLabel`, `label` on `ColumnVisibilityTrigger`, `visibilityLabel`, `emptyState`, a `LiquidVariable`'s `label`, a `SortableGroup.Container`'s or `Kanban.Column`'s `aria-label`, a `FlowCanvas` node's `label`. You cannot use one to blank out a name or suppress default copy. That is deliberate:
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
// ✅ Fine. `row.title` missing → the component's own default copy.
|
|
9
|
+
<CursorPagination previousLabel={row.title ?? ''} … />
|
|
10
|
+
|
|
11
|
+
// ❌ Not a way to hide the label. It renders "Previous" all the same.
|
|
12
|
+
<CursorPagination previousLabel="" … />
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
**Two exceptions, both because the empty string already has a meaning of its own there.** An `<img>`'s `alt` — a RichText attachment block's `alt` field — is honoured when empty, because `alt=""` is HTML's marker for a decorative image. And a text input's `placeholder` (`DatePicker`, `DateRangePicker`) is honoured when empty, because wanting no placeholder is a real request and a placeholder is not an accessible name. Nothing else.
|
|
16
|
+
|
|
17
|
+
If you are writing a component _in_ this library, the rule behind that is an operator choice, and it is not optional:
|
|
18
|
+
|
|
19
|
+
- **Use `||`, never `??`, for any fallback that produces an accessible name** — whether the name comes from an attribute (`aria-label={x || t('key')}`) or from content (`<span>{label || t('key')}</span>`). `??` falls back only on `null`/`undefined`, so `''` reaches the DOM. Per the accname spec an empty `aria-label` contributes no name, so the computation does not stop there — it continues to name-from-content and then to `title`, and where the siblings are `aria-hidden` (a chevron, a spinner) the control ends up anonymous.
|
|
20
|
+
- The same applies to a fallback that exists so there is _always_ something readable — `col.visibilityLabel || header || col.id`, `v.label || v.code`. An empty string defeats the safeguard and renders a blank, unidentifiable row.
|
|
21
|
+
|
|
22
|
+
Two tests in `structure.test.ts` gate parts of this: one rejects `??` in `aria-label` / `aria-valuetext`, the other rejects `x ?? t(…)` anywhere in the source. **Neither implements the rule.** They cannot see whether an element is a control, whether its siblings are hidden, or that `v.label ?? v.code` and `node.label ?? id` are the same defect with a non-`t()` fallback — every one of those had to be found by reading. A green run is not coverage; the rule above is, and it is enforced by review.
|
|
@@ -51,3 +51,7 @@
|
|
|
51
51
|
- ❌ Hand-rolled spinning `<svg>` per page. `<CircularProgress />` indeterminate is the same visual, accessible and reduced-motion-aware.
|
|
52
52
|
- ❌ `<CircularProgress value={0}>` to render an empty circle. `value={0}` is determinate (0% done); the intent is usually indeterminate, so omit `value`.
|
|
53
53
|
- ❌ `<CircularProgress size="sm" label>` expecting centered text in a 16px circle. The label is auto-suppressed at `sm` by design; use `md` or `lg`.
|
|
54
|
+
|
|
55
|
+
## Progress hard rule
|
|
56
|
+
|
|
57
|
+
No hand-rolled spinning `<svg>`, no `value={0}` for "not started yet": see [Progress.md](Progress.md#progress-hard-rule).
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# `<CurrencySelect>`
|
|
1
|
+
# `<CurrencySelect>` — ISO 4217 currency picker over Select
|
|
2
2
|
|
|
3
3
|
Currency picker over the DS `Select`: you pass the ISO 4217 `currencies` to offer (required, in display order); it renders `CODE — Name` rows with localized names (`Intl.DisplayNames`). Single mode is controlled on `value: string | null` / `onChange(code | null)`; `multiple` switches to `string[]` (with `triggerDisplay` `"chips"` / `"summary"`). `currencyDisplay` sets how the SELECTED currency shows in the trigger: `"code"` (default, `EUR`), `"symbol"` (`€`) or `"both"` (`EUR €`). Search turns on automatically above 7 currencies (`searchable` overrides) and matches code, name and symbol. Invalid codes are dropped with a dev warning. `size`/`invalid`/`disabled`/`clearable`/`name` etc. pass through to `Select`.
|
|
4
4
|
|
package/docs/components/Kbd.md
CHANGED
package/docs/components/Link.md
CHANGED
|
@@ -40,7 +40,7 @@ import { Link as RouterLink } from 'react-router-dom';
|
|
|
40
40
|
- **Polymorphic**: `as={Component}` forwards all of Component's props with full TypeScript inference.
|
|
41
41
|
- **Library has no router dependency** — the `as` mechanism is consumer-driven.
|
|
42
42
|
- **Three variants**:
|
|
43
|
-
- `default`: link colour (`--color-link-fg`; the accent unless the accent set overrides it — `tealpine` uses a brighter teal at medium weight), hover-underline. Inline CTA ("View all →").
|
|
43
|
+
- `default`: link colour (`--color-link-fg`; the accent unless the accent set overrides it — `tealpine` uses a brighter teal at medium weight; `slate` links are medium weight), hover-underline. Inline CTA ("View all →").
|
|
44
44
|
- `muted`: muted color, hover-accent. Low-emphasis nav (breadcrumb-style).
|
|
45
45
|
- `subtle`: foreground color, hover-accent + underline. Dense-surface name links. **Not deprecated** — this is `Link`'s own variant and is unrelated to `Text`/`Title`'s `tone="subtle"`, which WAS deprecated. Same word, opposite status: a grep for "subtle deprecated" will land on the tone, and `PersonDisplay.Name` uses this variant deliberately.
|
|
46
46
|
- **`underline` prop** (`'hover'` | `'always'` | `'none'`, default `'hover'`) — orthogonal to `variant`:
|
|
@@ -62,4 +62,4 @@ import { Link as RouterLink } from 'react-router-dom';
|
|
|
62
62
|
- ❌ Forgetting `rel="noopener noreferrer"` on `target="_blank"` links.
|
|
63
63
|
- ❌ Using `variant="default"` for low-emphasis nav like breadcrumbs.
|
|
64
64
|
- ❌ A Link inside running text/body copy without `underline="always"` — color alone isn't a sufficient visual cue (WCAG 1.4.1).
|
|
65
|
-
- ❌ A `default` Link inside `<Text weight="semibold">` (or `bold`) under `data-accent="tealpine"` — the link renders at the accent's medium weight, lighter than the words around it. Wrap the bold run in `<strong>` or give the Link a matching weight via `className`.
|
|
65
|
+
- ❌ A `default` Link inside `<Text weight="semibold">` (or `bold`) under `data-accent="tealpine"` or `"slate"` — the link renders at the accent's medium weight, lighter than the words around it. Wrap the bold run in `<strong>` or give the Link a matching weight via `className`.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# `<MoneyInput>`
|
|
1
|
+
# `<MoneyInput>` — currency picker + formatted decimal-string amount
|
|
2
2
|
|
|
3
3
|
Money field: currency picker (`CurrencySelect`) + formatted amount `Input`, controlled on one `value: MoneyValue` (`{ amount: string | null; currency: string }`), `onChange(value)`. `amount` is a **canonical decimal string** (`"1234.50"`, `"-3"`) or `null` — never a JS number. `currencies` (ISO 4217, required) lists what the picker offers; exactly one renders a static prefix instead. The amount groups as you type in the locale's separators, keeps the caret where you are editing, rejects keystrokes past the currency's precision (2 for EUR, 0 for JPY, 3 for KWD) — a digit typed inside a full fraction overwrites the next one. Backspace/Delete step over group separators, and over the decimal while a fraction follows it (select the decimal to remove it). Blur pads the fraction (`12` → `12.00`). Switching currency rounds the amount half-up to the new precision (and pads it, as blur would). An external `amount` that is not a canonical decimal string displays empty with a dev warning. In comma-decimal locales the numpad `.` key types the decimal; a paste or drop is parsed on its own, then spliced over the selection. It must be exactly one number, optionally with currency symbols, a label word before it (`Total`, `Итого:`) and a currency word after it (an ISO code or `руб.`/`р.` — never a magnitude like `K`, `млн`, `million`) and a sign (`-5`, `−5`, `+5`, `5-`, `(1,234.50)`). Grouping must be unambiguous — first group 1–3 digits, then exactly 3, one kind of group character (`,` `.` space, NBSP, apostrophe) — and with both `.` and `,` the last one is the decimal whatever the locale. Anything doubtful (`12,34` or `1.234` in en, `15.01.2024`, `$5K`, `5 10`, a line break, extra words, a second number, stray parentheses) is rejected with no change, as is a partial paste that adds a second decimal, a sign, or fraction digits past the precision. A whole-value paste truncates to the precision (`$1,234.5678` → `1,234.56`). A negative paste must replace the whole value and needs `allowNegative` (or an already-negative value). `allowNegative` permits a leading `-`; without it, adding a `-` (typed or pasted) is rejected rather than silently dropped, and an existing external negative keeps its sign while edited. `onChange` fires on every edit and currency switch — never for blur-without-edit or external value changes. `name` emits hidden `<name>` / `<name>_currency` inputs.
|
|
4
4
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# `<PhoneInput>`
|
|
1
|
+
# `<PhoneInput>` — E.164 phone field with country picker
|
|
2
2
|
|
|
3
3
|
International phone field: searchable country picker (DS `Select`) + national-number `Input`, controlled on a single `value: string | null` (**E.164**), `onChange(e164 | null)`. `defaultCountry` (ISO alpha-2) seeds the picker when empty; `countries` restricts the list; `size`/`invalid`/`disabled` pass through. Country names are localized via `Intl.DisplayNames`; metadata/validation via `libphonenumber-js`. Validate with the exported `isValidPhone(e164)` and drive `invalid` (or wrap in `<Field error>`). `countryDisplay` sets how the SELECTED country shows in the trigger — `"code"` (default, `+1`), `"iso"` (`US +1`), `"name"` (`United States +1`), or `"flag"` (`🇺🇸 +1`; emoji flags don't render on Windows Chrome/Edge). Dropdown rows always show the full name + code (searchable); opening the picker selects the text for instant type-to-search; the country can't be cleared. The picker's accessible name is composed from the group's (`<Field label="Phone">` → "Phone Country"), so it never collides with a form's own Country field; `countryLabel` overrides it. Store the emitted E.164, not the formatted display.
|
|
4
4
|
|
|
@@ -54,3 +54,10 @@ Use for per-file upload bars, wizard step indicators, form-completion meters and
|
|
|
54
54
|
- ❌ A hand-rolled `<div style={{ width: `${n}%`, … }}>` bar — use the primitive.
|
|
55
55
|
- ❌ `<Progress role="…">` — `role` is locked to `progressbar`; TypeScript rejects the override.
|
|
56
56
|
- ❌ A separate consumer-rendered label element — the `label` slot already handles spacing, font size and tabular-nums for stable digit widths.
|
|
57
|
+
|
|
58
|
+
## Progress hard rule
|
|
59
|
+
|
|
60
|
+
- ❌ Hand-rolled `<div style={{ width: '${n}%', background: '#xxx' }}>` progress bars — use `<Progress value={n}>`.
|
|
61
|
+
- ❌ Hand-rolled spinning `<svg>` per page (every Saving-state, every loader). Use `<CircularProgress />` indeterminate.
|
|
62
|
+
- ❌ `<Progress tone="success" value={100}>` or `<Progress tone="success" />` to "celebrate" completion. Tones communicate STATE during progress (warning at 85%, danger at 95%), not success-on-done. A finished bar is just a finished bar — leave the default tone.
|
|
63
|
+
- ❌ `<CircularProgress value={0}>` to render an empty circle for "not started yet." `value={0}` is determinate (0% done). The intent is usually indeterminate — omit `value` entirely.
|
package/docs/components/Text.md
CHANGED
|
@@ -61,3 +61,11 @@ If you need a size or tone the primitive doesn't expose, that is a token-vocabul
|
|
|
61
61
|
- `<Text as="h2">`: Text doesn't accept heading tags; use `<Title order={2}>`.
|
|
62
62
|
- Wrapping a `<Title>` in `<Text>` for tone/weight tweaks: pass tone/weight to the `<Title>`. `size="inherit"` is for runs INSIDE a heading, not for wrapping it.
|
|
63
63
|
- Nesting `<Text>` in another `<Text>` with the default `as="p"`: the inner `<p>` inside the outer `<p>` is invalid HTML and triggers React's DOM nesting warning. Use `<Text as="span" tone="...">` for an inline override inside a paragraph.
|
|
64
|
+
|
|
65
|
+
## Typography hard rule
|
|
66
|
+
|
|
67
|
+
- ❌ `style={{ fontSize: 'var(--font-size-sm)' }}` / `style={{ color: 'var(--color-fg-muted)' }}` — use `<Text size="sm" tone="muted">`.
|
|
68
|
+
- ❌ Raw `<h1>` / `<h2>` / `<h3>` — use `<Title order={N}>`.
|
|
69
|
+
- ❌ `<Text style={{ color: '#someHex' }}>` — pick a tone from the whitelist.
|
|
70
|
+
- For semantic emphasis (the bold _is_ the meaning — e.g. a user's name in a notification, an error keyword), use `<strong>` or `<em>` inside `<Text>`. For visual-only weight changes (a medium-weight name in a list because hierarchy says so, not because the name is emphatic), use `<Text weight="medium">`. Pick the one that matches _why_ the text is heavy.
|
|
71
|
+
- If the size / tone / weight you need isn't on `<Title>` or `<Text>`, **that's a token-vocabulary conversation, not a component-skipping conversation.**
|
package/docs/components/Title.md
CHANGED
|
@@ -52,3 +52,7 @@
|
|
|
52
52
|
- `<h2 className={styles.title}>`: use `<Title order={2}>`; consumer SCSS should never name a typography class.
|
|
53
53
|
- `<Title order={1} size="xs">`: usually a sign the heading hierarchy is wrong. Bump the order up instead of shrinking a low-order heading.
|
|
54
54
|
- Skipping heading levels (`order={1}` then `order={4}`) hurts screen-reader users; use sequential orders.
|
|
55
|
+
|
|
56
|
+
## Typography hard rule
|
|
57
|
+
|
|
58
|
+
No raw `<h1>` / `<h2>` / `<h3>`, no inline `style` font sizes or colours, `<strong>` / `<em>` vs `weight`: see [Text.md](Text.md#typography-hard-rule).
|