dowel-ui 0.4.0 → 0.6.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 CHANGED
@@ -8,7 +8,10 @@ The lacodda line design system: theme tokens and React primitives, distributed a
8
8
 
9
9
  A dowel is the hidden peg that joins two boards so the seam does not show. That is what this does for the products of the line: they look made by one hand, and nobody sees the joint.
10
10
 
11
- **Status:** v0.4.0 - the theme, the scales, an accent per product, and the first primitive. See the [roadmap](#roadmap).
11
+ **[Documentation](https://lacodda.github.io/dowel/)** what everything is and why it is that way.
12
+ **[The stand](https://lacodda.github.io/dowel/stand/)** — every component, live, in either theme and in the accent of any product of the line.
13
+
14
+ **Status:** v0.6.0 - the theme, the scales, an accent per product, ten primitives and the lint rule that keeps them honest. The first product of the line lives on it. See the [roadmap](#roadmap).
12
15
 
13
16
  ## The theme
14
17
 
@@ -80,9 +83,24 @@ your code:
80
83
  npx shadcn@latest add https://lacodda.github.io/dowel/r/button.json
81
84
  ```
82
85
 
86
+ Ten of them so far: Button, Input, Textarea, Panel, Badge, Chip, Kbd, Spinner,
87
+ Truncate and Copyable.
88
+
83
89
  Each is written in the vocabulary - no raw colours, no `dark:` utilities - so
84
90
  the same component is correct in both themes and in every product's accent.
85
- Every one of them is shown live on [the stand](https://lacodda.github.io/dowel/components/button/), in both themes at once.
91
+ Every one of them is on [the stand](https://lacodda.github.io/dowel/stand/), live - change the theme and the accent and watch what follows.
92
+
93
+ That convention is a lint rule, and it ships with the package:
94
+
95
+ ```js
96
+ // eslint.config.js
97
+ import dowel from 'dowel-ui/eslint'
98
+
99
+ export default [...dowel.configs.recommended]
100
+ ```
101
+
102
+ It reports a hex, an `rgb()`, a stock Tailwind colour, `bg-white`, and any
103
+ `dark:` utility, in the file that wrote one - see [the guide](https://lacodda.github.io/dowel/guides/linting/).
86
104
 
87
105
  Full vocabulary, shown rather than tabulated: **[colours](https://lacodda.github.io/dowel/reference/tokens/)** in both themes, **[the scales](https://lacodda.github.io/dowel/reference/scales/)** - radius, type, motion, elevation and stacking order - and **[the accents](https://lacodda.github.io/dowel/reference/accents/)**, where the same screen is drawn in every colour of the line.
88
106
 
@@ -102,9 +120,10 @@ Development goes in versions; each one is a single coherent theme, and ends in a
102
120
 
103
121
  ## Documentation
104
122
 
105
- - [Getting started](https://lacodda.github.io/dowel/getting-started/)
123
+ - [The stand](https://lacodda.github.io/dowel/stand/) - every component, live
124
+ - [Getting started](https://lacodda.github.io/dowel/getting-started/) and [the lint rule](https://lacodda.github.io/dowel/guides/linting/)
106
125
  - [Components](https://lacodda.github.io/dowel/components/button/)
107
- - [Token reference](https://lacodda.github.io/dowel/reference/tokens/)
126
+ - [Tokens](https://lacodda.github.io/dowel/reference/tokens/), [scales](https://lacodda.github.io/dowel/reference/scales/) and [accents](https://lacodda.github.io/dowel/reference/accents/)
108
127
 
109
128
  ## License
110
129
 
@@ -0,0 +1,12 @@
1
+ import type { ESLint, Linter } from 'eslint';
2
+ /** The plugin, with its config bundled on - the shape flat config expects
3
+ * from a default export. Annotated rather than inferred: the inferred type
4
+ * reaches into ESLint's internals and cannot be written down by a consumer. */
5
+ declare const dowel: ESLint.Plugin & {
6
+ configs: {
7
+ recommended: Linter.Config[];
8
+ };
9
+ };
10
+ export default dowel;
11
+ export { noRawColor, findRawColor } from './no-raw-color.js';
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/eslint/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAoC5C;;+EAE+E;AAC/E,QAAA,MAAM,KAAK,EAAE,MAAM,CAAC,MAAM,GAAG;IAAE,OAAO,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,CAAA;KAAE,CAAA;CAEtE,CAAA;AAEF,eAAe,KAAK,CAAA;AACpB,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA"}
@@ -0,0 +1,40 @@
1
+ import { noRawColor } from './no-raw-color.js';
2
+ /*
3
+ * The dowel ESLint plugin.
4
+ *
5
+ * One rule, and it enforces the convention the whole system rests on: a
6
+ * component names colours from the vocabulary and never writes one down.
7
+ *
8
+ * It ships from the package rather than the registry because it is not a
9
+ * component - it is not copied into a product and edited there, it is a check
10
+ * every product runs the same way, and it should improve for all of them at
11
+ * once.
12
+ *
13
+ * import dowel from 'dowel-ui/eslint'
14
+ *
15
+ * export default [
16
+ * ...dowel.configs.recommended,
17
+ * ]
18
+ */
19
+ const plugin = {
20
+ meta: { name: 'dowel' },
21
+ rules: { 'no-raw-color': noRawColor },
22
+ };
23
+ /** The rule, applied where components live. Scoped to TypeScript sources: the
24
+ * theme is CSS and the build tools are Node, and neither is a component. */
25
+ const recommended = [
26
+ {
27
+ files: ['**/*.{ts,tsx}'],
28
+ plugins: { dowel: plugin },
29
+ rules: { 'dowel/no-raw-color': 'error' },
30
+ },
31
+ ];
32
+ /** The plugin, with its config bundled on - the shape flat config expects
33
+ * from a default export. Annotated rather than inferred: the inferred type
34
+ * reaches into ESLint's internals and cannot be written down by a consumer. */
35
+ const dowel = Object.assign(plugin, {
36
+ configs: { recommended },
37
+ });
38
+ export default dowel;
39
+ export { noRawColor, findRawColor } from './no-raw-color.js';
40
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/eslint/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAE9C;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,MAAM,GAAG;IACb,IAAI,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE;IACvB,KAAK,EAAE,EAAE,cAAc,EAAE,UAAU,EAAE;CACd,CAAA;AAEzB;4EAC4E;AAC5E,MAAM,WAAW,GAAoB;IACnC;QACE,KAAK,EAAE,CAAC,eAAe,CAAC;QACxB,OAAO,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE;QAC1B,KAAK,EAAE,EAAE,oBAAoB,EAAE,OAAO,EAAE;KACzC;CACF,CAAA;AAED;;+EAE+E;AAC/E,MAAM,KAAK,GAAkE,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE;IACjG,OAAO,EAAE,EAAE,WAAW,EAAE;CACzB,CAAC,CAAA;AAEF,eAAe,KAAK,CAAA;AACpB,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA"}
@@ -0,0 +1,11 @@
1
+ import type { Rule } from 'eslint';
2
+ interface Finding {
3
+ message: string;
4
+ }
5
+ /** What is wrong with this string, if anything. Exported for the test: the
6
+ * matching is the whole rule, and testing it directly is what lets a mutation
7
+ * to any single pattern show up as a failure. */
8
+ export declare function findRawColor(text: string): Finding | undefined;
9
+ export declare const noRawColor: Rule.RuleModule;
10
+ export {};
11
+ //# sourceMappingURL=no-raw-color.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-raw-color.d.ts","sourceRoot":"","sources":["../../src/eslint/no-raw-color.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AA2DlC,UAAU,OAAO;IACf,OAAO,EAAE,MAAM,CAAA;CAChB;AAED;;iDAEiD;AACjD,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAgC9D;AAED,eAAO,MAAM,UAAU,EAAE,IAAI,CAAC,UA2B7B,CAAA"}
@@ -0,0 +1,109 @@
1
+ /*
2
+ * `no-raw-color` - the rule behind the line's oldest convention.
3
+ *
4
+ * Two things are forbidden in a component, and they are the same mistake seen
5
+ * from either end:
6
+ *
7
+ * - a raw colour (`#d9569e`, `rgb(...)`, `bg-zinc-800`), because a colour
8
+ * without a name in the vocabulary cannot follow the product's accent and
9
+ * will not swap when the theme does;
10
+ * - a `dark:` utility, because it says the component knows which theme it is
11
+ * in. It does not, and must not: the token underneath changes instead.
12
+ *
13
+ * The check is deliberately textual. What matters is what ends up in a
14
+ * `className`, and that string is assembled from literals in `cva` variant
15
+ * maps, ternaries and template pieces - places a type-aware rule would have to
16
+ * follow anyway. So every string literal in the file is read, and the ones
17
+ * that look like a colour are reported.
18
+ *
19
+ * It runs on components, not on the theme: the theme is where raw colour is
20
+ * supposed to live, and it is CSS, which this never sees.
21
+ */
22
+ /** A hex colour: `#abc`, `#aabbcc`, `#aabbccdd`. Word-bounded at the end so a
23
+ * hash-prefixed word (`#section-title`) is not mistaken for one. */
24
+ const HEX = /#[0-9a-fA-F]{3}(?:[0-9a-fA-F]{1,5})?\b/;
25
+ /** A CSS colour function with numeric arguments. `color-mix` and `oklch(from
26
+ * var(--accent-base) ...)` are how the theme derives colour and are allowed -
27
+ * what is forbidden is naming a colour outright. */
28
+ const COLOR_FUNCTION = /\b(?:rgba?|hsla?|hwb|lab|lch|oklab)\s*\(/;
29
+ /** Tailwind's stock palette, as it appears inside a utility: `bg-zinc-800`,
30
+ * `hover:text-red-500/50`, `border-slate-200`. The line drops the stock
31
+ * palette from the theme, so these do not compile - but they are written by
32
+ * habit and by every code generator, and the error they produce points at CSS
33
+ * rather than at the file that asked. */
34
+ const STOCK_PALETTE = /\b(?:slate|gray|grey|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950)\b/;
35
+ /** A `dark:` variant, anywhere in a class string. */
36
+ const DARK_VARIANT = /(?:^|[\s"'`])dark:/;
37
+ /*
38
+ * `white` and `black` as Tailwind utilities, at full opacity: `bg-white`,
39
+ * `text-black`. They are colours with no name in the vocabulary and no way to
40
+ * follow a theme - white chrome stays white when the ground turns white.
41
+ *
42
+ * With an opacity they are something else, and allowed: `bg-black/50` is a
43
+ * modal scrim, `text-white/70` is a label over a picture. Neither is a theme
44
+ * colour that failed to be named - they are what a translucent veil and ink
45
+ * on an arbitrary image are made of, which is why the theme keeps
46
+ * `--color-white` and `--color-black` deliberately. The line's first consumer
47
+ * had eleven of them and every one was right.
48
+ */
49
+ const ABSOLUTE = /\b(?:bg|text|border|fill|stroke|ring|outline|divide|shadow|from|via|to)-(?:white|black)\b(?!\/)/;
50
+ /** What is wrong with this string, if anything. Exported for the test: the
51
+ * matching is the whole rule, and testing it directly is what lets a mutation
52
+ * to any single pattern show up as a failure. */
53
+ export function findRawColor(text) {
54
+ if (DARK_VARIANT.test(text)) {
55
+ return {
56
+ message: 'A `dark:` utility makes the component know which theme it is in. Use a token - the theme swaps it underneath.',
57
+ };
58
+ }
59
+ if (HEX.test(text)) {
60
+ return {
61
+ message: 'A raw colour cannot follow the product accent or the theme. Use a token from the dowel vocabulary.',
62
+ };
63
+ }
64
+ if (COLOR_FUNCTION.test(text)) {
65
+ return {
66
+ message: 'A raw colour cannot follow the product accent or the theme. Use a token from the dowel vocabulary.',
67
+ };
68
+ }
69
+ if (STOCK_PALETTE.test(text)) {
70
+ return {
71
+ message: "Tailwind's stock palette is dropped from the theme, so this compiles to nothing. Use a token from the dowel vocabulary.",
72
+ };
73
+ }
74
+ if (ABSOLUTE.test(text)) {
75
+ return {
76
+ message: '`white` and `black` do not change with the theme. Use `text-text`, `bg-bg` or another token.',
77
+ };
78
+ }
79
+ return undefined;
80
+ }
81
+ export const noRawColor = {
82
+ meta: {
83
+ type: 'problem',
84
+ docs: {
85
+ description: 'Forbid raw colours and `dark:` utilities in components; every colour goes through a dowel token.',
86
+ url: 'https://lacodda.github.io/dowel/guides/tokens/',
87
+ },
88
+ schema: [],
89
+ },
90
+ create(context) {
91
+ const report = (node, text) => {
92
+ const finding = findRawColor(text);
93
+ if (finding)
94
+ context.report({ node, message: finding.message });
95
+ };
96
+ return {
97
+ Literal(node) {
98
+ if (typeof node.value === 'string')
99
+ report(node, node.value);
100
+ },
101
+ TemplateElement(node) {
102
+ // A template element carries no cooked value when the template is
103
+ // tagged with an invalid escape; the raw text is always there.
104
+ report(node, node.value.cooked ?? node.value.raw);
105
+ },
106
+ };
107
+ },
108
+ };
109
+ //# sourceMappingURL=no-raw-color.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-raw-color.js","sourceRoot":"","sources":["../../src/eslint/no-raw-color.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH;oEACoE;AACpE,MAAM,GAAG,GAAG,wCAAwC,CAAA;AAEpD;;oDAEoD;AACpD,MAAM,cAAc,GAAG,0CAA0C,CAAA;AAEjE;;;;yCAIyC;AACzC,MAAM,aAAa,GACjB,gMAAgM,CAAA;AAElM,qDAAqD;AACrD,MAAM,YAAY,GAAG,oBAAoB,CAAA;AAEzC;;;;;;;;;;;GAWG;AACH,MAAM,QAAQ,GACZ,iGAAiG,CAAA;AAMnG;;iDAEiD;AACjD,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,IAAI,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QAC5B,OAAO;YACL,OAAO,EACL,+GAA+G;SAClH,CAAA;IACH,CAAC;IACD,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QACnB,OAAO;YACL,OAAO,EACL,oGAAoG;SACvG,CAAA;IACH,CAAC;IACD,IAAI,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QAC9B,OAAO;YACL,OAAO,EACL,oGAAoG;SACvG,CAAA;IACH,CAAC;IACD,IAAI,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QAC7B,OAAO;YACL,OAAO,EACL,yHAAyH;SAC5H,CAAA;IACH,CAAC;IACD,IAAI,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QACxB,OAAO;YACL,OAAO,EACL,8FAA8F;SACjG,CAAA;IACH,CAAC;IACD,OAAO,SAAS,CAAA;AAClB,CAAC;AAED,MAAM,CAAC,MAAM,UAAU,GAAoB;IACzC,IAAI,EAAE;QACJ,IAAI,EAAE,SAAS;QACf,IAAI,EAAE;YACJ,WAAW,EACT,kGAAkG;YACpG,GAAG,EAAE,gDAAgD;SACtD;QACD,MAAM,EAAE,EAAE;KACX;IACD,MAAM,CAAC,OAAO;QACZ,MAAM,MAAM,GAAG,CAAC,IAAe,EAAE,IAAY,EAAE,EAAE;YAC/C,MAAM,OAAO,GAAG,YAAY,CAAC,IAAI,CAAC,CAAA;YAClC,IAAI,OAAO;gBAAE,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC,CAAA;QACjE,CAAC,CAAA;QAED,OAAO;YACL,OAAO,CAAC,IAAI;gBACV,IAAI,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ;oBAAE,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,CAAA;YAC9D,CAAC;YACD,eAAe,CAAC,IAAI;gBAClB,kEAAkE;gBAClE,+DAA+D;gBAC/D,MAAM,CAAC,IAAiB,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;YAChE,CAAC;SACF,CAAA;IACH,CAAC;CACF,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dowel-ui",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "The lacodda line design system: theme tokens and React primitives, distributed as a shadcn-compatible registry.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -19,7 +19,8 @@
19
19
  "react",
20
20
  "tailwindcss",
21
21
  "shadcn",
22
- "registry"
22
+ "registry",
23
+ "eslint-plugin"
23
24
  ],
24
25
  "sideEffects": [
25
26
  "*.css"
@@ -33,18 +34,23 @@
33
34
  "types": "./dist/index.d.ts",
34
35
  "import": "./dist/index.js"
35
36
  },
37
+ "./eslint": {
38
+ "types": "./dist/eslint/index.d.ts",
39
+ "import": "./dist/eslint/index.js"
40
+ },
36
41
  "./theme.css": "./dist/theme.css",
37
42
  "./tokens.json": "./dist/tokens.json",
38
43
  "./accents/*.css": "./dist/accents/*.css"
39
44
  },
40
45
  "scripts": {
41
- "build": "tsc && node ../../tools/copy-theme.mjs && node ../../tools/build-tokens-json.mjs && node ../../tools/build-accents.mjs && node ../../tools/build-registry.mjs && node ../../tools/build-stand-theme.mjs",
46
+ "build": "tsc && node ../../tools/copy-theme.mjs && node ../../tools/build-tokens-json.mjs && node ../../tools/build-accents.mjs && node ../../tools/build-registry.mjs",
42
47
  "typecheck": "tsc --noEmit",
43
48
  "prepack": "node ../../tools/copy-readme.mjs"
44
49
  },
45
50
  "peerDependencies": {
46
51
  "react": "^19.0.0",
47
- "react-dom": "^19.0.0"
52
+ "react-dom": "^19.0.0",
53
+ "eslint": "^9.0.0 || ^10.0.0"
48
54
  },
49
55
  "devDependencies": {
50
56
  "@radix-ui/react-slot": "^1.3.3",
@@ -52,6 +58,7 @@
52
58
  "@types/react": "^19.2.18",
53
59
  "@types/react-dom": "^19.2.5",
54
60
  "class-variance-authority": "^0.7.1",
61
+ "eslint": "^10.9.1",
55
62
  "react": "^19.2.8",
56
63
  "react-dom": "^19.2.8"
57
64
  },
@@ -62,5 +69,10 @@
62
69
  "dependencies": {
63
70
  "clsx": "^2.1.1",
64
71
  "tailwind-merge": "^3.6.0"
72
+ },
73
+ "peerDependenciesMeta": {
74
+ "eslint": {
75
+ "optional": true
76
+ }
65
77
  }
66
78
  }