@octans/ui 1.0.0 → 1.2.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.
Files changed (58) hide show
  1. package/README.md +45 -2
  2. package/dist/components/ActionList/ActionList.vue.d.ts +4 -4
  3. package/dist/components/AppFrame/AppFrame.vue.d.ts +3 -3
  4. package/dist/components/Badge/Badge.vue.d.ts +3 -3
  5. package/dist/components/Banner/Banner.vue.d.ts +18 -0
  6. package/dist/components/Button/Button.vue.d.ts +66 -0
  7. package/dist/components/Calendar/types.d.ts +52 -0
  8. package/dist/components/CalendarHeatmap/CalendarHeatmap.vue.d.ts +1 -1
  9. package/dist/components/Card/CardSection.vue.d.ts +1 -1
  10. package/dist/components/ColorSelector/ColorSelector.vue.d.ts +1 -1
  11. package/dist/components/ColorSelector/types.d.ts +2 -2
  12. package/dist/components/DataTable/BulkActions.vue.d.ts +21 -3
  13. package/dist/components/DatePicker/types.d.ts +33 -0
  14. package/dist/components/FileInput/FileInput.vue.d.ts +104 -0
  15. package/dist/components/FileInput/ItemList.vue.d.ts +6 -0
  16. package/dist/components/FileInput/pageDrop.d.ts +23 -0
  17. package/dist/components/Filters/FilterActions.vue.d.ts +21 -3
  18. package/dist/components/Filters/FilterItem.vue.d.ts +30 -3
  19. package/dist/components/Filters/Filters.vue.d.ts +72 -9
  20. package/dist/components/Formatter/Formatter.vue.d.ts +18 -0
  21. package/dist/components/KeyboardKey/KeyboardKey.vue.d.ts +38 -0
  22. package/dist/components/KeyboardKey/index.d.ts +2 -0
  23. package/dist/components/Labelled/Labelled.vue.d.ts +18 -2
  24. package/dist/components/Modal/ModalSection.vue.d.ts +71 -0
  25. package/dist/components/Modal/index.d.ts +2 -1
  26. package/dist/components/Page/Page.vue.d.ts +34 -4
  27. package/dist/components/{Popper/Popper.vue.d.ts → Popover/Popover.vue.d.ts} +4 -4
  28. package/dist/components/Popover/index.d.ts +90 -0
  29. package/dist/components/{Popper → Popover}/types.d.ts +3 -3
  30. package/dist/components/RangeSlider/RangeSlider.vue.d.ts +27 -0
  31. package/dist/components/Select/Select.vue.d.ts +30 -3
  32. package/dist/components/SkeletonPage/SkeletonPage.vue.d.ts +19 -0
  33. package/dist/components/SkeletonPage/index.d.ts +2 -0
  34. package/dist/components/SkeletonPage/types.d.ts +26 -0
  35. package/dist/components/Tag/Tag.vue.d.ts +3 -3
  36. package/dist/components/TextField/types.d.ts +10 -0
  37. package/dist/components/TimePicker/types.d.ts +10 -0
  38. package/dist/components/ToggleSwitch/ToggleSwitch.vue.d.ts +8 -2
  39. package/dist/components/ToggleSwitch/types.d.ts +29 -0
  40. package/dist/components/Tooltip/Tooltip.vue.d.ts +2 -2
  41. package/dist/components/all.d.ts +3 -1
  42. package/dist/components/types.d.ts +5 -0
  43. package/dist/lib.d.ts +4 -1
  44. package/dist/reset.css +73 -0
  45. package/dist/style.css +1 -1
  46. package/dist/ui.js +4156 -3721
  47. package/dist/ui.umd.js +3 -3
  48. package/dist/utils/color.d.ts +11 -0
  49. package/dist/utils/contrastPairs.d.ts +39 -0
  50. package/dist/utils/createTheme.d.ts +29 -0
  51. package/dist/utils/customTheme.d.ts +1 -1
  52. package/dist/utils/date.d.ts +12 -1
  53. package/dist/utils/format.d.ts +8 -0
  54. package/dist/utils/positionPanel.d.ts +2 -2
  55. package/dist/utils/radix.d.ts +2 -2
  56. package/package.json +5 -2
  57. package/skills/octans-ui/SKILL.md +303 -0
  58. package/dist/components/Popper/index.d.ts +0 -2
