@jirawatpyk/aura-tokens 5.3.0 → 5.5.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 +1 -1
- package/aura-tailwind.css +1 -1
- package/aura-tailwind.prefixed.css +1 -1
- package/aura.css +1 -1
- package/components/aura.bundle.js +11 -2
- package/components/index.d.ts +43 -16
- package/eslint-plugin-aura.js +19 -4
- package/package.json +6 -6
- package/scripts/a11y-check.js +26 -5
- package/scripts/lint-tokens.js +8 -3
- package/tailwind.tokens.cjs +1 -1
- package/tokens.json +1 -1
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ Enterprise Standard, Human Creative. Tokens, Tailwind preset, Figma variables a
|
|
|
15
15
|
| `figma-variables.csv` | 204 variables: all colours in both modes (aliases kept), spacing, radius and sizes. **Generated.** |
|
|
16
16
|
| `components/` | `aura.bundle.js` (sets `window.Aura`), `aura.components.css`, `index.d.ts`. |
|
|
17
17
|
| `eslint-plugin-aura.js` | `aura/no-hardcoded-color`: hex, colour functions (rgb, hsl, oklch, color-mix…), named colours in style values and Tailwind palette / `bg-[#…]` classes. ESLint 9: `import aura from '@jirawatpyk/aura-tokens/eslint-plugin'; export default [aura.configs.recommended];` (5.2). |
|
|
18
|
-
| `scripts/build-tokens.
|
|
18
|
+
| `scripts/build-tokens.mts` | Rebuilds the generated files from `tokens.json`. |
|
|
19
19
|
| `scripts/a11y-check.js` | Checks 210 pairs in both themes: text on its grounds (WCAG AA), control edges and state marks at 3:1, neighbouring chart and status fills a lightness step apart. |
|
|
20
20
|
| `scripts/lint-tokens.js` | Fails on hard-coded colours (a file that must hold them says `aura-lint: allow-colours` with a reason). |
|
|
21
21
|
|
package/aura-tailwind.css
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* AURA v5.
|
|
1
|
+
/* AURA v5.5.0 — Tailwind v4 theme. GENERATED by scripts/build-tokens.mts from tokens.json. Do not edit by hand.
|
|
2
2
|
* @import "tailwindcss";
|
|
3
3
|
* @import "@jirawatpyk/aura-tokens/tailwind.css";
|
|
4
4
|
* Gives bg-bg-surface, text-fg-primary, border-border-strong, p-aura-6, rounded-aura-xl, shadow-aura-overlay,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* AURA v5.
|
|
1
|
+
/* AURA v5.5.0 — Tailwind v4 theme with aura- prefixed names. GENERATED by scripts/build-tokens.mts. Do not edit by hand.
|
|
2
2
|
* For apps that already have a Tailwind theme (shadcn): nothing here redefines an existing name.
|
|
3
3
|
* @layer aura-tokens, theme, base, aura, components, utilities;
|
|
4
4
|
* @import "tailwindcss";
|
package/aura.css
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* AURA v5.
|
|
1
|
+
/* AURA v5.5.0 — GENERATED by scripts/build-tokens.mts from tokens.json. Do not edit by hand. */
|
|
2
2
|
/* Fonts: load aura-fonts.css (or the <link> tags in README) separately, so a blocked Google Fonts never breaks this file. */
|
|
3
3
|
|
|
4
4
|
:root, [data-theme="light"] {
|
|
@@ -68,7 +68,7 @@ window.Aura = (() => {
|
|
|
68
68
|
Combobox: () => Combobox,
|
|
69
69
|
Command: () => Command,
|
|
70
70
|
Container: () => Container,
|
|
71
|
-
DataTable: () =>
|
|
71
|
+
DataTable: () => DataTable2,
|
|
72
72
|
DatePicker: () => DatePicker,
|
|
73
73
|
DateRangePicker: () => DateRangePicker,
|
|
74
74
|
Dialog: () => Dialog,
|
|
@@ -1286,6 +1286,14 @@ window.Aura = (() => {
|
|
|
1286
1286
|
const mounted = useMounted();
|
|
1287
1287
|
const live = !native && mounted;
|
|
1288
1288
|
const density = useDensity();
|
|
1289
|
+
React13.useEffect(function() {
|
|
1290
|
+
if (props.label || props["aria-label"] || props["aria-labelledby"] || props.title) return;
|
|
1291
|
+
if (document.querySelector('label[for="' + id.replace(/["\\]/g, "\\$&") + '"]')) return;
|
|
1292
|
+
devWarnOnce(
|
|
1293
|
+
"select-name",
|
|
1294
|
+
"Select needs a label, aria-label or aria-labelledby: screen readers announce it with no name."
|
|
1295
|
+
);
|
|
1296
|
+
}, []);
|
|
1289
1297
|
const selRef = React13.useRef(null), trigRef = React13.useRef(null), listRef = React13.useRef(null);
|
|
1290
1298
|
const shown = React13.useState(function() {
|
|
1291
1299
|
return initialLabel(props);
|
|
@@ -3974,7 +3982,7 @@ window.Aura = (() => {
|
|
|
3974
3982
|
var ROW_H_DEFAULT = 48;
|
|
3975
3983
|
var OVERSCAN = 8;
|
|
3976
3984
|
var FLEX_MIN = 160;
|
|
3977
|
-
var
|
|
3985
|
+
var DataTableImpl = React28.forwardRef(function DataTable(props, ref) {
|
|
3978
3986
|
const t = useStrings();
|
|
3979
3987
|
const columns = props.columns || DEFAULT_COLUMNS;
|
|
3980
3988
|
const byKey = {};
|
|
@@ -5209,6 +5217,7 @@ window.Aura = (() => {
|
|
|
5209
5217
|
}
|
|
5210
5218
|
return /* @__PURE__ */ React28.createElement("div", { ref, className: cx("aura-table-box", props.className) }, out);
|
|
5211
5219
|
});
|
|
5220
|
+
var DataTable2 = DataTableImpl;
|
|
5212
5221
|
|
|
5213
5222
|
// src/Card.tsx
|
|
5214
5223
|
var React29 = __toESM(require_react(), 1);
|
package/components/index.d.ts
CHANGED
|
@@ -46,7 +46,18 @@ export interface DataTableState {
|
|
|
46
46
|
sort: DataTableSort | null;
|
|
47
47
|
page: number;
|
|
48
48
|
}
|
|
49
|
-
|
|
49
|
+
/** Internal helper (exported only because the declarations reference it): `T` as written, but never inferred from
|
|
50
|
+
* this position (works before TypeScript 5.4's own `NoInfer`), so a table's row type comes from its `rows`. */
|
|
51
|
+
export type NoInferRow<T> = [
|
|
52
|
+
T
|
|
53
|
+
][T extends any ? 0 : never];
|
|
54
|
+
/** A callback that gets one row of type `Row` and returns `T`. Checked the way method parameters are, so a callback
|
|
55
|
+
* written for a narrower or wider row type still fits; one written for an unrelated type is an error (5.5). */
|
|
56
|
+
export type RowCallback<T, Row = Record<string, any>> = {
|
|
57
|
+
bivarianceHack(row: NoInferRow<Row>): T;
|
|
58
|
+
}["bivarianceHack"];
|
|
59
|
+
/** One column. `Row` is the table's row type (5.5): `DataTableColumn<Order>` types `render` and `sortValue`. */
|
|
60
|
+
export interface DataTableColumn<Row extends Record<string, any> = Record<string, any>> {
|
|
50
61
|
/** Key into each row object. */
|
|
51
62
|
key: string;
|
|
52
63
|
/** Header label (source style: UPPERCASE). */
|
|
@@ -62,9 +73,9 @@ export interface DataTableColumn {
|
|
|
62
73
|
/** Header becomes a button cycling asc → desc → unsorted. */
|
|
63
74
|
sortable?: boolean | undefined;
|
|
64
75
|
/** Value to sort by, when not the cell value (dates, amounts). */
|
|
65
|
-
sortValue?:
|
|
76
|
+
sortValue?: RowCallback<string | number | null, Row> | undefined;
|
|
66
77
|
/** Custom cell content. */
|
|
67
|
-
render?:
|
|
78
|
+
render?: RowCallback<React$1.ReactNode, Row> | undefined;
|
|
68
79
|
/** Set false to keep a sized column fixed when the table is `resizable`. */
|
|
69
80
|
resizable?: boolean | undefined;
|
|
70
81
|
/** Resize limits in px. Defaults 64 / 480. For the flexible column, minWidth defaults to 160. */
|
|
@@ -94,11 +105,14 @@ export interface DataTableEmpty {
|
|
|
94
105
|
/** One action, usually a secondary Button. */
|
|
95
106
|
action?: React$1.ReactNode | undefined;
|
|
96
107
|
}
|
|
97
|
-
/** Enterprise data table: 48px rows, hairline dividers, mono header band.
|
|
98
|
-
|
|
108
|
+
/** Enterprise data table: 48px rows, hairline dividers, mono header band. Generic over the row type (5.5): with
|
|
109
|
+
* `rows={orders}` every `render`, `sortValue`, `getRowHref` and `onRowActivate` gets an `Order`. */
|
|
110
|
+
export interface DataTableProps<Row extends Record<string, any> = Record<string, any>> {
|
|
99
111
|
/** Defaults to ID · NAME · STATUS · OWNER (96 / 160 / 112 / auto px). */
|
|
100
|
-
columns?: DataTableColumn[] | undefined;
|
|
101
|
-
|
|
112
|
+
columns?: DataTableColumn<Row>[] | undefined;
|
|
113
|
+
/** Row objects, of any interface; their type is the table's row type. Cells show the value at each column's key
|
|
114
|
+
* unless the column has `render`. */
|
|
115
|
+
rows: ReadonlyArray<Row>;
|
|
102
116
|
/** Column key giving each row a unique React key. Default: the first column's key. */
|
|
103
117
|
rowKey?: string | undefined;
|
|
104
118
|
/** Accessible name for the table. */
|
|
@@ -145,7 +159,7 @@ export interface DataTableProps {
|
|
|
145
159
|
totalRows?: number | undefined;
|
|
146
160
|
/** Makes each row a link: the first column's content renders as the provider's linkComponent (or `<a>`), and a click
|
|
147
161
|
* or Enter anywhere on the row follows it. Ctrl/⌘-click opens a new tab as usual. */
|
|
148
|
-
getRowHref?:
|
|
162
|
+
getRowHref?: RowCallback<string, Row> | undefined;
|
|
149
163
|
/** Pager arrows become links to these URLs (search-param paging). Without onPageChange the link navigates. */
|
|
150
164
|
getPageHref?: ((page: number) => string) | undefined;
|
|
151
165
|
/** Router link for getRowHref / getPageHref. Default: AuraProvider's linkComponent, else `<a>`. */
|
|
@@ -153,7 +167,7 @@ export interface DataTableProps {
|
|
|
153
167
|
/** Skeleton row count when there is no pageSize. Default 5. */
|
|
154
168
|
skeletonRows?: number | undefined;
|
|
155
169
|
/** Called on row click or Enter. */
|
|
156
|
-
onRowActivate?:
|
|
170
|
+
onRowActivate?: RowCallback<void, Row> | undefined;
|
|
157
171
|
/** Fixed height in px: sticky header, and only the rows in view are rendered. */
|
|
158
172
|
height?: number | undefined;
|
|
159
173
|
/** Column menu on every header, drag-to-reorder, and the show/hide columns button. */
|
|
@@ -321,11 +335,14 @@ export interface FormErrorItem {
|
|
|
321
335
|
message: React$1.ReactNode;
|
|
322
336
|
}
|
|
323
337
|
/** The list of problems at the top of a form after a failed submit; each links to its field (GOV.UK pattern). */
|
|
338
|
+
/** react-hook-form's `formState.errors` (FieldErrors) or any object shaped like it: a `message` at any depth. */
|
|
339
|
+
export type FormErrorTree = {
|
|
340
|
+
readonly [field: string]: object | undefined;
|
|
341
|
+
};
|
|
324
342
|
export interface FormErrorSummaryProps {
|
|
325
|
-
/** A list, or react-hook-form's `formState.errors` as is
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
} | undefined>;
|
|
343
|
+
/** A list, or react-hook-form's `formState.errors` as is: `{ name: { message } }`, nested for `address.street`
|
|
344
|
+
* and field arrays (`items.0.name`). Empty → renders nothing. */
|
|
345
|
+
errors: FormErrorItem[] | FormErrorTree;
|
|
329
346
|
/** Default "Fix N fields to continue" in the provider's language. */
|
|
330
347
|
title?: React$1.ReactNode | undefined;
|
|
331
348
|
/** Called with the field when a link is followed, e.g. react-hook-form's `setFocus`. Default: focus the element whose id (or name) is the field. */
|
|
@@ -419,7 +436,10 @@ export type SelectOption = string | {
|
|
|
419
436
|
};
|
|
420
437
|
/** 5.3: opens AURA's own list (light and dark alike) over a real `<select>`, which keeps `name`, `required`, the ref,
|
|
421
438
|
* `onChange`, react-hook-form `register` and form posts. `multiple` or `size > 1` keeps the native list box. */
|
|
422
|
-
export interface SelectProps extends FieldProps, Omit<React$1.SelectHTMLAttributes<HTMLSelectElement>, "required"> {
|
|
439
|
+
export interface SelectProps extends Omit<FieldProps, "label">, Omit<React$1.SelectHTMLAttributes<HTMLSelectElement>, "required"> {
|
|
440
|
+
/** Visible label; also the accessible name. Omit only with `aria-label` or `aria-labelledby` (a toolbar, a table
|
|
441
|
+
* cell); development builds warn when the field has no name at all. */
|
|
442
|
+
label?: string | undefined;
|
|
423
443
|
/** The choices; or pass `<option>` / `<optgroup>` children instead (5.3: optional). */
|
|
424
444
|
options?: SelectOption[] | undefined;
|
|
425
445
|
/** Shown in fg-tertiary on the closed field until something is chosen; not listed as a choice. */
|
|
@@ -1277,8 +1297,15 @@ export declare const Tooltip: React$1.ForwardRefExoticComponent<TooltipProps & R
|
|
|
1277
1297
|
export declare const Dialog: React$1.ForwardRefExoticComponent<DialogProps & React$1.RefAttributes<HTMLDivElement>>;
|
|
1278
1298
|
/** Side panel over the page: record detail, filters, mobile navigation. Modal (focus trap, scroll lock, focus restore). */
|
|
1279
1299
|
export declare const Drawer: React$1.ForwardRefExoticComponent<DrawerProps & React$1.RefAttributes<HTMLDivElement>>;
|
|
1280
|
-
/** Enterprise data table: 48px rows, hairline dividers, mono header band.
|
|
1281
|
-
|
|
1300
|
+
/** Enterprise data table: 48px rows, hairline dividers, mono header band. Generic over the row type (5.5): the
|
|
1301
|
+
* type of `rows` types every `render`, `sortValue`, `getRowHref` and `onRowActivate`. */
|
|
1302
|
+
export declare const DataTable: {
|
|
1303
|
+
(props: DataTableProps & {
|
|
1304
|
+
rows: readonly never[];
|
|
1305
|
+
} & React$1.RefAttributes<HTMLDivElement>): React$1.ReactElement | null;
|
|
1306
|
+
<Row extends Record<string, any> = Record<string, any>>(props: DataTableProps<Row> & React$1.RefAttributes<HTMLDivElement>): React$1.ReactElement | null;
|
|
1307
|
+
displayName?: string | undefined;
|
|
1308
|
+
};
|
|
1282
1309
|
export declare const Card: React$1.ForwardRefExoticComponent<CardProps & React$1.RefAttributes<HTMLElement>>;
|
|
1283
1310
|
export declare const Tabs: React$1.ForwardRefExoticComponent<TabsProps & React$1.RefAttributes<HTMLDivElement>>;
|
|
1284
1311
|
/** Side navigation: sections of links or buttons, collapsible groups, counts and badges. Arrow keys move between items. */
|
package/eslint-plugin-aura.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// @ts-check
|
|
1
2
|
// eslint-plugin-aura.js — AURA 5.2
|
|
2
3
|
// Flags hard-coded colours in JS/TS: hex (#rgb, #rgba, #rrggbb, #rrggbbaa), colour functions (rgb/hsl/hwb/lab/lch/
|
|
3
4
|
// oklab/oklch/color-mix), CSS named colours in a style value, Tailwind palette classes (bg-red-500, text-white) and
|
|
@@ -17,6 +18,7 @@ const HEX = /#(?:[0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3,4})(?![\w-])/i;
|
|
|
17
18
|
* call a template literal continues (`rgb(${r}, …)`). Token-based ones pass: rgb(var(--brand-rgb) / .5),
|
|
18
19
|
* color-mix(in srgb, var(--aura-accent) 20%, transparent). var() and url() are removed before any check. */
|
|
19
20
|
const FN_CALL = /\b(?:rgba?|hsla?|hwb|lab|lch|oklab|oklch|color-mix)\(([^()]*)(\)|$)/gi;
|
|
21
|
+
/** @param {string} v */
|
|
20
22
|
function fnHit(v) {
|
|
21
23
|
FN_CALL.lastIndex = 0;
|
|
22
24
|
let m;
|
|
@@ -26,7 +28,7 @@ function fnHit(v) {
|
|
|
26
28
|
}
|
|
27
29
|
return null;
|
|
28
30
|
}
|
|
29
|
-
const stripRefs = (v) => v.replace(/var\([^()]*(?:\([^()]*\)[^()]*)*\)/g, ' ').replace(/url\([^)]*\)/g, ' ');
|
|
31
|
+
const stripRefs = (/** @type {string} */ v) => v.replace(/var\([^()]*(?:\([^()]*\)[^()]*)*\)/g, ' ').replace(/url\([^)]*\)/g, ' ');
|
|
30
32
|
const TW_ARB = new RegExp('(?:^|[\\s:"\'`])(?:' + UTIL + ')-\\[(?:#|rgb|hsl|oklch|color)', 'i');
|
|
31
33
|
const TW_PAL = new RegExp(
|
|
32
34
|
'(?:^|[\\s:"\'`!])(?:' + UTIL + ')-(?:(?:' + PALETTE + ')-\\d{2,3}|white|black)(?:\\/\\d+)?(?![\\w-])',
|
|
@@ -49,6 +51,10 @@ const COLOUR_KEY =
|
|
|
49
51
|
const NOT_COLOUR_ATTR =
|
|
50
52
|
/^(?:href|to|id|htmlFor|for|src|action|formAction|xlinkHref|xlink:href|aria-[a-z]+|data-[\w-]+|name|key)$/;
|
|
51
53
|
|
|
54
|
+
/**
|
|
55
|
+
* @param {unknown} raw
|
|
56
|
+
* @param {boolean} colourKey
|
|
57
|
+
*/
|
|
52
58
|
function hitIn(raw, colourKey) {
|
|
53
59
|
if (typeof raw !== 'string') return null;
|
|
54
60
|
const value = stripRefs(raw);
|
|
@@ -59,6 +65,8 @@ function hitIn(raw, colourKey) {
|
|
|
59
65
|
if (colourKey && NAMED_WHOLE.test(value)) return value.trim();
|
|
60
66
|
return null;
|
|
61
67
|
}
|
|
68
|
+
/** An ESTree / JSX AST node, loosely (the plugin runs on whatever parser the project uses). @typedef {any} Node */
|
|
69
|
+
/** @param {Node} node */
|
|
62
70
|
function skip(node) {
|
|
63
71
|
let p = node.parent;
|
|
64
72
|
if (!p) return false;
|
|
@@ -80,11 +88,17 @@ function skip(node) {
|
|
|
80
88
|
return true;
|
|
81
89
|
return false;
|
|
82
90
|
}
|
|
91
|
+
/** @param {Node} node */
|
|
83
92
|
function colourKeyOf(node) {
|
|
84
93
|
const p = node.parent;
|
|
85
94
|
if (p && p.type === 'Property' && p.value === node && p.key) return COLOUR_KEY.test(p.key.name || p.key.value || '');
|
|
86
95
|
return false;
|
|
87
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* @param {{ report(d: { node: Node, message: string }): void }} context
|
|
99
|
+
* @param {Node} node
|
|
100
|
+
* @param {string} hit
|
|
101
|
+
*/
|
|
88
102
|
function report(context, node, hit) {
|
|
89
103
|
context.report({
|
|
90
104
|
node,
|
|
@@ -101,14 +115,14 @@ const plugin = {
|
|
|
101
115
|
docs: { description: 'ห้ามใช้สี hardcode ให้ใช้ token — use AURA semantic tokens, never raw colours' },
|
|
102
116
|
schema: [],
|
|
103
117
|
},
|
|
104
|
-
create(context) {
|
|
118
|
+
create(/** @type {any} */ context) {
|
|
105
119
|
return {
|
|
106
|
-
Literal(node) {
|
|
120
|
+
Literal(/** @type {Node} */ node) {
|
|
107
121
|
if (skip(node)) return;
|
|
108
122
|
const hit = hitIn(node.value, colourKeyOf(node));
|
|
109
123
|
if (hit) report(context, node, hit);
|
|
110
124
|
},
|
|
111
|
-
TemplateElement(node) {
|
|
125
|
+
TemplateElement(/** @type {Node} */ node) {
|
|
112
126
|
if (skip(node)) return;
|
|
113
127
|
const hit = hitIn(node.value && node.value.raw, false);
|
|
114
128
|
if (hit) report(context, node, hit);
|
|
@@ -117,6 +131,7 @@ const plugin = {
|
|
|
117
131
|
},
|
|
118
132
|
},
|
|
119
133
|
},
|
|
134
|
+
/** @type {Record<string, { plugins: unknown, rules: Record<string, string> }>} */
|
|
120
135
|
configs: {},
|
|
121
136
|
};
|
|
122
137
|
/* Flat config (ESLint 9, the default): the plugin object itself, not a name. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jirawatpyk/aura-tokens",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.5.0",
|
|
4
4
|
"description": "AURA Design System — tokens (CSS variables, Tailwind preset, Figma variables), theme contrast checks and token lint.",
|
|
5
5
|
"main": "tailwind.tokens.cjs",
|
|
6
6
|
"types": "components/index.d.ts",
|
|
@@ -55,14 +55,14 @@
|
|
|
55
55
|
"./package.json": "./package.json"
|
|
56
56
|
},
|
|
57
57
|
"scripts": {
|
|
58
|
-
"build": "node scripts/build-tokens.
|
|
58
|
+
"build": "node scripts/build-tokens.mts",
|
|
59
59
|
"a11y": "node scripts/a11y-check.js",
|
|
60
|
-
"check:tailwind4": "node scripts/tailwind4-check.
|
|
60
|
+
"check:tailwind4": "node scripts/tailwind4-check.mts",
|
|
61
61
|
"lint:tokens": "node scripts/lint-tokens.js ../react/src && node scripts/lint-tokens.js ../react/styles",
|
|
62
|
-
"check:generated": "node scripts/build-tokens.
|
|
62
|
+
"check:generated": "node scripts/build-tokens.mts && git diff --exit-code -- aura.css aura-fonts.css aura-fonts.local.css tailwind.tokens.cjs aura-tailwind.css aura-tailwind.prefixed.css figma-variables.csv",
|
|
63
63
|
"prepack": "npm run build",
|
|
64
|
-
"check:fonts": "node scripts/fonts-check.
|
|
65
|
-
"check:eslint": "node scripts/eslint-check.
|
|
64
|
+
"check:fonts": "node scripts/fonts-check.mts",
|
|
65
|
+
"check:eslint": "node scripts/eslint-check.mts"
|
|
66
66
|
},
|
|
67
67
|
"keywords": [
|
|
68
68
|
"design-system",
|
package/scripts/a11y-check.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
// @ts-check
|
|
2
3
|
/* AURA contrast check — resolves tokens.json and tests every documented text/ground pair in BOTH themes.
|
|
3
4
|
* Text needs 4.5:1, control edges and focus rings 3:1 (WCAG 2.1 AA). Writes a11y-report.json; exits 1 on any failure. */
|
|
4
5
|
const fs = require('fs');
|
|
@@ -6,6 +7,11 @@ const path = require('path');
|
|
|
6
7
|
const TOKENS = path.join(__dirname, '..', 'tokens.json');
|
|
7
8
|
const T = JSON.parse(fs.readFileSync(TOKENS, 'utf8'));
|
|
8
9
|
|
|
10
|
+
/**
|
|
11
|
+
* @param {unknown} value
|
|
12
|
+
* @param {string} theme
|
|
13
|
+
* @returns {string}
|
|
14
|
+
*/
|
|
9
15
|
function resolve(value, theme, depth = 0) {
|
|
10
16
|
if (depth > 16) throw new Error('alias loop at ' + value);
|
|
11
17
|
const m = /^\{(primitive|semantic|component)\.([a-z0-9-]+)(?:\.([a-z0-9]+))?\}$/.exec(String(value).trim());
|
|
@@ -16,24 +22,38 @@ function resolve(value, theme, depth = 0) {
|
|
|
16
22
|
if (v === undefined) throw new Error(`missing ${tier}.${a} (${theme})`);
|
|
17
23
|
return resolve(v, theme, depth + 1);
|
|
18
24
|
}
|
|
19
|
-
const tok = (name, theme) => resolve(T.semantic[theme][name] ?? T.component[theme][name], theme);
|
|
25
|
+
const tok = (/** @type {string} */ name, /** @type {string} */ theme) => resolve(T.semantic[theme][name] ?? T.component[theme][name], theme);
|
|
20
26
|
|
|
27
|
+
/**
|
|
28
|
+
* @param {string} c
|
|
29
|
+
* @param {string} [ground]
|
|
30
|
+
* @returns {number[]}
|
|
31
|
+
*/
|
|
21
32
|
function rgb(c, ground) {
|
|
22
33
|
let m = /^#([0-9a-f]{6})$/i.exec(c);
|
|
23
|
-
|
|
34
|
+
const hex = m && m[1];
|
|
35
|
+
if (hex) return [0, 2, 4].map((i) => parseInt(hex.slice(i, i + 2), 16));
|
|
24
36
|
m = /^#([0-9a-f]{3})$/i.exec(c);
|
|
25
37
|
if (m) return m[1].split('').map((x) => parseInt(x + x, 16));
|
|
26
38
|
m = /^rgba?\(([^)]+)\)$/i.exec(c);
|
|
27
39
|
if (m) {
|
|
28
40
|
const [r, g, b, a = 1] = m[1].split(',').map(Number);
|
|
41
|
+
if ([r, g, b, a].some((x) => x === undefined || Number.isNaN(x))) throw new Error('unsupported colour ' + c);
|
|
29
42
|
if (a >= 1 || !ground) return [r, g, b];
|
|
30
43
|
const G = rgb(ground);
|
|
31
|
-
|
|
44
|
+
/** @type {(v: number, i: number) => number} */
|
|
45
|
+
const mix = (v, i) => Math.round(a * v + (1 - a) * (G[i] || 0));
|
|
46
|
+
return [r || 0, g || 0, b || 0].map(mix);
|
|
32
47
|
}
|
|
33
48
|
throw new Error('unsupported colour ' + c);
|
|
34
49
|
}
|
|
35
|
-
const
|
|
36
|
-
|
|
50
|
+
const W = [0.2126, 0.7152, 0.0722];
|
|
51
|
+
const lum = (/** @type {string} */ c) => rgb(c).map((v) => v / 255).map((v) => (v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4))
|
|
52
|
+
.reduce((s, v, i) => s + v * (W[i] || 0), 0);
|
|
53
|
+
/**
|
|
54
|
+
* @param {string} fg
|
|
55
|
+
* @param {string} bg
|
|
56
|
+
*/
|
|
37
57
|
function ratio(fg, bg) {
|
|
38
58
|
const B = '#' + rgb(bg).map((v) => v.toString(16).padStart(2, '0')).join('');
|
|
39
59
|
const F = '#' + rgb(fg, B).map((v) => v.toString(16).padStart(2, '0')).join('');
|
|
@@ -43,6 +63,7 @@ function ratio(fg, bg) {
|
|
|
43
63
|
|
|
44
64
|
const TEXT = 4.5, UI = 3;
|
|
45
65
|
const grounds = ['bg-surface', 'bg-canvas', 'bg-surface-hover'];
|
|
66
|
+
/** @type {[string, string, number][]} */
|
|
46
67
|
const pairs = [];
|
|
47
68
|
for (const fg of ['fg-primary', 'fg-secondary', 'fg-tertiary', 'fg-accent', 'fg-danger', 'fg-positive']) for (const bg of grounds) pairs.push([fg, bg, TEXT]);
|
|
48
69
|
pairs.push(['fg-primary', 'bg-selected', TEXT], ['fg-secondary', 'bg-selected', TEXT]);
|
package/scripts/lint-tokens.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
// @ts-check
|
|
2
3
|
/* Token lint — fails when source files hard-code colours instead of AURA tokens, or (CSS) motion:
|
|
3
4
|
* a transition/animation must take its duration and easing from --aura-duration-* / --aura-ease / --aura-spring
|
|
4
5
|
* (`linear` is allowed: it's for spinners and progress, not a feel).
|
|
@@ -24,16 +25,19 @@ const NAMED = 'white|black|red|green|blue|yellow|orange|purple|pink|gray|grey|si
|
|
|
24
25
|
const CSS_NAMED = new RegExp('(?:^|[;{\\s])(?:color|background(?:-color)?|border(?:-[a-z]+)?-color|fill|stroke|outline-color)\\s*:\\s*[^;]*(?<![\\w-])(?:' + NAMED + ')(?![\\w-])');
|
|
25
26
|
/* Colour functions count only with a literal colour inside (a hex or three numbers), as in eslint-plugin-aura. */
|
|
26
27
|
const FN_CALL = /\b(?:rgba?|hsla?|hwb|lab|lch|oklab|oklch|color-mix)\(([^()]*)(\)|$)/gi;
|
|
28
|
+
/** @param {string} v */
|
|
27
29
|
function fnHit(v) {
|
|
28
30
|
FN_CALL.lastIndex = 0;
|
|
29
31
|
let m;
|
|
30
32
|
while ((m = FN_CALL.exec(v))) if (!m[2] || /#[0-9a-f]{3}/i.test(m[1]) || (m[1].match(/-?\d*\.?\d+/g) || []).length >= 3) return m[0];
|
|
31
33
|
return null;
|
|
32
34
|
}
|
|
33
|
-
const stripRefs = (v) => v.replace(/var\([^()]*(?:\([^()]*\)[^()]*)*\)/g, ' ').replace(/url\([^)]*\)/g, ' ');
|
|
35
|
+
const stripRefs = (/** @type {string} */ v) => v.replace(/var\([^()]*(?:\([^()]*\)[^()]*)*\)/g, ' ').replace(/url\([^)]*\)/g, ' ');
|
|
34
36
|
const MOTION_PROP = /\b(?:transition|animation)(?:-duration|-timing-function|-delay)?\s*:/i;
|
|
35
37
|
const MOTION_RAW = /(?:^|[\s,:(])(?:\d*\.?\d+m?s)\b|cubic-bezier\(|\bease(?:-in-out|-in|-out)?\b/gi;
|
|
38
|
+
/** @type {string[]} */
|
|
36
39
|
const violations = [];
|
|
40
|
+
/** @param {string} d */
|
|
37
41
|
function scan(d) {
|
|
38
42
|
if (!fs.existsSync(d)) return;
|
|
39
43
|
for (const f of fs.readdirSync(d)) {
|
|
@@ -46,8 +50,9 @@ function scan(d) {
|
|
|
46
50
|
if (/aura-lint:\s*allow-colours/.test(text.slice(0, 600))) continue;
|
|
47
51
|
text.split('\n').forEach((line, i) => {
|
|
48
52
|
const plain = stripRefs(line);
|
|
49
|
-
const
|
|
50
|
-
|
|
53
|
+
const fn = fnHit(plain);
|
|
54
|
+
const m = plain.match(COLOUR) || (fn ? [fn] : null);
|
|
55
|
+
if (m) violations.push(`${path.relative(process.cwd(), full)}:${i + 1} ${m.map((/** @type {string} */ s) => s.trim().replace(/^[:"'`!]/, '')).join(', ')}`);
|
|
51
56
|
if (/\.s?css$/.test(f) && !/^\s*--/.test(line)) {
|
|
52
57
|
const n = CSS_NAMED.exec(stripRefs(line.replace(/\/\*.*?\*\//g, '')));
|
|
53
58
|
if (n) violations.push(`${path.relative(process.cwd(), full)}:${i + 1} named colour: ${n[0].trim()}`);
|
package/tailwind.tokens.cjs
CHANGED
package/tokens.json
CHANGED