dowel-ui 0.1.0 → 0.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 CHANGED
@@ -8,7 +8,7 @@ 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.1.0 - the theme. Primitives and the registry follow; see the [roadmap](#roadmap).
11
+ **Status:** v0.3.0 - the theme, the scales and an accent per product. Primitives follow; see the [roadmap](#roadmap).
12
12
 
13
13
  ## The theme
14
14
 
@@ -54,8 +54,24 @@ The stock palette is dropped deliberately, so a stray `bg-zinc-800` does not com
54
54
  - **The accent is derived, not configured.** A product states one hue and the theme works out the rest, including what colour text has to be to sit on top of it. A light accent takes dark glyphs, a dark one white - checked against every colour in the line rather than left to each product to get right.
55
55
  - **The greys belong to the product.** They carry a trace of its hue, so the chrome of one product is not the chrome of another with a different button colour.
56
56
  - **Contrast is a test, not an intention.** Every accent of the line is measured against WCAG AA in CI, in both themes, as a fill and as text.
57
+ - **The scales were read, not invented.** Radius, type, elevation and stacking order come from what the line's products already draw, so existing code fits them - and the places where those products disagreed with themselves are settled rather than preserved.
57
58
 
58
- Full vocabulary, with every token shown in both themes: **[the token reference](https://lacodda.github.io/dowel/reference/tokens/)**.
59
+ The same vocabulary also ships as [DTCG](https://www.designtokens.org/tr/2025.10/format/) JSON at `dowel-ui/tokens.json`, generated from the stylesheet so the two cannot drift.
60
+
61
+ A product of the line states one thing about its appearance - which product it is:
62
+
63
+ ```css
64
+ @import 'dowel-ui/theme.css';
65
+ @import 'dowel-ui/accents/kilna.css';
66
+ ```
67
+
68
+ Or copy the files in instead of depending on the package:
69
+
70
+ ```bash
71
+ npx shadcn@latest add https://lacodda.github.io/dowel/r/theme.json
72
+ ```
73
+
74
+ 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.
59
75
 
60
76
  ## Roadmap
61
77
 
@@ -0,0 +1,10 @@
1
+ /*
2
+ * atlas - nebula #8A7DF5, the colour of its mark (at).
3
+ *
4
+ * Import after the theme. Everything else - the hover shade, the soft fill,
5
+ * the focus ring, the colour of text on an accent fill, and the trace of the
6
+ * hue the greys carry - is derived from this one value.
7
+ */
8
+ :root {
9
+ --accent-base: #8A7DF5;
10
+ }
@@ -0,0 +1,10 @@
1
+ /*
2
+ * austeris - orchid #C25BD9, the colour of its mark (au).
3
+ *
4
+ * Import after the theme. Everything else - the hover shade, the soft fill,
5
+ * the focus ring, the colour of text on an accent fill, and the trace of the
6
+ * hue the greys carry - is derived from this one value.
7
+ */
8
+ :root {
9
+ --accent-base: #C25BD9;
10
+ }
@@ -0,0 +1,10 @@
1
+ /*
2
+ * dowel - signal amber #E8862D, the colour of its mark (dw).
3
+ *
4
+ * Import after the theme. Everything else - the hover shade, the soft fill,
5
+ * the focus ring, the colour of text on an accent fill, and the trace of the
6
+ * hue the greys carry - is derived from this one value.
7
+ */
8
+ :root {
9
+ --accent-base: #E8862D;
10
+ }
@@ -0,0 +1,10 @@
1
+ /*
2
+ * efema - cobalt #5470E8, the colour of its mark (ef).
3
+ *
4
+ * Import after the theme. Everything else - the hover shade, the soft fill,
5
+ * the focus ring, the colour of text on an accent fill, and the trace of the
6
+ * hue the greys carry - is derived from this one value.
7
+ */
8
+ :root {
9
+ --accent-base: #5470E8;
10
+ }
@@ -0,0 +1,10 @@
1
+ /*
2
+ * kasl-server - gold #D9A82E, the colour of its mark (ks).
3
+ *
4
+ * Import after the theme. Everything else - the hover shade, the soft fill,
5
+ * the focus ring, the colour of text on an accent fill, and the trace of the
6
+ * hue the greys carry - is derived from this one value.
7
+ */
8
+ :root {
9
+ --accent-base: #D9A82E;
10
+ }
@@ -0,0 +1,10 @@
1
+ /*
2
+ * kasl - lime #A9C23F, the colour of its mark (ka).
3
+ *
4
+ * Import after the theme. Everything else - the hover shade, the soft fill,
5
+ * the focus ring, the colour of text on an accent fill, and the trace of the
6
+ * hue the greys carry - is derived from this one value.
7
+ */
8
+ :root {
9
+ --accent-base: #A9C23F;
10
+ }
@@ -0,0 +1,10 @@
1
+ /*
2
+ * kilna - magenta #D9569E, the colour of its mark (ki).
3
+ *
4
+ * Import after the theme. Everything else - the hover shade, the soft fill,
5
+ * the focus ring, the colour of text on an accent fill, and the trace of the
6
+ * hue the greys carry - is derived from this one value.
7
+ */
8
+ :root {
9
+ --accent-base: #D9569E;
10
+ }
@@ -0,0 +1,10 @@
1
+ /*
2
+ * lyrid - azure #4A8FE8, the colour of its mark (ly).
3
+ *
4
+ * Import after the theme. Everything else - the hover shade, the soft fill,
5
+ * the focus ring, the colour of text on an accent fill, and the trace of the
6
+ * hue the greys carry - is derived from this one value.
7
+ */
8
+ :root {
9
+ --accent-base: #4A8FE8;
10
+ }
@@ -0,0 +1,10 @@
1
+ /*
2
+ * lyrn - indigo #6D7BF2, the colour of its mark (ln).
3
+ *
4
+ * Import after the theme. Everything else - the hover shade, the soft fill,
5
+ * the focus ring, the colour of text on an accent fill, and the trace of the
6
+ * hue the greys carry - is derived from this one value.
7
+ */
8
+ :root {
9
+ --accent-base: #6D7BF2;
10
+ }
@@ -0,0 +1,10 @@
1
+ /*
2
+ * midda - violet #A46BE8, the colour of its mark (mi).
3
+ *
4
+ * Import after the theme. Everything else - the hover shade, the soft fill,
5
+ * the focus ring, the colour of text on an accent fill, and the trace of the
6
+ * hue the greys carry - is derived from this one value.
7
+ */
8
+ :root {
9
+ --accent-base: #A46BE8;
10
+ }
@@ -0,0 +1,10 @@
1
+ /*
2
+ * nitid - cyan #3FA9D9, the colour of its mark (nd).
3
+ *
4
+ * Import after the theme. Everything else - the hover shade, the soft fill,
5
+ * the focus ring, the colour of text on an accent fill, and the trace of the
6
+ * hue the greys carry - is derived from this one value.
7
+ */
8
+ :root {
9
+ --accent-base: #3FA9D9;
10
+ }
@@ -0,0 +1,10 @@
1
+ /*
2
+ * nooma - emerald #3FA873, the colour of its mark (nm).
3
+ *
4
+ * Import after the theme. Everything else - the hover shade, the soft fill,
5
+ * the focus ring, the colour of text on an accent fill, and the trace of the
6
+ * hue the greys carry - is derived from this one value.
7
+ */
8
+ :root {
9
+ --accent-base: #3FA873;
10
+ }
@@ -0,0 +1,10 @@
1
+ /*
2
+ * sefy - teal #35A8A0, the colour of its mark (se).
3
+ *
4
+ * Import after the theme. Everything else - the hover shade, the soft fill,
5
+ * the focus ring, the colour of text on an accent fill, and the trace of the
6
+ * hue the greys carry - is derived from this one value.
7
+ */
8
+ :root {
9
+ --accent-base: #35A8A0;
10
+ }
@@ -0,0 +1,10 @@
1
+ /*
2
+ * turnout - rose #E85B72, the colour of its mark (tn).
3
+ *
4
+ * Import after the theme. Everything else - the hover shade, the soft fill,
5
+ * the focus ring, the colour of text on an accent fill, and the trace of the
6
+ * hue the greys carry - is derived from this one value.
7
+ */
8
+ :root {
9
+ --accent-base: #E85B72;
10
+ }
package/dist/index.d.ts CHANGED
@@ -1,15 +1,41 @@
1
+ export { lineProducts, lineProduct, type LineProduct } from './line';
2
+ export { defaultStorageKey, initTheme, nextTheme, resolvedTheme, useTheme, useThemeSwitch, type Theme, } from './theme';
1
3
  /** Tokens that carry a colour. Ordered as they read on a screen: grounds,
2
4
  * then hairlines, then ink, then the accent, then status. */
3
5
  export declare const colorTokens: readonly ["bg", "raise", "soft", "softer", "line", "line-2", "text", "dim", "faint", "accent", "accent-2", "accent-soft", "on-accent", "good", "good-soft", "warn", "warn-soft", "bad", "bad-soft", "info", "info-soft"];
4
6
  /** Tokens a product overrides to make the theme its own: the accent from the
5
7
  * brand-line registry, and how much of it bleeds into the greys. */
6
8
  export declare const themeParameters: readonly ["accent-base", "neutral-base", "neutral-tint", "neutral-tint-strong", "ground", "ink"];
7
- /** Everything else the theme defines. */
8
- export declare const otherTokens: readonly ["shadow-raise"];
9
+ /** Elevation. Three steps, and they change with the theme: a shadow that
10
+ * works on a dark ground is invisible on a light one. */
11
+ export declare const elevationTokens: readonly ["shadow-lift", "shadow-raise", "shadow-float"];
12
+ /** Corner radius. `md` is the control radius - inputs, buttons, list rows;
13
+ * `inner` is what a shape nested inside another one takes. */
14
+ export declare const radiusTokens: readonly ["radius-xs", "radius-sm", "radius-md", "radius-lg", "radius-xl", "radius-2xl", "radius-inner"];
15
+ /** Type: sizes with their line heights, weights, and the two tracking steps
16
+ * the products actually need. */
17
+ export declare const typeTokens: readonly ["font-sans", "font-mono", "text-2xs", "text-xs", "text-sm", "text-base", "text-lg", "text-xl", "text-2xl", "font-weight-normal", "font-weight-medium", "font-weight-semibold", "tracking-caption", "tracking-tight"];
18
+ /** Motion. The durations are read directly rather than through a utility -
19
+ * Tailwind's `duration-*` takes a literal number - while the easings are a
20
+ * namespace, so `ease-out` is a class. */
21
+ export declare const motionTokens: readonly ["duration-quick", "duration-base", "duration-slow", "ease-out", "ease-in-out"];
22
+ /** Stacking order. Not a Tailwind namespace either: a component reads these
23
+ * as `z-index: var(--z-modal)`. The names are a promise about what covers
24
+ * what, and the values only mean anything relative to each other. */
25
+ export declare const layerTokens: readonly ["z-popup", "z-sticky", "z-menu", "z-floating", "z-overlay", "z-modal", "z-palette", "z-toast"];
26
+ /** Every token the theme defines, in one list. Anything that iterates the
27
+ * vocabulary - a docs page, an inspector, the JSON export, the test that keeps
28
+ * this file honest against the stylesheet - reads this, so a new category
29
+ * cannot be added and quietly missed by half of them. */
30
+ export declare const allTokens: readonly ["bg", "raise", "soft", "softer", "line", "line-2", "text", "dim", "faint", "accent", "accent-2", "accent-soft", "on-accent", "good", "good-soft", "warn", "warn-soft", "bad", "bad-soft", "info", "info-soft", "accent-base", "neutral-base", "neutral-tint", "neutral-tint-strong", "ground", "ink", "shadow-lift", "shadow-raise", "shadow-float", "radius-xs", "radius-sm", "radius-md", "radius-lg", "radius-xl", "radius-2xl", "radius-inner", "font-sans", "font-mono", "text-2xs", "text-xs", "text-sm", "text-base", "text-lg", "text-xl", "text-2xl", "font-weight-normal", "font-weight-medium", "font-weight-semibold", "tracking-caption", "tracking-tight", "duration-quick", "duration-base", "duration-slow", "ease-out", "ease-in-out", "z-popup", "z-sticky", "z-menu", "z-floating", "z-overlay", "z-modal", "z-palette", "z-toast"];
9
31
  export type ColorToken = (typeof colorTokens)[number];
10
32
  export type ThemeParameter = (typeof themeParameters)[number];
11
- export type OtherToken = (typeof otherTokens)[number];
12
- export type Token = ColorToken | ThemeParameter | OtherToken;
33
+ export type ElevationToken = (typeof elevationTokens)[number];
34
+ export type RadiusToken = (typeof radiusTokens)[number];
35
+ export type TypeToken = (typeof typeTokens)[number];
36
+ export type MotionToken = (typeof motionTokens)[number];
37
+ export type LayerToken = (typeof layerTokens)[number];
38
+ export type Token = ColorToken | ThemeParameter | ElevationToken | RadiusToken | TypeToken | MotionToken | LayerToken;
13
39
  /** The custom property a token is read from: `token('accent')` is
14
40
  * `'--accent'`. Spelled out here so that no caller builds the string itself
15
41
  * and gets the prefix subtly wrong. */
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAiBA;6DAC6D;AAC7D,eAAO,MAAM,WAAW,0NAsBd,CAAA;AAEV;oEACoE;AACpE,eAAO,MAAM,eAAe,kGAOlB,CAAA;AAEV,yCAAyC;AACzC,eAAO,MAAM,WAAW,2BAA4B,CAAA;AAEpD,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAA;AACrD,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAA;AAC7D,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAA;AACrD,MAAM,MAAM,KAAK,GAAG,UAAU,GAAG,cAAc,GAAG,UAAU,CAAA;AAE5D;;uCAEuC;AACvC,wBAAgB,KAAK,CAAC,IAAI,EAAE,KAAK,GAAG,MAAM,CAEzC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,KAAK,WAAW,EAAE,MAAM,QAAQ,CAAA;AACpE,OAAO,EACL,iBAAiB,EACjB,SAAS,EACT,SAAS,EACT,aAAa,EACb,QAAQ,EACR,cAAc,EACd,KAAK,KAAK,GACX,MAAM,SAAS,CAAA;AAEhB;6DAC6D;AAC7D,eAAO,MAAM,WAAW,0NAsBd,CAAA;AAEV;oEACoE;AACpE,eAAO,MAAM,eAAe,kGAOlB,CAAA;AAEV;yDACyD;AACzD,eAAO,MAAM,eAAe,0DAA2D,CAAA;AAEvF;8DAC8D;AAC9D,eAAO,MAAM,YAAY,0GAQf,CAAA;AAEV;iCACiC;AACjC,eAAO,MAAM,UAAU,gOAeb,CAAA;AAEV;;0CAE0C;AAC1C,eAAO,MAAM,YAAY,0FAMf,CAAA;AAEV;;qEAEqE;AACrE,eAAO,MAAM,WAAW,0GASd,CAAA;AAEV;;;yDAGyD;AACzD,eAAO,MAAM,SAAS,k0BAQZ,CAAA;AAEV,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAA;AACrD,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAA;AAC7D,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAA;AAC7D,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAA;AACvD,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,CAAC,CAAA;AACnD,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAA;AACvD,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAA;AACrD,MAAM,MAAM,KAAK,GACb,UAAU,GACV,cAAc,GACd,cAAc,GACd,WAAW,GACX,SAAS,GACT,WAAW,GACX,UAAU,CAAA;AAEd;;uCAEuC;AACvC,wBAAgB,KAAK,CAAC,IAAI,EAAE,KAAK,GAAG,MAAM,CAEzC"}
package/dist/index.js CHANGED
@@ -14,6 +14,8 @@
14
14
  * Primitives are distributed through the registry, not from here: a component
15
15
  * is copied into the product and becomes the product's own code.
16
16
  */
17
+ export { lineProducts, lineProduct } from './line';
18
+ export { defaultStorageKey, initTheme, nextTheme, resolvedTheme, useTheme, useThemeSwitch, } from './theme';
17
19
  /** Tokens that carry a colour. Ordered as they read on a screen: grounds,
18
20
  * then hairlines, then ink, then the accent, then status. */
19
21
  export const colorTokens = [
@@ -49,8 +51,74 @@ export const themeParameters = [
49
51
  'ground',
50
52
  'ink',
51
53
  ];
52
- /** Everything else the theme defines. */
53
- export const otherTokens = ['shadow-raise'];
54
+ /** Elevation. Three steps, and they change with the theme: a shadow that
55
+ * works on a dark ground is invisible on a light one. */
56
+ export const elevationTokens = ['shadow-lift', 'shadow-raise', 'shadow-float'];
57
+ /** Corner radius. `md` is the control radius - inputs, buttons, list rows;
58
+ * `inner` is what a shape nested inside another one takes. */
59
+ export const radiusTokens = [
60
+ 'radius-xs',
61
+ 'radius-sm',
62
+ 'radius-md',
63
+ 'radius-lg',
64
+ 'radius-xl',
65
+ 'radius-2xl',
66
+ 'radius-inner',
67
+ ];
68
+ /** Type: sizes with their line heights, weights, and the two tracking steps
69
+ * the products actually need. */
70
+ export const typeTokens = [
71
+ 'font-sans',
72
+ 'font-mono',
73
+ 'text-2xs',
74
+ 'text-xs',
75
+ 'text-sm',
76
+ 'text-base',
77
+ 'text-lg',
78
+ 'text-xl',
79
+ 'text-2xl',
80
+ 'font-weight-normal',
81
+ 'font-weight-medium',
82
+ 'font-weight-semibold',
83
+ 'tracking-caption',
84
+ 'tracking-tight',
85
+ ];
86
+ /** Motion. The durations are read directly rather than through a utility -
87
+ * Tailwind's `duration-*` takes a literal number - while the easings are a
88
+ * namespace, so `ease-out` is a class. */
89
+ export const motionTokens = [
90
+ 'duration-quick',
91
+ 'duration-base',
92
+ 'duration-slow',
93
+ 'ease-out',
94
+ 'ease-in-out',
95
+ ];
96
+ /** Stacking order. Not a Tailwind namespace either: a component reads these
97
+ * as `z-index: var(--z-modal)`. The names are a promise about what covers
98
+ * what, and the values only mean anything relative to each other. */
99
+ export const layerTokens = [
100
+ 'z-popup',
101
+ 'z-sticky',
102
+ 'z-menu',
103
+ 'z-floating',
104
+ 'z-overlay',
105
+ 'z-modal',
106
+ 'z-palette',
107
+ 'z-toast',
108
+ ];
109
+ /** Every token the theme defines, in one list. Anything that iterates the
110
+ * vocabulary - a docs page, an inspector, the JSON export, the test that keeps
111
+ * this file honest against the stylesheet - reads this, so a new category
112
+ * cannot be added and quietly missed by half of them. */
113
+ export const allTokens = [
114
+ ...colorTokens,
115
+ ...themeParameters,
116
+ ...elevationTokens,
117
+ ...radiusTokens,
118
+ ...typeTokens,
119
+ ...motionTokens,
120
+ ...layerTokens,
121
+ ];
54
122
  /** The custom property a token is read from: `token('accent')` is
55
123
  * `'--accent'`. Spelled out here so that no caller builds the string itself
56
124
  * and gets the prefix subtly wrong. */
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH;6DAC6D;AAC7D,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,IAAI;IACJ,OAAO;IACP,MAAM;IACN,QAAQ;IACR,MAAM;IACN,QAAQ;IACR,MAAM;IACN,KAAK;IACL,OAAO;IACP,QAAQ;IACR,UAAU;IACV,aAAa;IACb,WAAW;IACX,MAAM;IACN,WAAW;IACX,MAAM;IACN,WAAW;IACX,KAAK;IACL,UAAU;IACV,MAAM;IACN,WAAW;CACH,CAAA;AAEV;oEACoE;AACpE,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,aAAa;IACb,cAAc;IACd,cAAc;IACd,qBAAqB;IACrB,QAAQ;IACR,KAAK;CACG,CAAA;AAEV,yCAAyC;AACzC,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,cAAc,CAAU,CAAA;AAOpD;;uCAEuC;AACvC,MAAM,UAAU,KAAK,CAAC,IAAW;IAC/B,OAAO,KAAK,IAAI,EAAE,CAAA;AACpB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,YAAY,EAAE,WAAW,EAAoB,MAAM,QAAQ,CAAA;AACpE,OAAO,EACL,iBAAiB,EACjB,SAAS,EACT,SAAS,EACT,aAAa,EACb,QAAQ,EACR,cAAc,GAEf,MAAM,SAAS,CAAA;AAEhB;6DAC6D;AAC7D,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,IAAI;IACJ,OAAO;IACP,MAAM;IACN,QAAQ;IACR,MAAM;IACN,QAAQ;IACR,MAAM;IACN,KAAK;IACL,OAAO;IACP,QAAQ;IACR,UAAU;IACV,aAAa;IACb,WAAW;IACX,MAAM;IACN,WAAW;IACX,MAAM;IACN,WAAW;IACX,KAAK;IACL,UAAU;IACV,MAAM;IACN,WAAW;CACH,CAAA;AAEV;oEACoE;AACpE,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,aAAa;IACb,cAAc;IACd,cAAc;IACd,qBAAqB;IACrB,QAAQ;IACR,KAAK;CACG,CAAA;AAEV;yDACyD;AACzD,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,aAAa,EAAE,cAAc,EAAE,cAAc,CAAU,CAAA;AAEvF;8DAC8D;AAC9D,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,WAAW;IACX,WAAW;IACX,WAAW;IACX,WAAW;IACX,WAAW;IACX,YAAY;IACZ,cAAc;CACN,CAAA;AAEV;iCACiC;AACjC,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,WAAW;IACX,WAAW;IACX,UAAU;IACV,SAAS;IACT,SAAS;IACT,WAAW;IACX,SAAS;IACT,SAAS;IACT,UAAU;IACV,oBAAoB;IACpB,oBAAoB;IACpB,sBAAsB;IACtB,kBAAkB;IAClB,gBAAgB;CACR,CAAA;AAEV;;0CAE0C;AAC1C,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,gBAAgB;IAChB,eAAe;IACf,eAAe;IACf,UAAU;IACV,aAAa;CACL,CAAA;AAEV;;qEAEqE;AACrE,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,SAAS;IACT,UAAU;IACV,QAAQ;IACR,YAAY;IACZ,WAAW;IACX,SAAS;IACT,WAAW;IACX,SAAS;CACD,CAAA;AAEV;;;yDAGyD;AACzD,MAAM,CAAC,MAAM,SAAS,GAAG;IACvB,GAAG,WAAW;IACd,GAAG,eAAe;IAClB,GAAG,eAAe;IAClB,GAAG,YAAY;IACf,GAAG,UAAU;IACb,GAAG,YAAY;IACf,GAAG,WAAW;CACN,CAAA;AAkBV;;uCAEuC;AACvC,MAAM,UAAU,KAAK,CAAC,IAAW;IAC/B,OAAO,KAAK,IAAI,EAAE,CAAA;AACpB,CAAC"}
package/dist/line.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ export interface LineProduct {
2
+ /** Package-and-directory name, and the name of the accent file. */
3
+ readonly name: string;
4
+ /** The two-letter code on the product's mark. */
5
+ readonly code: string;
6
+ /** The accent, as the registry states it. */
7
+ readonly accent: string;
8
+ /** The colour's name in the line's palette. */
9
+ readonly colorName: string;
10
+ }
11
+ export declare const lineProducts: readonly LineProduct[];
12
+ /** The product with this name, or `undefined`. */
13
+ export declare function lineProduct(name: string): LineProduct | undefined;
14
+ //# sourceMappingURL=line.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"line.d.ts","sourceRoot":"","sources":["../src/line.ts"],"names":[],"mappings":"AAeA,MAAM,WAAW,WAAW;IAC1B,mEAAmE;IACnE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,iDAAiD;IACjD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,6CAA6C;IAC7C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,+CAA+C;IAC/C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AAED,eAAO,MAAM,YAAY,EAAE,SAAS,WAAW,EAe9C,CAAA;AAED,kDAAkD;AAClD,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAEjE"}
package/dist/line.js ADDED
@@ -0,0 +1,35 @@
1
+ /*
2
+ * The products of the lacodda line, and the colour each one is known by.
3
+ *
4
+ * These are the accents from the line's registry of marks: one hue per
5
+ * product, fixed for the life of that product. A product's theme is its mark's
6
+ * colour - the tile on its README, the icon in the taskbar and the primary
7
+ * button on its screens are the same colour, because they are the same fact.
8
+ *
9
+ * The registry itself lives with the mark generator, outside this repository.
10
+ * It is copied here rather than read, because a build must not reach into a
11
+ * private toolchain to know what colour a button is; the release gate checks
12
+ * that every product listed has an accent file, and the contrast tests run
13
+ * over this list, so a wrong value cannot travel far.
14
+ */
15
+ export const lineProducts = [
16
+ { name: 'kasl', code: 'ka', accent: '#A9C23F', colorName: 'lime' },
17
+ { name: 'kasl-server', code: 'ks', accent: '#D9A82E', colorName: 'gold' },
18
+ { name: 'turnout', code: 'tn', accent: '#E85B72', colorName: 'rose' },
19
+ { name: 'sefy', code: 'se', accent: '#35A8A0', colorName: 'teal' },
20
+ { name: 'atlas', code: 'at', accent: '#8A7DF5', colorName: 'nebula' },
21
+ { name: 'nitid', code: 'nd', accent: '#3FA9D9', colorName: 'cyan' },
22
+ { name: 'midda', code: 'mi', accent: '#A46BE8', colorName: 'violet' },
23
+ { name: 'nooma', code: 'nm', accent: '#3FA873', colorName: 'emerald' },
24
+ { name: 'kilna', code: 'ki', accent: '#D9569E', colorName: 'magenta' },
25
+ { name: 'lyrid', code: 'ly', accent: '#4A8FE8', colorName: 'azure' },
26
+ { name: 'efema', code: 'ef', accent: '#5470E8', colorName: 'cobalt' },
27
+ { name: 'dowel', code: 'dw', accent: '#E8862D', colorName: 'signal amber' },
28
+ { name: 'lyrn', code: 'ln', accent: '#6D7BF2', colorName: 'indigo' },
29
+ { name: 'austeris', code: 'au', accent: '#C25BD9', colorName: 'orchid' },
30
+ ];
31
+ /** The product with this name, or `undefined`. */
32
+ export function lineProduct(name) {
33
+ return lineProducts.find((product) => product.name === name);
34
+ }
35
+ //# sourceMappingURL=line.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"line.js","sourceRoot":"","sources":["../src/line.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAaH,MAAM,CAAC,MAAM,YAAY,GAA2B;IAClD,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE;IAClE,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE;IACzE,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE;IACrE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE;IAClE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE;IACrE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE;IACnE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE;IACrE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE;IACtE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE;IACtE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE;IACpE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE;IACrE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,cAAc,EAAE;IAC3E,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE;IACpE,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE;CACzE,CAAA;AAED,kDAAkD;AAClD,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,OAAO,YAAY,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,KAAK,IAAI,CAAC,CAAA;AAC9D,CAAC"}
package/dist/theme.css CHANGED
@@ -80,7 +80,14 @@
80
80
  --bad-soft: color-mix(in oklab, var(--bad) 14%, transparent);
81
81
  --info-soft: color-mix(in oklab, var(--info) 15%, transparent);
82
82
 
83
+ /* Elevation, three steps. The products had one shadow and used it for
84
+ * everything that leaves the flow - a toast, a dropdown and a modal all
85
+ * floated by the same amount, so a modal never felt further away than the
86
+ * menu it covered. `raise` keeps its original value, so nothing shifts under
87
+ * the products already using it; the other two are the steps either side. */
88
+ --shadow-lift: 0 2px 8px rgb(0 0 0 / 0.3);
83
89
  --shadow-raise: 0 10px 34px rgb(0 0 0 / 0.45);
90
+ --shadow-float: 0 24px 60px rgb(0 0 0 / 0.55);
84
91
  }
85
92
 
86
93
  /*
@@ -125,7 +132,9 @@
125
132
  --bad-soft: color-mix(in oklab, var(--bad) 12%, transparent);
126
133
  --info-soft: color-mix(in oklab, var(--info) 11%, transparent);
127
134
 
135
+ --shadow-lift: 0 2px 8px rgb(30 24 38 / 0.08);
128
136
  --shadow-raise: 0 10px 30px rgb(30 24 38 / 0.14);
137
+ --shadow-float: 0 24px 60px rgb(30 24 38 / 0.18);
129
138
  }
130
139
  }
131
140
 
@@ -161,7 +170,9 @@
161
170
  --bad-soft: color-mix(in oklab, var(--bad) 12%, transparent);
162
171
  --info-soft: color-mix(in oklab, var(--info) 11%, transparent);
163
172
 
173
+ --shadow-lift: 0 2px 8px rgb(30 24 38 / 0.08);
164
174
  --shadow-raise: 0 10px 30px rgb(30 24 38 / 0.14);
175
+ --shadow-float: 0 24px 60px rgb(30 24 38 / 0.18);
165
176
  }
166
177
 
167
178
  /*
@@ -236,10 +247,131 @@
236
247
  --color-white: #fff;
237
248
  --color-black: #000;
238
249
 
250
+ /*
251
+ * Type. System stacks on purpose: a downloaded face costs a network round
252
+ * trip before the first word appears, and the line's products are desktop
253
+ * tools where the operating system's own face is the one the user already
254
+ * reads everything else in.
255
+ */
239
256
  --font-sans: 'Segoe UI Variable Text', 'Segoe UI', system-ui, -apple-system, sans-serif;
240
257
  --font-mono: ui-monospace, 'Cascadia Code', 'SF Mono', Consolas, monospace;
241
258
 
259
+ /*
260
+ * Radius. Taken from what the products actually draw, not from a ratio:
261
+ * `rounded-[9px]` appears twenty times and `rounded-[10px]` twelve, because
262
+ * a control and the primary button were tuned by eye and then copied. The
263
+ * scale keeps the cluster they landed in and gives it names.
264
+ *
265
+ * `md` is the control radius - inputs, buttons, list rows. That the primary
266
+ * button was one pixel rounder than every other variant is not preserved:
267
+ * the products differ from themselves there, and buttons of the same size
268
+ * sitting side by side should not have mismatched corners.
269
+ */
270
+ --radius-xs: 4px;
271
+ --radius-sm: 6px;
272
+ --radius-md: 9px;
273
+ --radius-lg: 12px;
274
+ --radius-xl: 16px;
275
+ --radius-2xl: 20px;
276
+
277
+ /*
278
+ * A radius nested inside another has to be smaller by the gap between them,
279
+ * or the inner corner looks wrong against the outer one. The products did
280
+ * this by hand once - 18px outside, 17px inside - and nowhere else.
281
+ */
282
+ --radius-inner: calc(var(--radius-lg) - 1px);
283
+
284
+ /*
285
+ * Type scale. The products live between 10px and 14px: `text-sm` and
286
+ * `text-xs` together account for nine tenths of every size in both, and the
287
+ * rest scattered across 9, 9.5, 10, 10.5, 11, 11.5, 12.5 and 13 - nine steps
288
+ * inside four pixels, which no eye distinguishes and no reason justifies.
289
+ * This is the same range with the noise removed.
290
+ */
291
+ --text-2xs: 10px;
292
+ --text-2xs--line-height: 14px;
293
+ --text-xs: 11px;
294
+ --text-xs--line-height: 15px;
295
+ --text-sm: 12px;
296
+ --text-sm--line-height: 16px;
297
+ --text-base: 14px;
298
+ --text-base--line-height: 20px;
299
+ --text-lg: 16px;
300
+ --text-lg--line-height: 22px;
301
+ --text-xl: 18px;
302
+ --text-xl--line-height: 24px;
303
+ --text-2xl: 21px;
304
+ --text-2xl--line-height: 28px;
305
+
306
+ /* Weights. `semibold` is what both products use for anything emphasised;
307
+ * `bold` appears in neither, and the one `font-[650]` in a page title is the
308
+ * kind of value a scale exists to absorb. */
309
+ --font-weight-normal: 400;
310
+ --font-weight-medium: 500;
311
+ --font-weight-semibold: 600;
312
+
313
+ /* Tracking. The uppercase caption is the only place the products track at
314
+ * all - and they do it at 0.08em in six files and 0.09em in three, a
315
+ * difference nobody can see. One name settles it. */
316
+ --tracking-caption: 0.085em;
317
+ --tracking-tight: -0.01em;
318
+
319
+ /* Easing. `out` for anything the user asked for - it arrives fast and
320
+ * settles, which reads as responsive. `in-out` for something moving on its
321
+ * own. `in` is deliberately absent: it starts slowly, which on a control
322
+ * reads as lag. */
323
+ --ease-out: cubic-bezier(0.2, 0, 0, 1);
324
+ --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
325
+
326
+ /*
327
+ * Elevation. Three steps, because the products had one and used it for a
328
+ * toast, a dropdown and a modal alike - so a modal never sat further from
329
+ * the page than the menu it covered.
330
+ */
331
+ --shadow-lift: var(--shadow-lift);
242
332
  --shadow-raise: var(--shadow-raise);
333
+ --shadow-float: var(--shadow-float);
334
+ }
335
+
336
+ /*
337
+ * Stacking order.
338
+ *
339
+ * Not in `@theme`: Tailwind has no z-index namespace, so `z-50` is a literal
340
+ * fifty and a named step would not compile. These are custom properties a
341
+ * component reads directly - `z-index: var(--z-modal)`.
342
+ *
343
+ * The order is the products' own, with the gaps closed. They ran 10 for an
344
+ * in-flow popup, 20 sticky, 30 menu, 40 for a floating button, 50 for modals
345
+ * and drawers, then jumped to 70 and 80 for the command palette - which had to
346
+ * clear the modal layer and had no name to do it with.
347
+ */
348
+ :root {
349
+ /*
350
+ * Motion. One duration existed before this - 160ms on a route change - and
351
+ * everything else rode Tailwind's default. These are the steps around it:
352
+ * `quick` for a colour or an opacity that should feel immediate, `base` for
353
+ * something that moves, `slow` for something arriving from off-screen.
354
+ *
355
+ * Not in `@theme`: Tailwind's `duration-*` utility takes a literal number,
356
+ * not a named step, so these are read directly - `transition-duration:
357
+ * var(--duration-base)`. The easing curves opposite them ARE a namespace,
358
+ * so `ease-out` is a class.
359
+ *
360
+ * Every duration here is for people who want motion: `prefers-reduced-
361
+ * motion` cuts them to nothing further down.
362
+ */
363
+ --duration-quick: 120ms;
364
+ --duration-base: 160ms;
365
+ --duration-slow: 240ms;
366
+
367
+ --z-popup: 10;
368
+ --z-sticky: 20;
369
+ --z-menu: 30;
370
+ --z-floating: 40;
371
+ --z-overlay: 50;
372
+ --z-modal: 60;
373
+ --z-palette: 70;
374
+ --z-toast: 80;
243
375
  }
244
376
 
245
377
  /*
@@ -0,0 +1,67 @@
1
+ /** `system` follows the operating system; the other two pin a class. */
2
+ export type Theme = 'system' | 'light' | 'dark';
3
+ /** Where the choice is remembered. A product may use its own key; the default
4
+ * is shared, so two products of the line on the same origin agree. */
5
+ export declare const defaultStorageKey = "dowel.theme";
6
+ /**
7
+ * Apply the remembered theme immediately, before React renders.
8
+ *
9
+ * Call this from the entry module, above the first render. Without it the page
10
+ * paints in the default theme and then corrects itself, which a reader who
11
+ * chose light sees as a flash of dark.
12
+ */
13
+ export declare function initTheme(storageKey?: string): void;
14
+ /**
15
+ * The current theme and a way to change it.
16
+ *
17
+ * While the theme is `system` this follows the operating system as it changes,
18
+ * so a reader who switches their machine to dark at sunset does not have to
19
+ * reload the page.
20
+ */
21
+ export declare function useTheme(storageKey?: string): {
22
+ theme: Theme;
23
+ /** Which theme is showing: `system` resolved against the operating system. */
24
+ resolved: 'light' | 'dark';
25
+ setTheme: (theme: Theme) => void;
26
+ };
27
+ /** The next theme in a fixed cycle, for a control that is one button rather
28
+ * than three. */
29
+ export declare function nextTheme(current: Theme): Theme;
30
+ /**
31
+ * A theme switch, as behaviour without markup.
32
+ *
33
+ * The control itself is the product's: kilna's is a row in its sidebar with an
34
+ * icon and a translated label, and a design system that shipped that would be
35
+ * shipping kilna's sidebar. What is worth sharing is the part every product
36
+ * would otherwise get subtly wrong - the cycle order, the accessible state,
37
+ * and telling assistive technology that a button changes the page's own
38
+ * appearance.
39
+ *
40
+ * const { theme, buttonProps } = useThemeSwitch()
41
+ * <button {...buttonProps} aria-label={t(`theme.${theme}`)}>
42
+ * <Icon /> {t(`theme.${theme}`)}
43
+ * </button>
44
+ *
45
+ * The label stays with the product: a primitive with a string of its own is a
46
+ * primitive that cannot be translated.
47
+ */
48
+ export declare function useThemeSwitch(storageKey?: string): {
49
+ theme: Theme;
50
+ resolved: 'light' | 'dark';
51
+ setTheme: (theme: Theme) => void;
52
+ /** Spread onto the button that cycles the theme. */
53
+ buttonProps: {
54
+ type: 'button';
55
+ onClick: () => void;
56
+ 'aria-live': 'polite';
57
+ };
58
+ };
59
+ /**
60
+ * Which theme is showing, right now, outside React.
61
+ *
62
+ * `useTheme` returns the same thing as `resolved` and keeps it current; this
63
+ * is for the places a hook cannot reach - a canvas that has to pick a colour,
64
+ * a chart library told what palette to draw in.
65
+ */
66
+ export declare function resolvedTheme(theme: Theme): 'light' | 'dark';
67
+ //# sourceMappingURL=theme.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../src/theme.ts"],"names":[],"mappings":"AAiBA,wEAAwE;AACxE,MAAM,MAAM,KAAK,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,CAAA;AAI/C;sEACsE;AACtE,eAAO,MAAM,iBAAiB,gBAAgB,CAAA;AAiC9C;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,UAAU,GAAE,MAA0B,GAAG,IAAI,CAEtE;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,UAAU,GAAE,MAA0B,GAAG;IAChE,KAAK,EAAE,KAAK,CAAA;IACZ,8EAA8E;IAC9E,QAAQ,EAAE,OAAO,GAAG,MAAM,CAAA;IAC1B,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;CACjC,CA8BA;AAED;iBACiB;AACjB,wBAAgB,SAAS,CAAC,OAAO,EAAE,KAAK,GAAG,KAAK,CAG/C;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,CAAC,UAAU,GAAE,MAA0B,GAAG;IACtE,KAAK,EAAE,KAAK,CAAA;IACZ,QAAQ,EAAE,OAAO,GAAG,MAAM,CAAA;IAC1B,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;IAChC,oDAAoD;IACpD,WAAW,EAAE;QACX,IAAI,EAAE,QAAQ,CAAA;QACd,OAAO,EAAE,MAAM,IAAI,CAAA;QACnB,WAAW,EAAE,QAAQ,CAAA;KACtB,CAAA;CACF,CAiBA;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,GAAG,MAAM,CAG5D"}
package/dist/theme.js ADDED
@@ -0,0 +1,127 @@
1
+ import { useCallback, useEffect, useState } from 'react';
2
+ const THEMES = ['system', 'light', 'dark'];
3
+ /** Where the choice is remembered. A product may use its own key; the default
4
+ * is shared, so two products of the line on the same origin agree. */
5
+ export const defaultStorageKey = 'dowel.theme';
6
+ /** Storage is a convenience, never a requirement: private windows, embedded
7
+ * views and blocked site data all make it throw, and a theme switch that
8
+ * cannot remember should still switch. */
9
+ function readStored(key) {
10
+ try {
11
+ const raw = localStorage.getItem(key);
12
+ return THEMES.includes(raw) ? raw : null;
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ }
18
+ function writeStored(key, theme) {
19
+ try {
20
+ localStorage.setItem(key, theme);
21
+ }
22
+ catch {
23
+ // Nothing to do: the choice applies to this page, it just will not survive.
24
+ }
25
+ }
26
+ function applyTheme(theme) {
27
+ const root = document.documentElement;
28
+ root.classList.toggle('light', theme === 'light');
29
+ root.classList.toggle('dark', theme === 'dark');
30
+ }
31
+ /** Whether the operating system is asking for a dark theme right now. */
32
+ function prefersDark() {
33
+ return typeof window !== 'undefined' && window.matchMedia('(prefers-color-scheme: dark)').matches;
34
+ }
35
+ /**
36
+ * Apply the remembered theme immediately, before React renders.
37
+ *
38
+ * Call this from the entry module, above the first render. Without it the page
39
+ * paints in the default theme and then corrects itself, which a reader who
40
+ * chose light sees as a flash of dark.
41
+ */
42
+ export function initTheme(storageKey = defaultStorageKey) {
43
+ applyTheme(readStored(storageKey) ?? 'system');
44
+ }
45
+ /**
46
+ * The current theme and a way to change it.
47
+ *
48
+ * While the theme is `system` this follows the operating system as it changes,
49
+ * so a reader who switches their machine to dark at sunset does not have to
50
+ * reload the page.
51
+ */
52
+ export function useTheme(storageKey = defaultStorageKey) {
53
+ const [theme, setThemeState] = useState(() => readStored(storageKey) ?? 'system');
54
+ const [systemIsDark, setSystemIsDark] = useState(prefersDark);
55
+ useEffect(() => {
56
+ applyTheme(theme);
57
+ }, [theme]);
58
+ useEffect(() => {
59
+ // The stylesheet follows the operating system on its own - that is what
60
+ // the `prefers-color-scheme` block is for, and the classes are already
61
+ // right. What has to be tracked in React is `resolved`, because a control
62
+ // that draws a sun or a moon needs to know which one is showing.
63
+ const query = window.matchMedia('(prefers-color-scheme: dark)');
64
+ const onChange = (event) => setSystemIsDark(event.matches);
65
+ query.addEventListener('change', onChange);
66
+ return () => query.removeEventListener('change', onChange);
67
+ }, []);
68
+ const setTheme = useCallback((next) => {
69
+ writeStored(storageKey, next);
70
+ setThemeState(next);
71
+ }, [storageKey]);
72
+ const resolved = theme === 'system' ? (systemIsDark ? 'dark' : 'light') : theme;
73
+ return { theme, resolved, setTheme };
74
+ }
75
+ /** The next theme in a fixed cycle, for a control that is one button rather
76
+ * than three. */
77
+ export function nextTheme(current) {
78
+ const index = THEMES.indexOf(current);
79
+ return THEMES[(index + 1) % THEMES.length] ?? 'system';
80
+ }
81
+ /**
82
+ * A theme switch, as behaviour without markup.
83
+ *
84
+ * The control itself is the product's: kilna's is a row in its sidebar with an
85
+ * icon and a translated label, and a design system that shipped that would be
86
+ * shipping kilna's sidebar. What is worth sharing is the part every product
87
+ * would otherwise get subtly wrong - the cycle order, the accessible state,
88
+ * and telling assistive technology that a button changes the page's own
89
+ * appearance.
90
+ *
91
+ * const { theme, buttonProps } = useThemeSwitch()
92
+ * <button {...buttonProps} aria-label={t(`theme.${theme}`)}>
93
+ * <Icon /> {t(`theme.${theme}`)}
94
+ * </button>
95
+ *
96
+ * The label stays with the product: a primitive with a string of its own is a
97
+ * primitive that cannot be translated.
98
+ */
99
+ export function useThemeSwitch(storageKey = defaultStorageKey) {
100
+ const { theme, resolved, setTheme } = useTheme(storageKey);
101
+ const onClick = useCallback(() => setTheme(nextTheme(theme)), [theme, setTheme]);
102
+ return {
103
+ theme,
104
+ resolved,
105
+ setTheme,
106
+ buttonProps: {
107
+ type: 'button',
108
+ onClick,
109
+ // The button's own label changes when it is pressed - it names the
110
+ // theme now in force - and a screen reader should hear that.
111
+ 'aria-live': 'polite',
112
+ },
113
+ };
114
+ }
115
+ /**
116
+ * Which theme is showing, right now, outside React.
117
+ *
118
+ * `useTheme` returns the same thing as `resolved` and keeps it current; this
119
+ * is for the places a hook cannot reach - a canvas that has to pick a colour,
120
+ * a chart library told what palette to draw in.
121
+ */
122
+ export function resolvedTheme(theme) {
123
+ if (theme !== 'system')
124
+ return theme;
125
+ return prefersDark() ? 'dark' : 'light';
126
+ }
127
+ //# sourceMappingURL=theme.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme.js","sourceRoot":"","sources":["../src/theme.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,233 @@
1
+ {
2
+ "$description": "The dowel token vocabulary: the scales every product of the lacodda line is drawn on. Colours are omitted - they are derived per product from its own accent.",
3
+ "radius": {
4
+ "$type": "dimension",
5
+ "$description": "Corner radius. `md` is the control radius: inputs, buttons, list rows.",
6
+ "xs": {
7
+ "$value": {
8
+ "value": 4,
9
+ "unit": "px"
10
+ }
11
+ },
12
+ "sm": {
13
+ "$value": {
14
+ "value": 6,
15
+ "unit": "px"
16
+ }
17
+ },
18
+ "md": {
19
+ "$value": {
20
+ "value": 9,
21
+ "unit": "px"
22
+ }
23
+ },
24
+ "lg": {
25
+ "$value": {
26
+ "value": 12,
27
+ "unit": "px"
28
+ }
29
+ },
30
+ "xl": {
31
+ "$value": {
32
+ "value": 16,
33
+ "unit": "px"
34
+ }
35
+ },
36
+ "2xl": {
37
+ "$value": {
38
+ "value": 20,
39
+ "unit": "px"
40
+ }
41
+ }
42
+ },
43
+ "typography": {
44
+ "$type": "typography",
45
+ "$description": "Type steps, each with the line height it is set on.",
46
+ "2xs": {
47
+ "$value": {
48
+ "fontSize": {
49
+ "value": 10,
50
+ "unit": "px"
51
+ },
52
+ "lineHeight": {
53
+ "value": 14,
54
+ "unit": "px"
55
+ }
56
+ }
57
+ },
58
+ "xs": {
59
+ "$value": {
60
+ "fontSize": {
61
+ "value": 11,
62
+ "unit": "px"
63
+ },
64
+ "lineHeight": {
65
+ "value": 15,
66
+ "unit": "px"
67
+ }
68
+ }
69
+ },
70
+ "sm": {
71
+ "$value": {
72
+ "fontSize": {
73
+ "value": 12,
74
+ "unit": "px"
75
+ },
76
+ "lineHeight": {
77
+ "value": 16,
78
+ "unit": "px"
79
+ }
80
+ }
81
+ },
82
+ "base": {
83
+ "$value": {
84
+ "fontSize": {
85
+ "value": 14,
86
+ "unit": "px"
87
+ },
88
+ "lineHeight": {
89
+ "value": 20,
90
+ "unit": "px"
91
+ }
92
+ }
93
+ },
94
+ "lg": {
95
+ "$value": {
96
+ "fontSize": {
97
+ "value": 16,
98
+ "unit": "px"
99
+ },
100
+ "lineHeight": {
101
+ "value": 22,
102
+ "unit": "px"
103
+ }
104
+ }
105
+ },
106
+ "xl": {
107
+ "$value": {
108
+ "fontSize": {
109
+ "value": 18,
110
+ "unit": "px"
111
+ },
112
+ "lineHeight": {
113
+ "value": 24,
114
+ "unit": "px"
115
+ }
116
+ }
117
+ },
118
+ "2xl": {
119
+ "$value": {
120
+ "fontSize": {
121
+ "value": 21,
122
+ "unit": "px"
123
+ },
124
+ "lineHeight": {
125
+ "value": 28,
126
+ "unit": "px"
127
+ }
128
+ }
129
+ }
130
+ },
131
+ "fontFamily": {
132
+ "$type": "fontFamily",
133
+ "sans": {
134
+ "$value": [
135
+ "Segoe UI Variable Text",
136
+ "Segoe UI",
137
+ "system-ui",
138
+ "-apple-system",
139
+ "sans-serif"
140
+ ]
141
+ },
142
+ "mono": {
143
+ "$value": [
144
+ "ui-monospace",
145
+ "Cascadia Code",
146
+ "SF Mono",
147
+ "Consolas",
148
+ "monospace"
149
+ ]
150
+ }
151
+ },
152
+ "fontWeight": {
153
+ "$type": "fontWeight",
154
+ "normal": {
155
+ "$value": 400
156
+ },
157
+ "medium": {
158
+ "$value": 500
159
+ },
160
+ "semibold": {
161
+ "$value": 600
162
+ }
163
+ },
164
+ "duration": {
165
+ "$type": "duration",
166
+ "$description": "Motion. Cut to nothing under `prefers-reduced-motion`.",
167
+ "quick": {
168
+ "$value": {
169
+ "value": 120,
170
+ "unit": "ms"
171
+ }
172
+ },
173
+ "base": {
174
+ "$value": {
175
+ "value": 160,
176
+ "unit": "ms"
177
+ }
178
+ },
179
+ "slow": {
180
+ "$value": {
181
+ "value": 240,
182
+ "unit": "ms"
183
+ }
184
+ }
185
+ },
186
+ "easing": {
187
+ "$type": "cubicBezier",
188
+ "out": {
189
+ "$value": [
190
+ 0.2,
191
+ 0,
192
+ 0,
193
+ 1
194
+ ]
195
+ },
196
+ "in-out": {
197
+ "$value": [
198
+ 0.4,
199
+ 0,
200
+ 0.2,
201
+ 1
202
+ ]
203
+ }
204
+ },
205
+ "layer": {
206
+ "$type": "number",
207
+ "$description": "Stacking order. The names are a promise about what covers what; the values only mean anything relative to each other.",
208
+ "popup": {
209
+ "$value": 10
210
+ },
211
+ "sticky": {
212
+ "$value": 20
213
+ },
214
+ "menu": {
215
+ "$value": 30
216
+ },
217
+ "floating": {
218
+ "$value": 40
219
+ },
220
+ "overlay": {
221
+ "$value": 50
222
+ },
223
+ "modal": {
224
+ "$value": 60
225
+ },
226
+ "palette": {
227
+ "$value": 70
228
+ },
229
+ "toast": {
230
+ "$value": 80
231
+ }
232
+ }
233
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dowel-ui",
3
- "version": "0.1.0",
3
+ "version": "0.3.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",
@@ -33,10 +33,12 @@
33
33
  "types": "./dist/index.d.ts",
34
34
  "import": "./dist/index.js"
35
35
  },
36
- "./theme.css": "./dist/theme.css"
36
+ "./theme.css": "./dist/theme.css",
37
+ "./tokens.json": "./dist/tokens.json",
38
+ "./accents/*.css": "./dist/accents/*.css"
37
39
  },
38
40
  "scripts": {
39
- "build": "tsc && node ../../tools/copy-theme.mjs",
41
+ "build": "tsc && node ../../tools/copy-theme.mjs && node ../../tools/build-tokens-json.mjs && node ../../tools/build-accents.mjs && node ../../tools/build-registry.mjs",
40
42
  "typecheck": "tsc --noEmit",
41
43
  "prepack": "node ../../tools/copy-readme.mjs"
42
44
  },