@@ -106,7 +106,18 @@ export declare function relativeLuminance({ r, g, b }: Rgba): number;
106
106
  /**
107
107
  * Whether black text reads better than white on this colour. Used to keep a
108
108
  * swatch's own label legible whatever colour it is showing.
109
+ *
110
+ * A lightness rule of thumb, not a measurement — use `contrastRatio` when the
111
+ * answer has to hold up as a WCAG number rather than just look right.
109
112
  */
110
113
  export declare function prefersDarkText(c: Rgba): boolean;
114
+ /**
115
+ * WCAG contrast ratio between two colours, 1–21. Symmetric: the lighter of the
116
+ * pair goes on top whichever order they arrive in.
117
+ *
118
+ * Alpha is ignored, as in `relativeLuminance` — `flatten` a translucent colour
119
+ * over its backdrop first if you want the composited answer.
120
+ */
121
+ export declare function contrastRatio(a: Rgba, b: Rgba): number;
111
122
  /** Composites a colour over an opaque backdrop, so its alpha can be seen. */
112
123
  export declare function flatten(c: Rgba, backdrop: Rgba): Rgba;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The token pairs whose contrast actually matters, with the ratio each owes.
3
+ *
4
+ * One list, two consumers: `scripts/check-contrast.mjs` audits the built-in
5
+ * palette with it at build time, and the Theme Builder measures a theme being
6
+ * edited against it live. Keeping them on the same table is the point — a pair
7
+ * the script guards but the Builder never shows is a rule a custom theme can
8
+ * break in silence.
9
+ *
10
+ * The script is plain `.mjs` and imports this `.ts` directly: Node strips the
11
+ * types on the way in (the engines field already requires Node 24, where that
12
+ * is on by default). So keep this file to types the stripper can simply
13
+ * delete — no enums, no namespaces, no `const enum`.
14
+ *
15
+ * On the thresholds:
16
+ *
17
+ * 4.5 body text (WCAG 1.4.3).
18
+ * 3.0 UI components and graphical objects needed to understand the
19
+ * interface (WCAG 1.4.11) — input outlines, focus rings.
20
+ *
21
+ * A plain divider is decorative and carries NO WCAG requirement, so `border`
22
+ * gets a loose sanity floor instead: enough to be visible, not so much that
23
+ * every table row grows a heavy rule. Don't "fix" a border failure by
24
+ * darkening it past what the design wants — check which of the two cases it
25
+ * actually is first.
26
+ */
27
+ export interface ContrastPair {
28
+ /** Token read as the foreground. */
29
+ fg: string;
30
+ /** Token it sits on. */
31
+ bg: string;
32
+ /** What the pair is, for messages. */
33
+ label: string;
34
+ /** The ratio it has to clear. */
35
+ min: number;
36
+ }
37
+ export declare const CONTRAST_PAIRS: ContrastPair[];
38
+ /** Every pair a given token is the foreground of. */
39
+ export declare function pairsForToken(name: string): ContrastPair[];
@@ -3,6 +3,19 @@ import { ThemeType } from './theme';
3
3
  /** The ramp prefixes a seed can be given for, in display order. */
4
4
  export declare const THEME_SEED_ROLES: readonly ["neutral", "primary", "secondary", "tertiary", "info", "success", "warning", "error"];
5
5
  export type ThemeSeedRoleType = (typeof THEME_SEED_ROLES)[number];
6
+ /** The roles that have a fill, and therefore a `text-on-<role>` label. */
7
+ export declare const THEME_FILL_ROLES: ThemeFillRoleType[];
8
+ export type ThemeFillRoleType = Exclude<ThemeSeedRoleType, 'neutral'>;
9
+ /**
10
+ * The ramp step each role's semantic FILL token points at (see tokens.scss).
11
+ * `createTheme` measures the generated colour at this step to pick a legible
12
+ * `text-on-<role>`.
13
+ *
14
+ * Exported because the Theme Builder needs the same mapping: a fill edited by
15
+ * hand has to re-derive its label the way a seeded one does, and it can only
16
+ * know which step to read from here.
17
+ */
18
+ export declare const ROLE_FILL_STEP: Record<ThemeFillRoleType, number>;
6
19
  export type ThemeRadiusPresetType = 'none' | 'small' | 'medium' | 'large';
