@octans/ui 1.0.0 → 1.3.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/README.md +45 -2
- package/dist/components/ActionList/ActionList.vue.d.ts +12 -4
- package/dist/components/ActionList/ActionListButton.vue.d.ts +13 -1
- package/dist/components/AppFrame/AppFrame.vue.d.ts +3 -3
- package/dist/components/Badge/Badge.vue.d.ts +3 -3
- package/dist/components/Banner/Banner.vue.d.ts +18 -0
- package/dist/components/Button/Button.vue.d.ts +66 -0
- package/dist/components/Calendar/types.d.ts +52 -0
- package/dist/components/CalendarHeatmap/CalendarHeatmap.vue.d.ts +1 -1
- package/dist/components/Card/CardSection.vue.d.ts +1 -1
- package/dist/components/ColorSelector/ColorSelector.vue.d.ts +1 -1
- package/dist/components/ColorSelector/types.d.ts +2 -2
- package/dist/components/DataTable/BulkActions.vue.d.ts +21 -3
- package/dist/components/DatePicker/types.d.ts +33 -0
- package/dist/components/FileInput/FileInput.vue.d.ts +104 -0
- package/dist/components/FileInput/ItemList.vue.d.ts +6 -0
- package/dist/components/FileInput/pageDrop.d.ts +23 -0
- package/dist/components/Filters/FilterActions.vue.d.ts +21 -3
- package/dist/components/Filters/FilterItem.vue.d.ts +31 -3
- package/dist/components/Filters/Filters.vue.d.ts +73 -9
- package/dist/components/Formatter/Formatter.vue.d.ts +18 -0
- package/dist/components/KeyboardKey/KeyboardKey.vue.d.ts +38 -0
- package/dist/components/KeyboardKey/index.d.ts +2 -0
- package/dist/components/Labelled/Labelled.vue.d.ts +18 -2
- package/dist/components/Modal/ModalSection.vue.d.ts +71 -0
- package/dist/components/Modal/index.d.ts +2 -1
- package/dist/components/Page/Page.vue.d.ts +34 -4
- package/dist/components/{Popper/Popper.vue.d.ts → Popover/Popover.vue.d.ts} +4 -4
- package/dist/components/Popover/index.d.ts +90 -0
- package/dist/components/{Popper → Popover}/types.d.ts +3 -3
- package/dist/components/RangeSlider/RangeSlider.vue.d.ts +27 -0
- package/dist/components/Select/Select.vue.d.ts +35 -3
- package/dist/components/SkeletonPage/SkeletonPage.vue.d.ts +19 -0
- package/dist/components/SkeletonPage/index.d.ts +2 -0
- package/dist/components/SkeletonPage/types.d.ts +26 -0
- package/dist/components/Tabs/Tabs.vue.d.ts +1 -14
- package/dist/components/Tabs/types.d.ts +11 -1
- package/dist/components/Tag/Tag.vue.d.ts +3 -3
- package/dist/components/TextField/types.d.ts +10 -0
- package/dist/components/TimePicker/types.d.ts +10 -0
- package/dist/components/ToggleSwitch/ToggleSwitch.vue.d.ts +8 -2
- package/dist/components/ToggleSwitch/types.d.ts +29 -0
- package/dist/components/Tooltip/Tooltip.vue.d.ts +2 -2
- package/dist/components/all.d.ts +3 -1
- package/dist/components/types.d.ts +5 -0
- package/dist/lib.d.ts +4 -1
- package/dist/reset.css +73 -0
- package/dist/style.css +1 -1
- package/dist/ui.js +4255 -3770
- package/dist/ui.umd.js +3 -3
- package/dist/utils/color.d.ts +11 -0
- package/dist/utils/contrastPairs.d.ts +39 -0
- package/dist/utils/createTheme.d.ts +29 -0
- package/dist/utils/customTheme.d.ts +1 -1
- package/dist/utils/date.d.ts +12 -1
- package/dist/utils/format.d.ts +8 -0
- package/dist/utils/positionPanel.d.ts +2 -2
- package/dist/utils/radix.d.ts +2 -2
- package/dist/utils/tokens.d.ts +1 -1
- package/package.json +29 -24
- package/skills/octans-ui/SKILL.md +303 -0
- package/dist/components/Popper/index.d.ts +0 -2
package/dist/utils/color.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/utils/date.d.ts
CHANGED
|
@@ -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 };
|
package/dist/utils/format.d.ts
CHANGED
|
@@ -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. `<
|
|
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 `<
|
|
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;
|
package/dist/utils/radix.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { PopoverPlacementType } from '../components/Popover';
|
|
2
2
|
import { PopoverContentProps } from 'reka-ui';
|
|
3
|
-
export declare function getRadixPopperPlacement(placement:
|
|
3
|
+
export declare function getRadixPopperPlacement(placement: PopoverPlacementType | ''): {
|
|
4
4
|
side: PopoverContentProps['side'];
|
|
5
5
|
align: PopoverContentProps['align'];
|
|
6
6
|
};
|
package/dist/utils/tokens.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Keep it in sync when adding a token. `tokens.test.ts` fails if the stylesheet
|
|
9
9
|
* and this registry drift apart, so it cannot rot silently.
|
|
10
10
|
*/
|
|
11
|
-
export type TokenKind = 'color' | 'shadow' | 'radius' | 'font';
|
|
11
|
+
export type TokenKind = 'color' | 'shadow' | 'radius' | 'font' | 'length';
|
|
12
12
|
export interface TokenDefinition {
|
|
13
13
|
/** Token name without the `--octans-` prefix. */
|
|
14
14
|
name: string;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octans/ui",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "A Vue 3 component library
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "A Vue 3 component library \u2014 accessible, themeable UI primitives and application shell components.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"vue",
|
|
7
7
|
"vue3",
|
|
@@ -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,12 +39,35 @@
|
|
|
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
|
},
|
|
44
46
|
"publishConfig": {
|
|
45
47
|
"access": "public"
|
|
46
48
|
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"dev": "pnpm storybook",
|
|
51
|
+
"storybook": "storybook dev -p 6006",
|
|
52
|
+
"build-storybook": "storybook build",
|
|
53
|
+
"build-site": "storybook build --output-dir site-dist/storybook && node scripts/build-site.mjs",
|
|
54
|
+
"type-check": "vue-tsc --noEmit -p tsconfig.app.json",
|
|
55
|
+
"test": "vitest run",
|
|
56
|
+
"test:watch": "vitest",
|
|
57
|
+
"test:coverage": "vitest run --coverage",
|
|
58
|
+
"check:contrast": "node scripts/check-contrast.mjs",
|
|
59
|
+
"lint": "eslint .",
|
|
60
|
+
"lint:fix": "eslint . --fix",
|
|
61
|
+
"format": "prettier --write src/ .storybook/ scripts/ *.ts *.js",
|
|
62
|
+
"format:check": "prettier --check src/ .storybook/ scripts/ *.ts *.js",
|
|
63
|
+
"icons:bundle": "node scripts/build-icon-bundle.mjs",
|
|
64
|
+
"check:icons": "node scripts/check-icon-bundle.mjs",
|
|
65
|
+
"build": "pnpm run build-lib",
|
|
66
|
+
"build-lib": "vp build --config vite.config-lib.ts && vp build --config vite.config-umd.ts",
|
|
67
|
+
"verify": "pnpm run lint && pnpm run type-check && pnpm run test && pnpm run check:contrast && pnpm run check:icons && pnpm run build-lib",
|
|
68
|
+
"prepublishOnly": "pnpm run verify",
|
|
69
|
+
"tag": "package-version-git-tag --push"
|
|
70
|
+
},
|
|
47
71
|
"dependencies": {
|
|
48
72
|
"@floating-ui/dom": "^1.8.0",
|
|
49
73
|
"@iconify/vue": "^5.0.1",
|
|
@@ -97,24 +121,5 @@
|
|
|
97
121
|
"engines": {
|
|
98
122
|
"node": ">=24"
|
|
99
123
|
},
|
|
100
|
-
"
|
|
101
|
-
|
|
102
|
-
"storybook": "storybook dev -p 6006",
|
|
103
|
-
"build-storybook": "storybook build",
|
|
104
|
-
"type-check": "vue-tsc --noEmit -p tsconfig.app.json",
|
|
105
|
-
"test": "vitest run",
|
|
106
|
-
"test:watch": "vitest",
|
|
107
|
-
"test:coverage": "vitest run --coverage",
|
|
108
|
-
"check:contrast": "node scripts/check-contrast.mjs",
|
|
109
|
-
"lint": "eslint .",
|
|
110
|
-
"lint:fix": "eslint . --fix",
|
|
111
|
-
"format": "prettier --write src/ .storybook/ scripts/ *.ts *.js",
|
|
112
|
-
"format:check": "prettier --check src/ .storybook/ scripts/ *.ts *.js",
|
|
113
|
-
"icons:bundle": "node scripts/build-icon-bundle.mjs",
|
|
114
|
-
"check:icons": "node scripts/check-icon-bundle.mjs",
|
|
115
|
-
"build": "pnpm run build-lib",
|
|
116
|
-
"build-lib": "vp build --config vite.config-lib.ts && vp build --config vite.config-umd.ts",
|
|
117
|
-
"verify": "pnpm run lint && pnpm run type-check && pnpm run test && pnpm run check:contrast && pnpm run check:icons && pnpm run build-lib",
|
|
118
|
-
"tag": "package-version-git-tag --push"
|
|
119
|
-
}
|
|
120
|
-
}
|
|
124
|
+
"packageManager": "pnpm@11.6.0"
|
|
125
|
+
}
|
|
@@ -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.
|