@peektravel/app-utilities 0.8.2 → 0.9.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/ui.md DELETED
@@ -1,1175 +0,0 @@
1
- # Odyssey Web Components — `@peektravel/app-utilities/ui`
2
-
3
- AI reference for the Odyssey UI components shipped under the `/ui` subpath. This
4
- is the **guide**; the authoritative, always-current contract is
5
- `dist/ui/index.d.ts` — read its TSDoc via go-to-definition before relying on any
6
- shape here. Document only what this file describes; never invent attributes,
7
- properties, or events.
8
-
9
- ---
10
-
11
- ## 1. Overview
12
-
13
- - **Framework-agnostic Custom Elements** (`<ody-*>`). Work in plain HTML, React,
14
- Vue, Angular, Svelte — anything that renders DOM.
15
- - **Light DOM** (no Shadow DOM). The shipped `odyssey.css` global classes style
16
- them; consuming apps can override with the same selectors.
17
- - **Dependency-free.** Native `Date`, `Intl`, `fetch`/`navigator.clipboard`,
18
- `customElements`. No date or popup libraries.
19
- - **Ported from the Peek Odyssey design system** (originally an Ember addon).
20
- - Importing the module **registers** every element as a side effect; the named
21
- exports (classes + types) are only for subclassing or type-checking.
22
-
23
- ---
24
-
25
- ## 2. Setup
26
-
27
- ```ts
28
- // Registers every <ody-*> element as a side effect (do this once, app entry):
29
- import '@peektravel/app-utilities/ui';
30
- // Design tokens (CSS custom properties — colours, fonts, z-index):
31
- import '@peektravel/app-utilities/ui/tokens.css';
32
- // Component styles:
33
- import '@peektravel/app-utilities/ui/odyssey.css';
34
- ```
35
-
36
- CSS can also be linked instead of imported:
37
-
38
- ```html
39
- <link rel="stylesheet" href="…/@peektravel/app-utilities/ui/tokens.css" />
40
- <link rel="stylesheet" href="…/@peektravel/app-utilities/ui/odyssey.css" />
41
- ```
42
-
43
- Registration is **idempotent** — importing the module more than once (or from
44
- more than one bundle) re-registers nothing and never throws, so lazy or
45
- duplicated imports are safe.
46
-
47
- If you register the bundle **lazily** (e.g. a dynamic `import()` to dodge SSR /
48
- hydration mismatches), two things stop un-upgraded elements from flashing as
49
- their raw text content (`<ody-button>Save</ody-button>` briefly showing the bare
50
- word "Save"):
51
-
52
- - `odyssey.css` ships a `:not(:defined) { visibility: hidden }` guard, so every
53
- `<ody-*>` stays invisible — layout preserved, no reflow — until it upgrades.
54
- Load the CSS **eagerly** so this applies before first paint. It has zero
55
- specificity, so you can override it for a deliberate fallback.
56
- - `whenOdysseyReady()` resolves once every imported element has upgraded, so you
57
- can gate your first render on it:
58
-
59
- ```ts
60
- import { whenOdysseyReady } from '@peektravel/app-utilities/ui';
61
- await whenOdysseyReady();
62
- // …now render your <ody-*> tree
63
- ```
64
-
65
- A *failed* dynamic import rejects at the `import()` call site — handle load
66
- failures there; `whenOdysseyReady()` reflects only the elements that did
67
- register. `registeredTags()` returns the registered tag list if you need to
68
- introspect it.
69
-
70
- Named exports for advanced use:
71
-
72
- ```ts
73
- import {
74
- toast, registerTranslation, registerIcon,
75
- iconNames, iconSvg, brandIconNames, brandIconSvg,
76
- OdyTable, type OdyTableColumn, // …classes + types
77
- } from '@peektravel/app-utilities/ui';
78
- ```
79
-
80
- ### 2.1 Spacing & layout tokens
81
-
82
- `tokens.css` publishes the canonical spacing scale and the two page widths.
83
- Reach for these instead of magic numbers so spacing stays consistent across
84
- apps even for one-off gaps:
85
-
86
- | Token | Value | Use for |
87
- | --- | --- | --- |
88
- | `--gap8` | `8px` | tight gaps |
89
- | `--gap16` | `16px` | the default gap between elements |
90
- | `--gap24` | `24px` | section-level spacing / page gutters |
91
- | `--gap32` | `32px` | large section spacing |
92
- | `--layout-page-width-narrow` | `868px` | the narrow settings-iframe width (design for this first) |
93
- | `--layout-page-width-wide` | `1310px` | the wide settings-iframe width |
94
-
95
- ```css
96
- .my-card { padding: var(--gap24); gap: var(--gap16); }
97
- ```
98
-
99
- The layout primitives already bake these in: `ody-app-page-container` uses the
100
- gap tokens for its responsive gutter, and `ody-section-*` / `ody-horizontal-divider`
101
- expose them through `gap-size` / `spacing` attributes.
102
-
103
- ---
104
-
105
- ## 3. Core conventions (read this — it is how all components work)
106
-
107
- ### 3.1 Scalar config = HTML attributes
108
- Strings and booleans are **attributes**. Boolean attributes are "on" when
109
- present (and not literally `="false"`):
110
-
111
- ```html
112
- <ody-button variant="primary" size="small" disabled>Save</ody-button>
113
- ```
114
-
115
- ### 3.2 Rich data = JS properties (cannot be attributes)
116
- Arrays, objects, and functions are set on the **element object**, not as
117
- attributes:
118
-
119
- ```js
120
- const table = document.querySelector('ody-table');
121
- table.columns = [{ key: 'name', label: 'Name' }]; // array
122
- table.data = [{ name: 'Alice' }]; // array
123
- datepicker.isDateDisallowed = (d) => d.getDay() === 0; // function
124
- datepicker.formatDate = (d) => d.toDateString(); // function
125
- ```
126
-
127
- Several composite components also accept a **JSON-string attribute fallback** so
128
- they work declaratively in plain HTML:
129
-
130
- | Component | JS property | JSON attribute fallback |
131
- |---|---|---|
132
- | `ody-dropdown-single` / `ody-dropdown-multi` / `ody-checkbox-group` | `el.options = [...]` | `options='[{"value":"a","label":"A"}]'` |
133
- | `ody-tabs` | — | `tabs='[{"id":"a","label":"A"}]'` |
134
- | `ody-toggle-button` | — | `options='[{"value":"a","label":"A"}]'` |
135
- | `ody-radio-button-group` | `el.options` (getter) | `options='[{"value":"a","label":"A"}]'` |
136
- | `ody-datepicker` | — | `presets='[{"label":"…","value":"…"}]'` |
137
- | `ody-table` | `el.columns`, `el.data` | none — JS only |
138
-
139
- ### 3.3 Output = `CustomEvent` (read `event.detail`)
140
- Components emit bubbling `CustomEvent`s. Listen with `addEventListener`; read
141
- data from `event.detail`. Event names are **custom** (e.g. `change`,
142
- `sort-change`) — see each component.
143
-
144
- ```js
145
- el.addEventListener('change', (e) => console.log(e.detail.value));
146
- ```
147
-
148
- ### 3.4 Content = light-DOM children (the default slot)
149
- Most components render your child nodes into an internal `[data-ody-slot]`
150
- placeholder. So the element's children become its body/label content:
151
-
152
- ```html
153
- <ody-button>This text is the label</ody-button>
154
- <ody-alert heading="Saved">This is the body content.</ody-alert>
155
- ```
156
-
157
- Although your children are physically relocated into that placeholder, the host
158
- element forwards the standard child-mutation calls
159
- (`appendChild` / `insertBefore` / `removeChild` / `replaceChild`) to the slot.
160
- So a framework reconciler that adds, removes, reorders, or swaps a **direct**
161
- child of a slotting element operates on the slot correctly — conditional,
162
- keyed, and `.map()`-ed children all reconcile without a
163
- `NotFoundError: … not a child of this node`. You do **not** need to wrap dynamic
164
- children in a single stable `<div>`.
165
-
166
- ### 3.5 Framework usage
167
-
168
- **Vanilla HTML / JS** — declarative; set rich data and add listeners via the
169
- element reference.
170
-
171
- **React < 19** does not set DOM properties or bind custom events from JSX
172
- attributes. Use lowercase tag names, a `ref` to set properties, and
173
- `addEventListener` for events:
174
-
175
- ```jsx
176
- function Guests({ columns, rows, onSort }) {
177
- const ref = useRef(null);
178
- useEffect(() => {
179
- const el = ref.current;
180
- el.columns = columns; // JS property
181
- el.data = rows; // JS property
182
- const handler = (e) => onSort(e.detail); // { key, direction }
183
- el.addEventListener('sort-change', handler);
184
- return () => el.removeEventListener('sort-change', handler);
185
- }, [columns, rows, onSort]);
186
- return <ody-table ref={ref} selectable sortable />;
187
- }
188
- ```
189
-
190
- **React 19** sets JSX props on a custom element as DOM **properties**
191
- (`el.searchable = true`), not attributes. The components are built for this: every
192
- reflected/read-only accessor accepts assignment, so passing props directly works
193
- and does **not** throw the `Cannot set property … which has only a getter` error
194
- a getter-only property would otherwise raise. Concretely:
195
-
196
- - **Boolean props reflect to the attribute.** `searchable`, `disabled`, etc.
197
- take effect as attribute presence:
198
-
199
- ```jsx
200
- <ody-dropdown-single searchable options={items} value={value} />
201
- ```
202
-
203
- - **Rich-data props are real properties.** `options` (dropdowns, checkbox-group,
204
- radio-button-group, toggle-button), `presets` (datepicker), and `data` /
205
- `columns` / `rows` (table) are true getter+setter properties. Pass an **array**
206
- directly (`options={items}`); a JSON **string** is also accepted for
207
- backwards-compatibility. They are the source of truth and are **not** reflected
208
- back onto a DOM attribute (so no giant JSON attributes).
209
-
210
- - **Controlled `value` never fights the caret.** The text/number fields
211
- (`ody-input`, `ody-inline-input`, `ody-search-input`, `ody-money-input`,
212
- `ody-percentage-input`) reflect an externally-set `value` into the live control
213
- in place, and the selection controls update their checked state in place — so a
214
- controlled binding keeps focus. `ody-checkbox-group`'s `value` **attribute** is
215
- a JSON array (comma-safe); its `.value` **property** and the `change` detail are
216
- a `string[]`.
217
-
218
- - **Read-only state accessors ignore assignment.** Props that mirror live
219
- internal state (`isOpen`, `isVisible`) are inert to writes — they are not
220
- controlled by re-render. Drive them imperatively through a `ref`
221
- (`ref.current.openPopover()` / `.show()`) and observe changes via the
222
- component's `CustomEvent`s.
223
-
224
- TypeScript still types these accessors as read-only (the setters are a runtime
225
- safety net for framework-driven assignment), so `el.isOpen = true` in TS code is
226
- flagged — assign only via JSX props / refs as above.
227
-
228
- **Vue / Angular** support custom elements natively: bind properties with
229
- `:prop` / `[prop]` and events with `@event` / `(event)`. (Configure Vue's
230
- `compilerOptions.isCustomElement` / Angular's `CUSTOM_ELEMENTS_SCHEMA`.)
231
-
232
- ---
233
-
234
- ## 4. When to use what
235
-
236
- | Need | Use |
237
- |---|---|
238
- | Action / submit | `ody-button`; primary + menu → `ody-split-button` |
239
- | Single choice from a list | `ody-dropdown-single` (or native `<select>`) |
240
- | Multiple choices from a list | `ody-dropdown-multi` (or native `<select multiple>`) |
241
- | Mutually-exclusive segmented control | `ody-toggle-button` or `ody-radio-button-group` |
242
- | Several independent toggles | `ody-checkbox-group` (with optional select-all) |
243
- | Single boolean | `ody-checkbox` |
244
- | Free text / multi-line | `ody-input` (`textarea`), compact → `ody-inline-input` |
245
- | Search field | `ody-search-input` |
246
- | Currency amount | `ody-money-input` |
247
- | Percentage 0–100 | `ody-percentage-input` |
248
- | Date / date range | `ody-datepicker` |
249
- | Tabular data, sorting, selection | `ody-table` |
250
- | Tab navigation | `ody-tabs` |
251
- | Transient feedback | `toast()` (+ `ody-toast-host`) |
252
- | Confirm / blocking dialog | `ody-modal` |
253
- | Slide-in side detail panel | `ody-panel` |
254
- | Click-to-open floating menu | `ody-popover` |
255
- | Hover/focus hint | `ody-tooltip` |
256
- | Status / contextual message | `ody-alert`, `ody-message`, `ody-status-dot`, `ody-check-in-status` |
257
- | Labels / pills | `ody-tag` |
258
- | Empty / error / no-results view | `ody-empty-state` |
259
- | KPI figures | `ody-stat-summary` + `ody-stat` |
260
- | Expand/collapse | `ody-accordion`, `ody-collapsible`, `ody-collapsible-section` |
261
- | Full-page wrapper for an **app settings UI** | `ody-app-page-container` (required — see below; `ody-page-container` is the deprecated full-bleed predecessor) |
262
- | Master/detail page layout | `ody-two-column` |
263
- | Row/column section layout | `ody-section-rows` / `ody-section-columns` |
264
- | Progress | `ody-loading-bar`, `ody-loading-spinner` |
265
- | Themeable icon | `ody-icon`; brand/logo art → `ody-brand-icon` |
266
-
267
- ---
268
-
269
- ## 5. Component catalog
270
-
271
- Conventions below: **Attributes** are HTML attributes; **Properties (JS)** must
272
- be set on the element object; **Events** bubble and are read via `event.detail`;
273
- **Slots / content** describes what child nodes do. Defaults are noted.
274
-
275
- ---
276
-
277
- ### Display tier
278
-
279
- #### `<ody-icon>`
280
- **Use when** you need a themeable inline SVG that inherits text colour.
281
- **Attributes**
282
- - `name` — icon name (see `iconNames()`); unknown names render nothing.
283
- - `size` — `extra-small` | `mid-small` | `small` | `base` | `medium` | `large` | `free` (default `base`).
284
- - `disabled` — boolean; dims the icon.
285
- **Example**
286
- ```html
287
- <ody-icon name="search" size="medium"></ody-icon>
288
- ```
289
-
290
- #### `<ody-button>`
291
- **Use when** triggering an action. Label is the child content.
292
- **Attributes**
293
- - `variant` — `primary` | `secondary` | `ghost` | `tertiary` | `danger` (default `primary`).
294
- - `appearance` — `interaction` | `success` | `danger` | `grey` (default `interaction`).
295
- - `size` — `base` | `small` (default `base`).
296
- - `type` — `button` | `submit` (default `button`).
297
- - `left-icon` / `right-icon` — icon names beside the label.
298
- - `loading`, `icon-only`, `active`, `disabled`, `icon-rotate` — boolean flags.
299
- **Slots / content** — child nodes are the label.
300
- **Example**
301
- ```html
302
- <ody-button variant="primary" left-icon="plus">Add item</ody-button>
303
- <ody-button icon-only left-icon="search" aria-label="Search"></ody-button>
304
- ```
305
- *(No custom event — listen for native `click`.)*
306
-
307
- #### `<ody-tag>`
308
- **Use when** showing a status pill / label. Label is the child content.
309
- **Attributes**
310
- - `variant` — `primary` (filled) | `secondary` (outline) (default `primary`).
311
- - `color` — `default` | `info` | `success` | `warning` | `danger` | `teal` | `lime` | `purple` | `pink` | `turquoise` | `yellow` | `orange` | `rose` | `blue` (default `default`).
312
- - `size` — `base` | `small` (default `base`).
313
- - `icon` — optional leading icon name.
314
- - `count` — optional trailing count.
315
- **Example**
316
- ```html
317
- <ody-tag color="success" icon="check">Confirmed</ody-tag>
318
- <ody-tag variant="secondary" color="purple" count="4">Tasks</ody-tag>
319
- ```
320
-
321
- #### `<ody-alert>`
322
- **Use when** showing a contextual message box. Body is the child content.
323
- **Attributes**
324
- - `variant` — `info` | `success` | `warning` | `danger` (default `info`).
325
- - `heading` — optional bold heading beside the icon.
326
- **Slots / content** — child nodes are the body.
327
- **Example**
328
- ```html
329
- <ody-alert variant="warning" heading="Careful">This cannot be undone.</ody-alert>
330
- ```
331
-
332
- #### `<ody-card>`
333
- **Use when** grouping content in a bordered container. Reach for the plain
334
- `no-bar` card by default; add a left accent bar only when the bar's colour
335
- carries meaning (e.g. status).
336
- **Attributes**
337
- - `no-bar` — boolean; omits the accent bar, rendering a plain bordered
338
- container. **This is the default card to use.**
339
- - `bar-color` — CSS colour for the accent bar (default neutral-200). Ignored
340
- when `no-bar` is set.
341
- - `clickable` — boolean; adds pointer affordance (handle `click` yourself).
342
- **Slots / content** — child nodes are the card content.
343
- **Example**
344
- ```html
345
- <!-- Default: plain bordered card -->
346
- <ody-card no-bar><p>Card body</p></ody-card>
347
-
348
- <!-- Accent bar when the colour is meaningful -->
349
- <ody-card bar-color="var(--color-interaction-300)"><p>Card body</p></ody-card>
350
- ```
351
-
352
- #### `<ody-horizontal-divider>`
353
- **Use when** you need a 1px horizontal rule that separates content. Unlike the
354
- deprecated `<ody-divider>`, it carries its own vertical rhythm
355
- (`margin-block: var(--gap16)`) so you don't have to add spacing around it.
356
- **Attributes**
357
- - `spacing` — `tight` (`--gap8`) | `none` (`0`); default is the normal `--gap16`.
358
- ```html
359
- <ody-horizontal-divider></ody-horizontal-divider>
360
- <ody-horizontal-divider spacing="tight"></ody-horizontal-divider>
361
- ```
362
-
363
- #### `<ody-divider>` _(deprecated — use `<ody-horizontal-divider>`)_
364
- **Use when** you need a 1px horizontal rule. No attributes, no content.
365
- **Deprecated:** renders with **zero** margin, so every caller has to add its own
366
- spacing. Prefer `<ody-horizontal-divider>`. Still works and unchanged.
367
- ```html
368
- <ody-divider></ody-divider>
369
- ```
370
-
371
- #### `<ody-status-dot>`
372
- **Use when** showing a coloured dot beside a label. Label is the child content.
373
- **Attributes**
374
- - `color` — `green` | `blue` | `orange` (default `green`).
375
- **Example**
376
- ```html
377
- <ody-status-dot color="blue">Pending</ody-status-dot>
378
- ```
379
-
380
- #### `<ody-message>`
381
- **Use when** showing a small inline status message. Text is the child content.
382
- **Attributes**
383
- - `icon` — optional leading icon name.
384
- **Example**
385
- ```html
386
- <ody-message icon="info">Saved as draft.</ody-message>
387
- ```
388
-
389
- #### `<ody-loading-spinner>`
390
- **Use when** indicating indeterminate loading. Optional label is the child content.
391
- **Attributes**
392
- - `size` — `small` | `base` | `large` (default `base`).
393
- **Example**
394
- ```html
395
- <ody-loading-spinner size="large">Loading…</ody-loading-spinner>
396
- ```
397
-
398
- #### `<ody-loading-bar>`
399
- **Use when** showing determinate progress.
400
- **Attributes**
401
- - `value` — fill percentage, clamped 0–100 (default `0`).
402
- - `color` — CSS colour for the fill (default interaction-300).
403
- - `label` — optional caption shown above the bar with the percentage.
404
- **Example**
405
- ```html
406
- <ody-loading-bar value="35" label="Uploading photos"></ody-loading-bar>
407
- ```
408
-
409
- ---
410
-
411
- ### Layout / structure tier
412
-
413
- #### `<ody-empty-state>`
414
- **Use when** rendering an empty / error / no-results placeholder. Child content
415
- (e.g. an action button) renders below the messages.
416
- **Attributes**
417
- - `variant` — `default` | `error` | `no-results` | `no-search` | `not-authorized` (default `default`); selects a fallback icon.
418
- - `img-src` / `img-alt` — render a custom image instead of the icon.
419
- - `icon` — override the illustration with a named icon.
420
- - `label` — bold primary message.
421
- - `caption` — secondary message.
422
- **Example**
423
- ```html
424
- <ody-empty-state variant="no-results" label="No results" caption="Try another search.">
425
- <ody-button>Clear filters</ody-button>
426
- </ody-empty-state>
427
- ```
428
-
429
- #### `<ody-breadcrumb>` + `<ody-breadcrumb-item>`
430
- **Use when** showing a breadcrumb trail. Compose items as children.
431
- `ody-breadcrumb` has no attributes (renders the `<nav><ol>` chrome).
432
- `ody-breadcrumb-item` **Attributes**: `current` — boolean; marks the active page
433
- (`aria-current="page"`). Each item's child content is the crumb label/link.
434
- **Example**
435
- ```html
436
- <ody-breadcrumb>
437
- <ody-breadcrumb-item><a href="/">Home</a></ody-breadcrumb-item>
438
- <ody-breadcrumb-item current>Details</ody-breadcrumb-item>
439
- </ody-breadcrumb>
440
- ```
441
-
442
- #### `<ody-stat-summary>` + `<ody-stat>` + `<ody-stat-summary-detail>` + `<ody-stat-detail>`
443
- **Use when** displaying a row of KPI figures with an optional detail strip.
444
- - `ody-stat-summary` — no attributes; wraps `ody-stat` children.
445
- - `ody-stat` **Attributes**: `label`, `value` (falls back to child content if omitted), `sub`, `tone` (`default` | `success` | `warning`).
446
- - `ody-stat-summary-detail` — no attributes; the divider + detail strip; place as a **sibling immediately after** the stat summary; wraps `ody-stat-detail` children.
447
- - `ody-stat-detail` **Attributes**: `value` (bold value appended after the child-content label).
448
- **Example**
449
- ```html
450
- <ody-stat-summary>
451
- <ody-stat label="Revenue" value="$12,400" sub="last 30 days"></ody-stat>
452
- <ody-stat label="Bookings" value="312" tone="success"></ody-stat>
453
- </ody-stat-summary>
454
- <ody-stat-summary-detail>
455
- <ody-stat-detail value="$1,030">Avg / booking</ody-stat-detail>
456
- </ody-stat-summary-detail>
457
- ```
458
-
459
- #### `<ody-inline-list>` + `<ody-inline-list-item>`
460
- **Use when** rendering a horizontal separated list. Compose items as children.
461
- - `ody-inline-list` **Attributes**: `separator` (`line` | `bullet`, default `line`), `gap` (px between items, default `12`).
462
- - `ody-inline-list-item` — no attributes; child content is the item.
463
- **Example**
464
- ```html
465
- <ody-inline-list separator="bullet" gap="8">
466
- <ody-inline-list-item>Draft</ody-inline-list-item>
467
- <ody-inline-list-item>Updated 3d ago</ody-inline-list-item>
468
- </ody-inline-list>
469
- ```
470
-
471
- #### `<ody-list-item>`
472
- **Use when** a clickable list row with a trailing chevron. Row content is the
473
- child content.
474
- **Attributes**
475
- - `item-id` — id emitted in the `select` event.
476
- - `active-item-id` — when equal to `item-id`, the row renders active.
477
- - `active` — boolean; force the active style.
478
- **Events**
479
- - `select` → `{ itemId }` — on click or Enter/Space.
480
- **Example**
481
- ```html
482
- <ody-list-item item-id="1" active-item-id="1"><strong>Sunset Kayak</strong></ody-list-item>
483
- ```
484
-
485
- #### `<ody-product-indicator>`
486
- **Use when** showing a product name with a coloured bar and optional detail.
487
- Extra content (e.g. a tag) is the child content.
488
- **Attributes**
489
- - `name` — product name (bold).
490
- - `detail` — secondary detail line.
491
- - `bar-color` — CSS colour for the bar (default neutral-400).
492
- - `text-color` — CSS colour for the name.
493
- - `size` — `base` | `small` (default `base`).
494
- - `indicator-id` — emitted in `select` (when `clickable`).
495
- - `clickable` — boolean; makes the whole indicator a button.
496
- **Events**
497
- - `select` → `{ id }` — only when `clickable`.
498
- **Example**
499
- ```html
500
- <ody-product-indicator name="Sunset Kayak Tour" detail="2 hours · Max 12"
501
- bar-color="var(--color-turquoise-300)"></ody-product-indicator>
502
- ```
503
-
504
- #### `<ody-toggle-button>`
505
- **Use when** a segmented control of mutually-exclusive options.
506
- **Attributes**
507
- - `options` — JSON array of `{ value, label?, leftIcon?, rightIcon?, iconOnly?, disabled? }`.
508
- - `selected` — the currently-selected option value (reflected on change).
509
- - `size` — `base` | `small` (default `base`).
510
- - `disabled` — boolean; disables the whole group.
511
- **Events**
512
- - `change` → `{ value }`.
513
- **Example**
514
- ```html
515
- <ody-toggle-button selected="week"
516
- options='[{"value":"day","label":"Day"},{"value":"week","label":"Week"}]'></ody-toggle-button>
517
- ```
518
-
519
- #### `<ody-section-columns>` + `<ody-section-column-item>`
520
- **Use when** laying out children in a row.
521
- - `ody-section-columns` **Attributes**: `gap-size` (`8` | `16` | `24` | `32` px, default `16`), `vertical-align` (`center` | `top` | `bottom`, default `bottom`).
522
- - `ody-section-column-item` **Attributes** (boolean): `no-grow`, `individual-scroll`, `stretch`, `with-padding`. Child content is the column body.
523
- **Example**
524
- ```html
525
- <ody-section-columns gap-size="24" vertical-align="top">
526
- <ody-section-column-item stretch>Left</ody-section-column-item>
527
- <ody-section-column-item no-grow>Right</ody-section-column-item>
528
- </ody-section-columns>
529
- ```
530
-
531
- #### `<ody-section-rows>` + `<ody-section-row-item>`
532
- **Use when** stacking children vertically with optional row headers.
533
- - `ody-section-rows` **Attributes**: `gap-size` (`8` | `16` | `24` | `32` px, default `16`).
534
- - `ody-section-row-item` **Attributes**: `title`, `description`, `description-icon` (icon name), `no-divider` (boolean). Child content is the row body.
535
- **Example**
536
- ```html
537
- <ody-section-rows gap-size="16">
538
- <ody-section-row-item title="General" description="Basic details" description-icon="info">
539
- Row body
540
- </ody-section-row-item>
541
- <ody-section-row-item title="Advanced" no-divider>More</ody-section-row-item>
542
- </ody-section-rows>
543
- ```
544
-
545
- #### `<ody-two-column>` + parts
546
- **Use when** a master/detail layout. The secondary panel shows only when
547
- `secondary-open` is present.
548
- - `ody-two-column` **Attributes**: `secondary-open` (boolean).
549
- - `ody-two-column-main` — no attributes; primary content area.
550
- - `ody-two-column-secondary` — no attributes; detail panel (hidden unless parent open).
551
- - `ody-two-column-secondary-header` **Attributes**: `title`. **Events**: `close` (clicking the close button).
552
- **Example**
553
- ```html
554
- <ody-two-column secondary-open>
555
- <ody-two-column-main>…list…</ody-two-column-main>
556
- <ody-two-column-secondary>
557
- <ody-two-column-secondary-header title="Details"></ody-two-column-secondary-header>
558
- …detail…
559
- </ody-two-column-secondary>
560
- </ody-two-column>
561
- ```
562
-
563
- #### `<ody-app-page-container>`
564
- > [!IMPORTANT]
565
- > **Every app settings UI must wrap its content in `<ody-app-page-container>`.**
566
- > It is the standard responsive page wrapper that keeps the settings page sized
567
- > to the two widths the settings host iframe renders at — **868px** (narrow) and
568
- > **1310px** (wide).
569
- >
570
- > **Design for 868px first — it is the default.** Optimise every settings layout
571
- > for the narrow 868px width, then make sure it *also* renders correctly when the
572
- > host expands it to 1310px (e.g. progressively widen or add columns; never
573
- > assume the extra width is present). Do **not** design 1310px-first and let it
574
- > degrade — 868px is the baseline that must always look right.
575
-
576
- **Use when** building the top-level layout of an app settings page (or any
577
- full-page UI embedded in the settings host).
578
- The container is `width: 100%`, so it fills whichever of the two widths the
579
- parent iframe gives it; a `max-width` of `1310px` caps and centres it if it is
580
- ever embedded somewhere wider. Unlike the deprecated `<ody-page-container>`, it
581
- ships a **default responsive gutter** — `var(--gap24)` at the wide width,
582
- tightening to `var(--gap16)` at/below 868px — so you no longer wrap the page in
583
- a padded `<div>`. It also establishes a CSS **container context** named
584
- `ody-page`, so the content inside can adapt between the two widths with container
585
- queries instead of viewport media queries — treat 868px as the base styles and
586
- use a container query to *enhance* for the wider view.
587
- **Attributes**
588
- - `flush` — drop the default gutter and render edge-to-edge, for the rare layout
589
- that truly wants full-bleed content.
590
- **Slots / content** — child nodes are the page content.
591
- **Example**
592
- ```html
593
- <ody-app-page-container>
594
- <ody-section-rows gap-size="24">
595
- …settings UI…
596
- </ody-section-rows>
597
- </ody-app-page-container>
598
- ```
599
- ```css
600
- /* Base styles target the default 868px width… */
601
- .my-settings-grid { grid-template-columns: 1fr; }
602
- /* …then progressively enhance for the expanded 1310px view. */
603
- @container ody-page (min-width: 1310px) {
604
- .my-settings-grid { grid-template-columns: 1fr 1fr; }
605
- }
606
- ```
607
-
608
- #### `<ody-page-container>` _(deprecated — use `<ody-app-page-container>`)_
609
- **Deprecated:** the original page wrapper is **full-bleed** (edge-to-edge, no
610
- side gutters), so every settings UI had to add its own edge padding. Prefer
611
- `<ody-app-page-container>`, which bakes in a responsive gutter (with a `flush`
612
- opt-out) while keeping the same widths and `ody-page` container context. This
613
- element still works and is unchanged; migrate by renaming the tag (add `flush`
614
- if you genuinely relied on the full-bleed behaviour).
615
- **Attributes** — none; the width is driven entirely by the parent.
616
-
617
- #### `<ody-collapsible-section>` + `<ody-collapsible-collapsed>` + `<ody-collapsible-content>`
618
- **Use when** a titled section that expands/collapses on header click.
619
- - `ody-collapsible-section` **Attributes**: `header-text`, `secondary-header-text`, `icon` (icon name), `expanded` (boolean; reflected). **Events**: `expanded` → `{ expanded }`.
620
- - `ody-collapsible-collapsed` — teaser shown only while collapsed.
621
- - `ody-collapsible-content` — body shown only while expanded.
622
- **Example**
623
- ```html
624
- <ody-collapsible-section header-text="Booking details" secondary-header-text="(3 items)" icon="info">
625
- <ody-collapsible-collapsed>Tap to see more</ody-collapsible-collapsed>
626
- <ody-collapsible-content>…full content…</ody-collapsible-content>
627
- </ody-collapsible-section>
628
- ```
629
-
630
- ---
631
-
632
- ### Form inputs
633
-
634
- All text inputs render a native control in the light DOM (form-friendly) and
635
- expose a JS `value` property mirroring the `value` attribute.
636
-
637
- #### `<ody-input>`
638
- **Use when** a standard text field (or multi-line textarea).
639
- **Attributes**
640
- - `label`, `placeholder`, `value`, `caption`.
641
- - `size` — `base` | `small` (default `base`).
642
- - `icon` — leading icon name.
643
- - `warning` / `info` — message text; also sets the state ring.
644
- - `maxlength` — when set, shows a character counter.
645
- - `textarea` — boolean; render a `<textarea>`.
646
- - `readonly`, `disabled`, `full-width`, `max-content` — boolean flags.
647
- - `no-clear` — boolean; suppress the clear button.
648
- **Properties (JS)** — `value: string` (get/set).
649
- **Events**
650
- - `input` → `{ value }` — every keystroke.
651
- - `change` → `{ value }` — on native change / clear.
652
- **Example**
653
- ```html
654
- <ody-input label="Name" placeholder="Full name" maxlength="40"></ody-input>
655
- <ody-input textarea label="Notes" full-width></ody-input>
656
- ```
657
-
658
- #### `<ody-inline-input>`
659
- **Use when** the compact, borderless inline variant of `ody-input` (e.g. inside
660
- tables/forms). Same attributes, properties, and events as `ody-input`.
661
- **Example**
662
- ```html
663
- <ody-inline-input label="Qty" value="2" size="small"></ody-inline-input>
664
- ```
665
-
666
- #### `<ody-search-input>`
667
- **Use when** a search field with a leading search icon.
668
- **Attributes**
669
- - `value`, `placeholder` (default `"Search"`).
670
- - `size` — `base` | `small` (default `base`).
671
- - `info-message` — neutral helper caption.
672
- - `warning-message` — warning caption + ring.
673
- - `disabled` — boolean.
674
- **Properties (JS)** — `value: string`.
675
- **Events** — `input` → `{ value }`; `change` → `{ value }`.
676
- **Example**
677
- ```html
678
- <ody-search-input placeholder="Search bookings"></ody-search-input>
679
- ```
680
-
681
- #### `<ody-money-input>`
682
- **Use when** entering a currency amount. On blur the value is sanitised, clamped
683
- to `max-amount`, and rounded to the currency precision (JPY → 0 decimals, else 2).
684
- **Attributes**
685
- - `value` — amount string.
686
- - `currency` — ISO code for symbol + precision (default `USD`).
687
- - `max-amount` — optional upper clamp applied on blur.
688
- - `label`, `size`, `warning`.
689
- - `readonly`, `disabled` — boolean.
690
- **Properties (JS)** — `value: string` (get/set); `precision: number` (read-only).
691
- **Events** — `input` → `{ value }` (per keystroke); `change` → `{ value }` (on blur, after formatting).
692
- **Example**
693
- ```html
694
- <ody-money-input label="Price" currency="USD" value="49.99" max-amount="500"></ody-money-input>
695
- ```
696
-
697
- #### `<ody-percentage-input>`
698
- **Use when** entering a percentage 0–100 with a trailing `%`. Invalid keystrokes
699
- are blocked; on blur the value is clamped/normalised.
700
- **Attributes**
701
- - `value`, `label`, `size`, `warning`.
702
- - `readonly`, `disabled` — boolean.
703
- **Properties (JS)** — `value: string`.
704
- **Events** — `input` → `{ value }` (per keystroke); `change` → `{ value }` (on blur, after clamping).
705
- **Example**
706
- ```html
707
- <ody-percentage-input label="Discount" value="10"></ody-percentage-input>
708
- ```
709
-
710
- #### `<ody-checkbox>`
711
- **Use when** a single boolean toggle. Optional label and extra child content.
712
- **Attributes**
713
- - `label` — text beside the box.
714
- - `size` — `base` | `small` (default `small`).
715
- - `checked`, `indeterminate`, `disabled` — boolean (`checked` is reflected).
716
- **Properties (JS)** — `checked: boolean`; `value: boolean` (alias of `checked`).
717
- **Events** — `change` → `{ checked, value }` (both boolean, equal).
718
- **Example**
719
- ```html
720
- <ody-checkbox label="I agree" checked></ody-checkbox>
721
- ```
722
-
723
- #### `<ody-radio-button-group>`
724
- **Use when** mutually-exclusive radio options.
725
- **Attributes**
726
- - `options` — JSON array of `{ label, value }`.
727
- - `value` — selected option value.
728
- - `size` — `base` | `small` (default `base`).
729
- - `disabled` — boolean; applies to every option.
730
- **Properties (JS)** — `value: string`; `options: OdyRadioOption[]` (read-only getter, parses the attribute).
731
- **Events** — `change` → `{ value }`.
732
- **Example**
733
- ```html
734
- <ody-radio-button-group value="m"
735
- options='[{"label":"Monthly","value":"m"},{"label":"Yearly","value":"y"}]'></ody-radio-button-group>
736
- ```
737
-
738
- #### `<ody-checkbox-group>`
739
- **Use when** several toggles, optionally with a "select all" parent (checked
740
- when all selected, indeterminate when some).
741
- **Attributes**
742
- - `options` — JSON array of `{ label, value }` (also a JS property, below).
743
- - `value` — comma-separated selected values.
744
- - `select-all-label` — when present, renders the parent select-all row.
745
- - `size` — `base` | `small` (default `small`).
746
- - `disabled` — boolean.
747
- **Properties (JS)** — `value: string[]` (get returns array, set takes array); `options: OdyCheckboxOption[]` (get parses the attribute, set writes the JSON attribute — assign `el.options = [...]` as with `<ody-dropdown-*>`).
748
- **Events** — `change` → `{ value: string[] }`.
749
- **Example**
750
- ```html
751
- <ody-checkbox-group select-all-label="All toppings" value="cheese,olives"
752
- options='[{"label":"Cheese","value":"cheese"},{"label":"Olives","value":"olives"},{"label":"Basil","value":"basil"}]'></ody-checkbox-group>
753
- ```
754
-
755
- ---
756
-
757
- ### Interactive
758
-
759
- #### `<ody-accordion>`
760
- **Use when** an expand/collapse panel with a header. Body is the child content.
761
- **Attributes**
762
- - `title` — header label text.
763
- - `open` — boolean; expanded state (reflected, toggled on header click).
764
- - `sticky` — boolean; pins the header while scrolling.
765
- **Events** — `toggle` → `{ open }`.
766
- **Example**
767
- ```html
768
- <ody-accordion title="Itinerary" open><p>Day 1…</p></ody-accordion>
769
- ```
770
-
771
- #### `<ody-collapsible>`
772
- **Use when** a lightweight inline collapsible. Body is the child content.
773
- **Attributes**
774
- - `label` — header label text.
775
- - `open` — boolean (reflected, toggled on header click).
776
- **Events** — `toggle` → `{ open }`.
777
- **Example**
778
- ```html
779
- <ody-collapsible label="Show more"><p>Hidden details…</p></ody-collapsible>
780
- ```
781
-
782
- #### `<ody-tabs>`
783
- **Use when** tab navigation. Renders only the tab list; you swap the panels.
784
- **Attributes**
785
- - `tabs` — JSON array of `{ id, label, disabled? }`.
786
- - `active` — id of the active tab (reflected, defaults to the first tab).
787
- - `size` — `base` | `small` (default `base`).
788
- - `position` — `top` | `left` (default `top`).
789
- **Events** — `change` → `{ id }`.
790
- **Example**
791
- ```html
792
- <ody-tabs active="overview"
793
- tabs='[{"id":"overview","label":"Overview"},{"id":"notes","label":"Notes","disabled":true}]'></ody-tabs>
794
- ```
795
-
796
- #### `<ody-copy-button>`
797
- **Use when** copying text to the clipboard with transient success feedback.
798
- **Attributes**
799
- - `value` — text copied to the clipboard.
800
- - `label` — optional label (icon-only when omitted).
801
- - `success-duration` — ms the success/error state shows (default `1200`).
802
- **Events** — `copy` → `{ value, ok }` (`ok: boolean` — `false` if the clipboard API is unavailable or write failed).
803
- **Example**
804
- ```html
805
- <ody-copy-button value="ABC-123" label="Copy code"></ody-copy-button>
806
- ```
807
-
808
- #### `<ody-check-in-status>`
809
- **Use when** showing a fulfillment/check-in status with an icon + label.
810
- **Attributes**
811
- - `status` — `IN_PROGRESS` | `NO_SHOW` | `NONE` | `OVERDUE` | `RESERVED` | `RETURNED` (default `RESERVED`); selects icon, colour, and default (localized) label.
812
- - `label` — overrides the status's default label.
813
- - `optional-text` — secondary line below the label.
814
- **Example**
815
- ```html
816
- <ody-check-in-status status="RETURNED" optional-text="2:30 PM"></ody-check-in-status>
817
- ```
818
-
819
- #### `<ody-option>`
820
- **Use when** a selectable menu/list row (e.g. inside a popover menu). Label is
821
- the child content.
822
- **Attributes**
823
- - `selected` — boolean; marks selected.
824
- - `disabled` — boolean; disables interaction.
825
- - `sub-menu` — boolean; shows a trailing sub-menu trigger.
826
- - `trigger-icon` — icon name for the sub-menu trigger (default `chevron-right`).
827
- **Events**
828
- - `select` — clicking the row (no detail).
829
- - `submenu` — clicking the sub-menu trigger (no detail).
830
- **Example**
831
- ```html
832
- <ody-option>Duplicate</ody-option>
833
- <ody-option sub-menu>Move to…</ody-option>
834
- ```
835
-
836
- #### `<ody-split-button>`
837
- **Use when** a primary action joined to a dropdown toggle. Panel content is the
838
- child content.
839
- **Attributes**
840
- - `text` — primary button label.
841
- - `variant` — button variant (default `secondary`).
842
- - `size` — `base` | `small` (default `base`).
843
- - `icon` — toggle icon name (default `chevron-down`).
844
- - `open` — boolean; whether the panel is shown (reflected).
845
- **Events**
846
- - `primary` — clicking the main button (no detail).
847
- - `toggle` → `{ open }` — clicking the dropdown toggle.
848
- **Example**
849
- ```html
850
- <ody-split-button text="Save" variant="primary">
851
- <ody-option>Save and close</ody-option>
852
- </ody-split-button>
853
- ```
854
-
855
- #### `<ody-table-header>`
856
- **Use when** a single sortable column header (standalone; for full tables use
857
- `ody-table`). Clicking cycles none → desc → asc → none.
858
- **Attributes**
859
- - `column-name` — header text.
860
- - `column-id` — passed through in the `sort` event.
861
- - `direction` — `UNSET` | `ASC` | `DESC` (reflected, default `UNSET`).
862
- - `static` — boolean; non-interactive header, no sort affordance.
863
- - `rounded-left` / `rounded-right` — boolean; round the matching corners.
864
- **Events** — `sort` → `{ direction, columnId }` (`direction` is `UNSET` | `ASC` | `DESC`).
865
- **Example**
866
- ```html
867
- <ody-table-header column-name="Date" column-id="date" direction="ASC"></ody-table-header>
868
- ```
869
-
870
- ---
871
-
872
- ### Overlays
873
-
874
- All overlay panels are portaled to `document.body` so they escape clipping
875
- ancestors.
876
-
877
- #### `<ody-modal>`
878
- **Use when** a blocking dialog. Body is the child content.
879
- **Attributes**
880
- - `open` — boolean; shown when present (mirrors `open()` / `close()`).
881
- - `heading` — optional title.
882
- - `size` — `base` | `small` | `medium` | `large` | `full` (default `base`).
883
- - `icon` — optional named icon before the heading.
884
- **Properties / methods (JS)** — `open()`, `close()`.
885
- **Events** — `close` (clicking the backdrop, the close button, or pressing Escape).
886
- **Example**
887
- ```html
888
- <ody-modal id="m" heading="Confirm"><p>Are you sure?</p></ody-modal>
889
- <script>document.getElementById('m').open();</script>
890
- ```
891
-
892
- #### `<ody-popover>`
893
- **Use when** a click-toggled floating panel anchored to a trigger.
894
- **Attributes**
895
- - `for` — id of an external trigger element (overrides a slotted trigger).
896
- - `placement` — `top` | `bottom` | `left` | `right` (default `bottom`).
897
- **Properties / methods (JS)** — `isOpen` (read-only), `open()`, `close()`, `toggle()`.
898
- **Events** — `open`, `close` (no detail).
899
- **Slots / content** — the **trigger** is the child marked `[data-ody-popover-trigger]` (else the first element child unless `for` is set); the **content** is the child marked `[data-ody-popover-content]` (else the remaining children).
900
- **Example**
901
- ```html
902
- <ody-popover placement="bottom">
903
- <ody-button data-ody-popover-trigger variant="secondary">Menu</ody-button>
904
- <div data-ody-popover-content><ody-option>Edit</ody-option></div>
905
- </ody-popover>
906
- ```
907
-
908
- #### `<ody-tooltip>`
909
- **Use when** a hover/focus hint anchored to a trigger.
910
- **Attributes**
911
- - `text` — the tooltip text (alternative to slotted content).
912
- - `placement` — `top` | `bottom` | `left` | `right` (default `top`).
913
- - `for` — id of an external trigger element.
914
- **Properties / methods (JS)** — `isVisible` (read-only), `show()`, `hide()`.
915
- **Slots / content** — the **trigger** is `for` (external) or the first slotted child; tooltip text is `text` or extra slotted children.
916
- **Example**
917
- ```html
918
- <ody-tooltip text="Helpful hint" placement="top">
919
- <ody-button icon-only left-icon="info" aria-label="Info"></ody-button>
920
- </ody-tooltip>
921
- ```
922
-
923
- #### `<ody-panel>`
924
- **Use when** a side panel that slides in from the right. Body is the child content.
925
- **Attributes**
926
- - `open` — boolean; toggles the slide-in (mirrors `open()` / `close()`).
927
- - `heading` — optional panel title.
928
- - `full-height` — boolean; spans the full viewport height.
929
- **Properties / methods (JS)** — `open()`, `close()`.
930
- **Events** — `close` (clicking the overlay or the close button).
931
- **Example**
932
- ```html
933
- <ody-panel id="p" heading="Details"><p>Body…</p></ody-panel>
934
- <script>document.getElementById('p').open();</script>
935
- ```
936
-
937
- #### `<ody-toast-host>` + `toast()`
938
- **Use when** transient feedback. You normally never write `<ody-toast-host>`
939
- yourself — call the exported `toast()` function, which lazily creates and
940
- portals a singleton host.
941
- **`toast(message, options?)`** — `options: { variant?, title?, timeout? }`:
942
- - `variant` — `info` | `success` | `warning` | `danger` (default `info`).
943
- - `title` — optional bold title above the message.
944
- - `timeout` — auto-dismiss ms; `0` disables auto-dismiss (default `4000`).
945
- Returns `{ dismiss: () => void }` to remove the toast early.
946
- `ody-toast-host` has a `push(message, options)` method with the same signature.
947
- **Example**
948
- ```js
949
- import { toast } from '@peektravel/app-utilities/ui';
950
- const t = toast('Saved successfully', { variant: 'success', title: 'Done' });
951
- // t.dismiss();
952
- ```
953
-
954
- ---
955
-
956
- ### Data & selection
957
-
958
- #### `<ody-dropdown-single>`
959
- **Use when** single choice from a list (select-only combobox, WAI-ARIA pattern).
960
- **Attributes**
961
- - `value` — selected option value (reflected; also a JS property).
962
- - `options` — JSON array fallback (`[{"value":"a","label":"A","disabled":false}]`).
963
- - `placeholder` — text when nothing is selected.
964
- - `label` / `aria-label` — accessible name for the trigger.
965
- - `search-placeholder` — placeholder for the filter input when `searchable`.
966
- - `disabled` — boolean.
967
- - `searchable` — boolean; renders a filter input atop the list.
968
- **Properties (JS)** — `value: string`; `options: OdySelectOption[]` (`{ value, label, disabled? }`); `disabled: boolean` (read-only).
969
- **Events** — `change` → `{ value: string }` (user selection only; **not** on programmatic set).
970
- **Keyboard** — ArrowDown/Up (or Enter/Space) open + move; Home/End jump; Enter selects + closes; Escape closes; Tab closes; printable chars typeahead.
971
- **Example**
972
- ```html
973
- <ody-dropdown-single label="Fruit" placeholder="Pick a fruit" searchable
974
- options='[{"value":"apple","label":"Apple"},{"value":"cherry","label":"Cherry","disabled":true}]'></ody-dropdown-single>
975
- ```
976
-
977
- #### `<ody-dropdown-multi>`
978
- **Use when** multiple choices from a list (multi-select listbox combobox). The
979
- trigger shows selected options as removable chips.
980
- **Attributes** — same set as `ody-dropdown-single`; `value` is comma-delimited.
981
- **Properties (JS)** — `value: string[]` (get returns array, set takes array); `options: OdySelectOption[]`; `disabled` (read-only).
982
- **Events** — `change` → `{ value: string[] }` (user selection only; **not** on programmatic set). Toggling an option keeps the panel open.
983
- **Keyboard** — ArrowDown/Up move; Home/End jump; Space/Enter toggles; Escape/Tab close.
984
- **Example**
985
- ```html
986
- <ody-dropdown-multi label="Toppings" placeholder="Choose toppings" searchable value="cheese"
987
- options='[{"value":"cheese","label":"Cheese"},{"value":"olives","label":"Olives"}]'></ody-dropdown-multi>
988
- ```
989
-
990
- #### `<ody-datepicker>`
991
- **Use when** picking a date or date range. Dependency-free calendar (native
992
- `Date`/`Intl`). The displayed text is localized; the `value` attribute and
993
- `change` payload always stay machine-readable ISO `yyyy-mm-dd`.
994
- **Attributes**
995
- - `value` — `yyyy-mm-dd` (single), or `yyyy-mm-dd/yyyy-mm-dd` (range mode).
996
- - `min` / `max` — inclusive bounds `yyyy-mm-dd`.
997
- - `range` — boolean; enables start/end range selection.
998
- - `first-day-of-week` — `0`–`6` (0 = Sunday, default `0`).
999
- - `placeholder` — trigger text when no value.
1000
- - `presets` — JSON array of `{ label, value }` shortcut buttons (`value` is single or range form).
1001
- - `display-format` — `short` | `medium` | `long` | `full` (default `medium`); how the trigger label is shown (via `Intl.DateTimeFormat` for the element's `lang`).
1002
- **Properties (JS)**
1003
- - `isDateDisallowed?: (date: Date) => boolean` — disable arbitrary days.
1004
- - `formatDate?: (date: Date) => string` — full control of the trigger label (wins over `display-format`).
1005
- - `isOpen` (read-only); methods `openPopover()`, `closePopover()`.
1006
- **Events** — `change` on **user** selection only (never on programmatic set):
1007
- - single mode → `{ value }` (ISO).
1008
- - range mode → `{ value, start, end }` (all ISO; `value` is `start/end`).
1009
- **Example**
1010
- ```html
1011
- <ody-datepicker value="2026-06-15" display-format="full"></ody-datepicker>
1012
- <ody-datepicker range value="2026-06-10/2026-07-04" min="2026-04-01" max="2026-09-30"
1013
- presets='[{"label":"This month","value":"2026-06-01/2026-06-30"}]'></ody-datepicker>
1014
- ```
1015
- ```js
1016
- const dp = document.querySelector('ody-datepicker');
1017
- dp.isDateDisallowed = (d) => d.getDay() === 0; // disable Sundays
1018
- dp.formatDate = (d) => d.toLocaleDateString('en-GB'); // custom label
1019
- ```
1020
-
1021
- #### `<ody-table>`
1022
- **Use when** tabular data with sortable headers and optional checkbox selection.
1023
- Rich data is supplied through **JS properties** only.
1024
- **Properties (JS)**
1025
- - `columns: OdyTableColumn[]` — `{ key, label, sortable?, align?, render? }`. If omitted, columns auto-generate from the keys of the first data row. `align` is `left` | `center` | `right` (default `left`). `render(cellEl, row)` populates a custom cell element.
1026
- - `data: OdyTableRow[]` (alias `rows`) — row objects (`Record<string, unknown>`).
1027
- - `selected: OdyTableRow[]` — getter of currently selected rows; setter replaces the selection.
1028
- **Attributes**
1029
- - `selectable` — boolean; adds a leading checkbox column with header select-all (indeterminate when partial).
1030
- - `sortable` — boolean; globally enables sorting (per-column opt-in via `column.sortable`).
1031
- - `sort-key` — the column key currently sorted (reflected).
1032
- - `sort-direction` — `ascending` | `descending` (reflected).
1033
- - `sticky-header` — boolean; pins the header while the body scrolls.
1034
- **Events** (all bubble)
1035
- - `sort-change` → `{ key, direction }` — `direction` is `ascending` | `descending` | `null` (cleared).
1036
- - `selection-change` → `{ selected: OdyTableRow[] }`.
1037
- - `row-click` → `{ row, index }`.
1038
- **Example**
1039
- ```html
1040
- <ody-table id="t" selectable sortable sticky-header></ody-table>
1041
- ```
1042
- ```js
1043
- const t = document.getElementById('t');
1044
- t.columns = [
1045
- { key: 'name', label: 'Name', sortable: true },
1046
- { key: 'age', label: 'Age', sortable: true, align: 'right' },
1047
- ];
1048
- t.data = [{ name: 'Alice', age: 42 }, { name: 'Bob', age: 25 }];
1049
- t.addEventListener('sort-change', (e) => console.log(e.detail)); // { key, direction }
1050
- t.addEventListener('selection-change', (e) => console.log(e.detail.selected));
1051
- ```
1052
-
1053
- ---
1054
-
1055
- ## 6. Icons
1056
-
1057
- Two separate libraries.
1058
-
1059
- ### Themeable icons — `<ody-icon>` (inherit `currentColor`)
1060
- Use for UI glyphs that should follow text colour. See `<ody-icon>` above for the
1061
- element. Programmatic API (named exports):
1062
-
1063
- | Function | Purpose |
1064
- |---|---|
1065
- | `iconNames(): string[]` | All registered themeable names, sorted (discover at runtime). |
1066
- | `hasIcon(name): boolean` | Whether a name is known. |
1067
- | `iconSvg(name, className?): string` | Raw `<svg>` string; unknown names → empty `<svg>`. |
1068
- | `renderIconSvg(icon, className?): string` | Build `<svg>` from `OdyIconData` (`{ viewBox, body }`). |
1069
- | `registerIcon(name, innerSvg, viewBox?)` | Register/override an icon (`viewBox` default `'0 0 24 24'`). |
1070
-
1071
- ```js
1072
- import { registerIcon, iconNames } from '@peektravel/app-utilities/ui';
1073
- registerIcon('star', '<path d="M12 2l3 7h7l-5.5 4 2 7L12 16l-6.5 4 2-7L2 9h7z"/>');
1074
- console.log(iconNames()); // discover available names
1075
- ```
1076
-
1077
- ### Brand / fixed-colour icons — `<ody-brand-icon>` (keep baked-in colours)
1078
- Use for logos, illustrations, and status art that must keep their original
1079
- colours/gradients (do **not** follow `currentColor`).
1080
- `<ody-brand-icon>` **Attributes**: `name` (see `brandIconNames()`; unknown
1081
- renders nothing), `size` (same scale as `ody-icon`, default `base`; `free` sizes
1082
- from the surrounding box).
1083
-
1084
- | Function | Purpose |
1085
- |---|---|
1086
- | `brandIconNames(): string[]` | All brand icon names, sorted. |
1087
- | `hasBrandIcon(name): boolean` | Whether a name is known. |
1088
- | `brandIconSvg(name, className?): string` | Raw `<svg>` string; unknown → empty `<svg>`. |
1089
-
1090
- ```html
1091
- <ody-brand-icon name="empty-state-default" size="free"></ody-brand-icon>
1092
- ```
1093
-
1094
- **Which to use** — text-coloured glyph → themeable (`ody-icon` / `iconSvg`);
1095
- multi-colour logo or illustration → brand (`ody-brand-icon` / `brandIconSvg`).
1096
-
1097
- ---
1098
-
1099
- ## 7. Localization
1100
-
1101
- Built-in strings (aria-labels, default placeholders, status labels — **not**
1102
- your content) are localized.
1103
-
1104
- **Register bundles** once:
1105
- ```ts
1106
- import { registerTranslation } from '@peektravel/app-utilities/ui';
1107
- registerTranslation('es', { close: 'Cerrar', clear: 'Borrar', search: 'Buscar' });
1108
- ```
1109
- Re-registering merges into the existing bundle and re-renders mounted components.
1110
-
1111
- **Language resolution** — a component reads the nearest ancestor `lang`
1112
- attribute (so `<html lang="es">` localizes everything), falling back region →
1113
- base language → English. A `<html lang>` change re-renders automatically.
1114
-
1115
- **Per-instance override** — an attribute wins over the registry for that
1116
- instance, e.g. `close-label`, `clear-label`, `search-placeholder`,
1117
- `previous-month-label`, `next-month-label`, `open-submenu-label`,
1118
- `toggle-menu-label`:
1119
- ```html
1120
- <ody-panel close-label="Cerrar" heading="Detalles">…</ody-panel>
1121
- ```
1122
-
1123
- **Term keys** (`OdyTerms`): `close`, `clear`, `search`, `noOptions`,
1124
- `previousMonth`, `nextMonth`, `selectDate`, `openSubMenu`, `toggleMenu`,
1125
- `breadcrumb`, `radioGroup`, `toggleGroup`, `checkInInProgress`, `checkInNoShow`,
1126
- `checkInOverdue`, `checkInReserved`, `checkInReturned`, `checkInNone`.
1127
-
1128
- **Datepicker** — calendar weekday/month names and the trigger display are
1129
- `Intl`-localized for the resolved `lang` (controlled by `display-format` /
1130
- `formatDate`); the `value` and `change` payload stay ISO `yyyy-mm-dd`.
1131
-
1132
- ---
1133
-
1134
- ## 8. Theming (CSS custom properties)
1135
-
1136
- `tokens.css` defines the design tokens used by the components; override them in
1137
- your own CSS to retheme. Categories:
1138
-
1139
- - **Colours** — `--color-<name>-<shade>`. Semantic: `interaction`, `info`,
1140
- `success`, `warning`, `danger` (shades `100`–`400`). Neutrals:
1141
- `--color-neutral-0/50/100/200/250/300/350/400`. Accents: `blue`, `green`,
1142
- `teal`, `lime`, `purple`, `pink`, `rose`, `turquoise`, `yellow`, `orange`,
1143
- `orange-alt` (typically shades `200`/`300`). Plus `--color-box-shadow`.
1144
- - **Typography** — `--ody-font-family`, `--ody-line-height`,
1145
- `--ody-line-height-150`, and weights `--ody-font-weight-thin` …
1146
- `--ody-font-weight-black` (`extra-light`, `light`, `normal`, `medium`,
1147
- `semi-bold`, `bold`, `extra-bold`).
1148
- - **Layout / elevation** — `--layout-top-bar-height`,
1149
- `--layout-breadcrumb-height`, `--ody-shadow-base`, `--ody-overlay`.
1150
- - **Z-index** — `--ody-z-general`, `--ody-z-side-nav`, `--ody-z-top-bar`,
1151
- `--ody-z-modal`, `--ody-z-over-modal`, `--ody-z-tooltip`, `--ody-z-wormhole`.
1152
-
1153
- ```css
1154
- :root { --color-interaction-300: #0066ff; --ody-font-family: 'Inter', sans-serif; }
1155
- ```
1156
-
1157
- Many components also accept colours inline via attributes (`bar-color`,
1158
- `text-color`, `color`) — pass any CSS colour, including a token reference:
1159
- ```html
1160
- <ody-card bar-color="var(--color-success-300)">…</ody-card>
1161
- ```
1162
-
1163
- ---
1164
-
1165
- ## 9. Not included (and what to use instead)
1166
-
1167
- These Odyssey components were **not** ported:
1168
-
1169
- | Not ported | Use instead |
1170
- |---|---|
1171
- | `nested-multi-select` | `ody-dropdown-multi` for flat lists; for grouping, compose multiple dropdowns or build with `ody-checkbox-group` + `ody-collapsible-section`. |
1172
- | `location-autocomplete` | A native `<input>` wired to your own geocoding/autocomplete; render results with `ody-option` rows in an `ody-popover`. |
1173
- | `filter-menu` / `filter-menu-single` | `ody-popover` (trigger + panel) containing `ody-checkbox-group` (multi) or `ody-radio-button-group` / `ody-dropdown-single` (single). |
1174
- | `accordion-checkbox` | `ody-collapsible-section` (or `ody-accordion`) with an `ody-checkbox-group` inside. |
1175
- | `datepicker-with-presets` | `ody-datepicker` with the `presets` attribute (presets are built in). |