7
20
  export interface CreateThemeOptions {
8
21
  /** Human-readable name, shown in the Theme Builder. */
@@ -50,3 +63,19 @@ export interface CreateThemeOptions {
50
63
  * palette only; check generated themes with the Theme Builder's preview.
51
64
  */
52
65
  export declare function createTheme(options: CreateThemeOptions): CustomTheme;
66
+ /** What `darkText` falls back to when a theme seeds no neutral of its own. */
67
+ export declare const DEFAULT_DARK_TEXT = "#11151a";
68
+ /**
69
+ * The label a fill wants: white if it clears WCAG's 4.5:1 there — the house
70
+ * style for labels on fills — otherwise whichever of white and the theme's
71
+ * near-black measures stronger. Measured, not guessed by lightness: on a
72
+ * mid-tone fill the two can sit either side of the threshold by a whisker.
73
+ *
74
+ * Returns null when the fill cannot be parsed, so a caller can leave whatever
75
+ * is already there alone rather than write a guess over it.
76
+ *
77
+ * Exported for the Theme Builder, which re-derives labels as fills are edited
78
+ * by hand. Both paths must agree — a fill that arrives via a seed and the same
79
+ * fill typed into the token list should end up with the same label.
80
+ */
81
+ export declare function labelForFill(fill: string, darkText?: string): string | null;
@@ -57,7 +57,7 @@ export declare function restoreCustomTheme(): CustomTheme | undefined;
57
57
  * including inherited and overridden values. Used to seed the editor with real
58
58
  * starting values rather than blanks.
59
59
  */
60
- export declare function getResolvedTokenValue(name: string): string;
60
+ export declare function getResolvedTokenValue(name: string, from?: Element): string;
61
61
  export declare function exportCustomTheme(theme: CustomTheme): string;
62
62
  export declare class ThemeImportError extends Error {
63
63
  }
@@ -1,4 +1,15 @@
1
1
  import { default as dayjs } from 'dayjs';
2
2
  declare function setLocale(locale: string): void;
3
+ declare function setTimezone(timezone?: string | null): void;
4
+ declare function getTimezone(): string | undefined;
5
+ /**
6
+ * Reads a value in the display time zone, or the viewer's clock when there
7
+ * isn't one. `timezone` overrides the global for this one call.
8
+ *
9
+ * A zone the runtime rejects falls back rather than throwing: a date that
10
+ * renders in the wrong zone is a bug, and a component that throws while
11
+ * rendering is an outage.
12
+ */
13
+ declare function inTimezone(value?: dayjs.ConfigType, timezone?: string): dayjs.Dayjs;
3
14
  declare const mysqlFormat = "YYYY-MM-DD HH:mm:ss";
4
- export { dayjs, setLocale, mysqlFormat };
15
+ export { dayjs, setLocale, setTimezone, getTimezone, inTimezone, mysqlFormat };
@@ -1,6 +1,14 @@
1
1
  export interface FormatContextInterface {
2
2
  locale?: string;
3
3
  currency?: string;
4
+ /**
5
+ * IANA time zone the date formatters render in (`'America/Los_Angeles'`).
6
+ *
7
+ * Defaults to the library-wide display zone set with `setTimezone`, and
8
+ * failing that the viewer's own clock — which is what every date here used
9
+ * to be, unconditionally.
10
+ */
11
+ timezone?: string;
4
12
  }
5
13
  export declare const emptyValuePlaceholder = "\u2014";
6
14
  export declare const formatters: Record<string, any>;
@@ -23,10 +23,10 @@ export interface PanelPosition {
23
23
  * Positions a floating panel against a reference element and keeps it there
24
24
  * while the page scrolls or either element resizes.
25
25
  *
26
- * This is the only imperative positioning in the library. `<Popper>` — and so
26
+ * This is the only imperative positioning in the library. `<Popover>` — and so
27
27
  * `ActionList`, `DatePicker`, `TimePicker` and `ColorSelector` — goes through
28
28
  * reka-ui, which does its own on the same floating-ui underneath. This exists
29
- * for `Select`, whose dropdown is hand-rolled rather than a `<Popper>`.
29
+ * for `Select`, whose dropdown is hand-rolled rather than a `<Popover>`.
30
30
  */
31
31
  export declare function positionPanel(reference: HTMLElement, panel: HTMLElement, options?: PositionPanelOptions): PanelPosition;
32
32
  export default positionPanel;
@@ -1,6 +1,6 @@
1
- import { PopperPlacementType } from '../components/Popper';
1
+ import { PopoverPlacementType } from '../components/Popover';
2
2
  import { PopoverContentProps } from 'reka-ui';
3
- export declare function getRadixPopperPlacement(placement: PopperPlacementType | ''): {
3
+ export declare function getRadixPopperPlacement(placement: PopoverPlacementType | ''): {
4
4
  side: PopoverContentProps['side'];
5
5
  align: PopoverContentProps['align'];
6
6
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@octans/ui",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "A Vue 3 component library — accessible, themeable UI primitives and application shell components.",
5
5
  "keywords": [
6
6
  "vue",
@@ -29,7 +29,8 @@
29
29
  },
30
30
  "files": [
31
31
  "dist",
32
- "bin"
32
+ "bin",
33
+ "skills"
33
34
  ],
34
35
  "exports": {
35
36
  ".": {
@@ -38,6 +39,7 @@
38
39
  "default": "./dist/ui.js"
39
40
  },
40
41
  "./style.css": "./dist/style.css",
42
+ "./reset.css": "./dist/reset.css",
41
43
  "./dist/*": "./dist/*",
42
44
  "./package.json": "./package.json"
43
45
  },
@@ -101,6 +103,7 @@
101
103
  "dev": "pnpm storybook",
102
104
  "storybook": "storybook dev -p 6006",
103
105
  "build-storybook": "storybook build",
106
+ "build-site": "storybook build --output-dir site-dist/storybook && node scripts/build-site.mjs",
104
107
  "type-check": "vue-tsc --noEmit -p tsconfig.app.json",
105
108
  "test": "vitest run",
106
109
  "test:watch": "vitest",
@@ -0,0 +1,303 @@
1
+ ---
2
+ name: octans-ui
3
+ description: Build interfaces with the @octans/ui Vue 3 component library — setup, the design-token system, theming, the $ui global helpers, and the inheritance traps that make components look broken in dark mode. Use when writing or reviewing Vue code that imports from @octans/ui, when styling against --octans-* tokens, or when building a custom theme.
4
+ license: MIT
5
+ compatibility: Any agent supporting the Agent Skills standard. Requires a Vue 3.5+ project with @octans/ui installed. Optional peer, vue-router, only for MaybeRouterLink and Navigation.
6
+ ---
7
+
8
+ # Octans UI
9
+
10
+ An opinionated Vue 3 component library: ~77 accessible, themeable components
11
+ plus the application shell pieces (nav, frame, sheets, toasts). Docs live at
12
+ <https://octans.dev>, source at <https://github.com/tarwin/octans-ui>.
13
+
14
+ ## Setup
15
+
16
+ Two things are required — the stylesheet, and a `UiProvider` at the app root.
17
+ A full-page app needs a third: the reset.
18
+
19
+ ```ts
20
+ // main.ts
21
+ import { createApp } from 'vue'
22
+ import UI from '@octans/ui'
23
+ import '@octans/ui/style.css'
24
+ // Only when Octans owns the whole page — see below.
25
+ import '@octans/ui/reset.css'
26
+ import App from './App.vue'
27
+
28
+ createApp(App).use(UI).mount('#app')
29
+ ```
30
+
31
+ ```vue
32
+ <!-- App.vue -->
33
+ <script setup lang="ts">
34
+ import { UiProvider } from '@octans/ui'
35
+ </script>
36
+
37
+ <template>
38
+ <UiProvider>
39
+ <!-- your app -->
40
+ </UiProvider>
41
+ </template>
42
+ ```
43
+
44
+ `UiProvider` supplies the context tooltips, popovers and the global overlays
45
+ rely on. Without it, anything using `Tooltip` throws on mount.
46
+
47
+ `.use(UI)` is optional — it registers every component globally, which pulls
48
+ them all into the bundle. Prefer named imports and skip the plugin:
49
+
50
+ ```vue
51
+ <script setup lang="ts">
52
+ import { Button, Card, CardSection } from '@octans/ui'
53
+ </script>
54
+ ```
55
+
56
+ The stylesheet and `UiProvider` are needed either way. The plugin also installs
57
+ the `$ui` global; without it, import `toast`, `confirmModal`, `promptModal`,
58
+ `loader`, `saveBar` and `progress` by name instead.
59
+
60
+ ### `reset.css` — when Octans owns the page
61
+
62
+ `style.css` deliberately resets nothing outside `.UIElement`. That keeps the
63
+ library safe to drop into a page it doesn't own, and it is the wrong default
64
+ for an app where Octans IS the UI: everything the app renders itself — the
65
+ shell, the layout wrappers, the nav — falls back to browser defaults. Serif
66
+ text, the user agent's 8px body margin, `content-box` sizing, and a body with
67
+ no height that grows past the viewport into a second scrollbar.
68
+
69
+ `@octans/ui/reset.css` is that missing layer, and nothing more. Import it after
70
+ `style.css` — it reads the tokens. **Symptoms it fixes**: app-authored markup
71
+ in a serif face, a gap around the whole shell, a second scrollbar, and
72
+ app-authored text at 16px beside 14px Octans components.
73
+
74
+ Skip it when Octans is one widget on someone else's page.
75
+
76
+ ## Design tokens
77
+
78
+ Three tiers, described in full in `docs/color-system.md`:
79
+
80
+ 1. **Seeds** — `createTheme({ primary: '#…' })` generates everything below.
81
+ 2. **Primitives** — `--octans-primary-500`, `--octans-neutral-800`, the raw
82
+ ramps. **Components must never use these directly.**
83
+ 3. **Semantic** — `--octans-surface`, `--octans-text-subdued`,
84
+ `--octans-border`. This is the public, themeable API.
85
+
86
+ **When styling anything, reach for a semantic token.** Every non-neutral role
87
+ (primary, secondary, tertiary, info, success, warning, error) exposes the same
88
+ four:
89
+
90
+ | Token | Meaning |
91
+ | ------------------------- | --------------------------------------------- |
92
+ | `--octans-<role>` | the solid fill |
93
+ | `--octans-text-on-<role>` | content sitting on that fill |
94
+ | `--octans-<role>-surface` | soft tinted background |
95
+ | `--octans-text-<role>` | readable text in that hue, on a plain surface |
96
+
97
+ Never hard-code a colour. A literal like `rgba(0, 0, 0, 0.54)` looks fine in
98
+ light mode and is invisible in dark.
99
+
100
+ Two rules that are easy to get wrong:
101
+
102
+ - Use `--octans-text-<role>` for coloured **text**, and `--octans-<role>` only
103
+ as a **fill**. Fills are chosen to sit behind white text, so they are too
104
+ dark to read as text on a dark surface.
105
+ - A colour must never be defined only inside a media query or a
106
+ `[data-octans-theme]` block, or it disappears in the other theme.
107
+
108
+ Fills do **not** flip between light and dark — only surfaces, borders and the
109
+ `text-<role>` tokens do. That is why `--octans-text-on-<role>` is defined once.
110
+
111
+ ## Theming
112
+
113
+ ```ts
114
+ import { setTheme, persistTheme } from '@octans/ui'
115
+
116
+ setTheme('dark') // 'light' | 'dark' | 'system'
117
+ persistTheme() // remember the choice in localStorage
118
+ ```
119
+
120
+ To rebrand from a seed colour — every ramp and semantic token follows, dark
121
+ mode included:
122
+
123
+ ```ts
124
+ import { createTheme, applyCustomTheme } from '@octans/ui'
125
+
126
+ applyCustomTheme(createTheme({ name: 'Violet', primary: '#7b5cff' }))
127
+ ```
128
+
129
+ `createTheme` measures each generated fill and picks a legible
130
+ `text-on-<role>` automatically. If you set a fill **by hand**, set its label to
131
+ match — white on a dark fill, near-black on a pale one.
132
+
133
+ Or override tokens in CSS, which survives library updates better than
134
+ rewriting semantic tokens one by one:
135
+
136
+ ```css
137
+ :root {
138
+ --octans-primary-500: #7b5cff;
139
+ --octans-primary-600: #6a48f0;
140
+ }
141
+ ```
142
+
143
+ Three knobs that are not colours:
144
+
145
+ ```css
146
+ :root {
147
+ /* The font for everything, components and app markup alike. */
148
+ --octans-font: 'Inter', system-ui, sans-serif;
149
+ --octans-font-mono: 'JetBrains Mono', monospace;
150
+
151
+ /* Every `Icon`'s vertical alignment. `0` is the default. Set it to
152
+ `-0.125em` for icons that sit inline with running text. */
153
+ --octans-icon-valign: -0.125em;
154
+ }
155
+ ```
156
+
157
+ `--octans-font` is the one to set. `--ui-font` still exists and still works —
158
+ `--octans-font` resolves through it — but every token in the public API is
159
+ spelled `--octans-*`, and that is the name the baseline reads.
160
+
161
+ ## Traps
162
+
163
+ These are the mistakes that actually happen. Most produce black text on a dark
164
+ surface, which passes review in light mode and fails in dark.
165
+
166
+ **Form controls inherit neither `color` nor `font`.** A `<button>`, `<input>`,
167
+ `<select>` or `<textarea>` starts from the user agent's `buttontext` and Arial,
168
+ whatever the surrounding theme says. Any control you style needs both stated:
169
+
170
+ ```scss
171
+ .MyButton {
172
+ font: inherit; // put it first, so a later font-size still wins
173
+ color: var(--octans-text);
174
+
175
+ // An authored `color` also opts out of the UA's dimming of a disabled
176
+ // control, so a control that can be disabled needs this too.
177
+ &:disabled {
178
+ color: var(--octans-text-disabled);
179
+ }
180
+ }
181
+ ```
182
+
183
+ **Teleported content inherits nothing.** Anything portalled to `<body>` — a
184
+ popover, a modal, a sheet — lands outside the tree it was written in and picks
185
+ up the host page's defaults. Put `UIElement` on the root of teleported content;
186
+ that class carries the text colour, font and box-sizing baseline.
187
+
188
+ **`color-scheme` covers what CSS cannot reach.** The native `<option>` list a
189
+ `<select>` opens, scrollbars, and the internals of `input[type=date|time]` take
190
+ their palette from `color-scheme`, which the token layer sets per theme. Don't
191
+ fight it with per-control CSS.
192
+
193
+ **Icons are SVG via Iconify**, drawn in `currentColor`, so they follow whatever
194
+ `color` resolves to — which is why a black icon usually means a control that
195
+ never got told its colour.
196
+
197
+ **Reaching through a wrapper a component rendered itself.** Neither `vue-tsc`
198
+ nor the build catches this: the CSS just silently stops applying. Some
199
+ components put an element of their own around your slot content — `Tooltip`
200
+ wraps its trigger in a `<span>`, `Popover` (and so `ActionList`, `DatePicker`,
201
+ `TimePicker`, `ColorSelector`) wraps everything in a `<div>`. That element
202
+ belongs to the LIBRARY's template, so Vue never stamps your component's
203
+ scoped-style attribute on it, and any selector that reached through it stops
204
+ matching:
205
+
206
+ ```scss
207
+ /* in your component, <style scoped> */
208
+ .Nav {
209
+ & > span:first-of-type {
210
+ margin-right: 16px;
211
+ } /* never matches once that span is Tooltip's */
212
+ &.sDisabled > span {
213
+ display: none;
214
+ } /* same — the control stays visible */
215
+ }
216
+ ```
217
+
218
+ Style an element you render yourself. Put `gap` on the flex container rather
219
+ than margins on its children, and reach for `:deep()` only deliberately.
220
+
221
+ ## Global helpers
222
+
223
+ Available as `$ui.*` in templates when the plugin is installed, or as named
224
+ imports:
225
+
226
+ ```ts
227
+ import { toast, confirmModal, promptModal, loader, saveBar } from '@octans/ui'
228
+
229
+ toast.success('Saved')
230
+ if (await confirmModal({ title: 'Delete this?' })) {
231
+ /* … */
232
+ }
233
+ loader.show()
234
+ ```
235
+
236
+ ## Internationalisation
237
+
238
+ Components translate through `$t`. Register dictionaries at install time or
239
+ later:
240
+
241
+ ```ts
242
+ import { addTranslations, setTranslationLocale } from '@octans/ui'
243
+ ```
244
+
245
+ Dates have two knobs, both set once at start-up and both global:
246
+
247
+ ```ts
248
+ import { setLocale, setTimezone } from '@octans/ui'
249
+
250
+ setLocale('fr') // formats, month names, and the day the week starts on
251
+ setTimezone('America/Los_Angeles') // what "now" is, and what zone dates render in
252
+ ```
253
+
254
+ `setTimezone` is for an app that must show ONE zone to everybody — an
255
+ operations console pinned to head-office time — rather than each viewer's own
256
+ clock, which is the default. It affects display only; it never reinterprets a
257
+ stored value. `Formatter`, `Calendar` and `DatePicker` each take a `timezone`
258
+ prop to override it for one instance.
259
+
260
+ ## Components
261
+
262
+ Layout and shell: `AppFrame` `Page` `Layout` `Card` `Stack` `Divider`
263
+ `FormLayout` `ScrollPane` `Splitter` `Sticky` `TextContainer` `FooterHelp`
264
+ `PageActions`
265
+
266
+ Navigation: `GlobalNav` `Navigation` `Tabs` `Link` `Pagination`
267
+ `MaybeRouterLink` `UnstyledLink`
268
+
269
+ Actions: `Button` `ButtonGroup` `ActionList`
270
+
271
+ Forms: `TextField` `Select` `Checkbox` `RadioButton` `ChoiceList` `Choice`
272
+ `ToggleSwitch` `SegmentedControl` `RangeSlider` `Rating` `OtpInput` `FileInput`
273
+ `DatePicker` `TimePicker` `TimezonePicker` `Calendar` `ColorPicker`
274
+ `ColorSelector` `GradientPicker` `Labelled` `InlineError` `NavField` `Filters`
275
+ `PreventAutoComplete`
276
+
277
+ Data display: `DataTable` `ResourceList` `List` `Badge` `Tag` `Thumbnail`
278
+ `StatGroup` `CalendarHeatmap` `Formatter` `SyntaxHighlighter`
279
+
280
+ Typography: `Heading` `TextStyle` `Caption` `LineClamper` `KeyboardKey`
281
+
282
+ Feedback: `Banner` `Spinner` `ProgressBar` `LoadingBar` `LoaderOverlay`
283
+ `SkeletonBodyText` `SkeletonCard` `SkeletonDisplayText` `SkeletonPage`
284
+ `SaveBar` `ToastManager`
285
+
286
+ Overlays: `Modal` `ModalSection` `Sheet` `Popover` `Tooltip`
287
+
288
+ `Popover` was called `Popper` until 1.2. The old name still works as a
289
+ deprecated alias — it wraps reka-ui's Popover primitives and never had
290
+ anything to do with popper.js.
291
+
292
+ Utilities: `Icon` `EventDelegator` `MaybeMountingPortal` `UiProvider`
293
+ `SaveBarController`
294
+
295
+ ## Looking things up
296
+
297
+ - Props and live examples: <https://octans.dev/storybook/> — every component
298
+ has its own page, and the Kitchen Sink shows one example of each.
299
+ - Plain-markdown docs an agent can fetch: <https://octans.dev/llms.txt>
300
+ - Token reference: `docs/color-system.md` in the repo.
301
+
302
+ Storybook is a single-page app, so fetching a `?path=…` URL returns the shell
303
+ rather than the page content. Use `llms.txt` when you need the text.
@@ -1,2 +0,0 @@
1
- export { default as Popper } from './Popper.vue';
2
- export * from './types';