@nesso-how/theme 0.1.0-alpha.33

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Omar Desogus
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,60 @@
1
+ # @nesso-how/theme
2
+
3
+ The design tokens for Nesso. This is the one place colours, fonts, spacing and radii are defined. The app, the graph package and the docs site all read from here, so changing a value once changes it everywhere.
4
+
5
+ Tokens are plain TypeScript objects. Each consumer turns them into CSS variables (or reads the raw hex, for embeds). There is no `theme.css` to import, because the consumers don't share a variable namespace: the app reads `--paper`, Starlight reads `--sl-color-bg`, and so on. Each one emits the variables it needs from the same source.
6
+
7
+ ## What lives here, and what doesn't
8
+
9
+ Category colours (the `--cat-*` variables for edge types) belong to the relation vocabulary, so they stay in `@nesso-how/relation-types`, not here. A theme pack only names the palette it pairs with (`categoryPalette`); the palette switch and the theme switch stay independent.
10
+
11
+ A token belongs in this package when it both varies by theme or mode and is shared across the app, the graph and the docs. That covers surface and ink colours, accent, shadows, fonts, and the type, spacing and radii scales. It leaves out anything structural rather than visual: z-index, breakpoints and app layout sizes (status bar height, sidebar width) live in the app, and motion is a foundation constant unless a pack needs to tune it.
12
+
13
+ ## How a theme pack is shaped
14
+
15
+ A `ThemePack` has two halves.
16
+
17
+ The mode-varying half is `light` and `dark`: surface colours, accent, the recall heatmap and shadows. You write `dark` as a diff over `light`, listing only what changes. Anything you leave out (the recall scale, in the default pack) is inherited from light, so the two modes never double the work.
18
+
19
+ The mode-invariant half is identical in light and dark: the fonts, the type scale (sizes, weights, line heights), spacing and radii.
20
+
21
+ One thing to know about radii: `sm` through `xl` are steps on a scale, but `pill` (999px) and `circle` (50%) are shapes, not steps. Don't reach for them as the small or large end of the ramp.
22
+
23
+ ## Turning tokens into CSS
24
+
25
+ ```ts
26
+ import { defaultTheme, themeCss, modeVars, baseVars } from '@nesso-how/theme'
27
+
28
+ // A full stylesheet: light tokens under :root, dark under [data-theme='dark'].
29
+ themeCss(defaultTheme)
30
+
31
+ // Or the flat records, to set on an element yourself.
32
+ baseVars(defaultTheme) // { '--font-sans': "...", '--space-6': '12px', ... }
33
+ modeVars(defaultTheme, 'dark') // { '--paper': '#1a1714', '--accent': '#c47a82', ... }
34
+ ```
35
+
36
+ The app injects `themeCss(defaultTheme)` into the page head at build time, through the `nessoTheme()` Vite plugin in `vite.config.ts`, so the variables exist before the first paint. Category colours are applied separately, from the relation-types palette. The docs site does the same through `docs/scripts/gen-theme-css.mjs`, which also produces a Starlight version with `starlightCss`.
37
+
38
+ ## Changing the default theme
39
+
40
+ Edit `src/default.ts`. Every colour, font, spacing and radius value the app uses is in there. Change it here, not in the app's CSS or components, and it flows everywhere.
41
+
42
+ ## Adding a theme pack
43
+
44
+ Start from the default, override only what differs, then register it:
45
+
46
+ ```ts
47
+ // src/packs/ledger.ts
48
+ import { defaultTheme, defineTheme } from '@nesso-how/theme'
49
+
50
+ export const ledger = defineTheme(defaultTheme, {
51
+ id: 'ledger',
52
+ name: 'Ledger',
53
+ categoryPalette: 'muted',
54
+ radii: { sm: '2px', md: '3px', lg: '4px' }, // a sharper look
55
+ light: { color: { paper: '#f7f3e8' } }, // parchment
56
+ dark: { color: { paper: '#14120d' } }, // its own dark, not a global invert
57
+ })
58
+ ```
59
+
60
+ Then add it to the `themes` map in `src/registry.ts`. `defineTheme` deep-merges and never touches the base, so a pack states only what it changes.
package/dist/css.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ import type { ModeTokens, ThemeMode, ThemePack } from './types.js';
2
+ /** Fully resolved colour + shadow tokens for a mode (dark = light + dark diff). */
3
+ export declare function resolveMode(pack: ThemePack, mode: ThemeMode): ModeTokens;
4
+ /** Mode-varying CSS variables (`--paper`, `--ink-2`, `--accent`, `--shadow-md` …). */
5
+ export declare function modeVars(pack: ThemePack, mode: ThemeMode): Record<string, string>;
6
+ /** Mode-invariant CSS variables (fonts, type scale, spacing, radii). */
7
+ export declare function baseVars(pack: ThemePack): Record<string, string>;
8
+ export declare function declarations(vars: Record<string, string>, indent?: string): string;
9
+ /**
10
+ * Full stylesheet for a pack: mode-invariant tokens + light tokens under
11
+ * `:root`, dark tokens under `[data-theme='dark']`. Category colours are NOT
12
+ * emitted here — they come from `@nesso-how/relation-types` `PALETTES`, applied
13
+ * separately so the palette switch stays orthogonal.
14
+ */
15
+ export declare function themeCss(pack: ThemePack): string;
package/dist/css.js ADDED
@@ -0,0 +1,92 @@
1
+ // SPDX-License-Identifier: MIT
2
+ import { deepMerge } from './merge.js';
3
+ /** Fully resolved colour + shadow tokens for a mode (dark = light + dark diff). */
4
+ export function resolveMode(pack, mode) {
5
+ return mode === 'light' ? pack.light : deepMerge(pack.light, pack.dark);
6
+ }
7
+ /** Mode-varying CSS variables (`--paper`, `--ink-2`, `--accent`, `--shadow-md` …). */
8
+ export function modeVars(pack, mode) {
9
+ const { color: c, shadow: s } = resolveMode(pack, mode);
10
+ return {
11
+ '--paper': c.paper,
12
+ '--paper-deep': c.paperDeep,
13
+ '--ink': c.ink[1],
14
+ '--ink-2': c.ink[2],
15
+ '--ink-3': c.ink[3],
16
+ '--ink-4': c.ink[4],
17
+ '--ink-5': c.ink[5],
18
+ '--bg-elev': c.bgElev,
19
+ '--bg-card': c.bgCard,
20
+ '--line': c.line,
21
+ '--line-strong': c.lineStrong,
22
+ '--grid-dot': c.gridDot,
23
+ '--accent': c.accent,
24
+ '--highlight': c.highlight,
25
+ '--highlight-soft': c.highlightSoft,
26
+ '--highlight-selection': c.highlightSelection,
27
+ '--conf-1': c.recall[1],
28
+ '--conf-2': c.recall[2],
29
+ '--conf-3': c.recall[3],
30
+ '--conf-4': c.recall[4],
31
+ '--conf-5': c.recall[5],
32
+ '--shadow-md': s.md,
33
+ '--shadow-lg': s.lg,
34
+ '--drop-shadow-lg': s.dropLg,
35
+ };
36
+ }
37
+ /** Mode-invariant CSS variables (fonts, type scale, spacing, radii). */
38
+ export function baseVars(pack) {
39
+ const { font, type, space, radii } = pack;
40
+ return {
41
+ '--font-display': font.display,
42
+ '--font-sans': font.sans,
43
+ '--font-mono': font.mono,
44
+ '--font-feature': font.featureSettings,
45
+ '--text-xs': type.size.xs,
46
+ '--text-sm': type.size.sm,
47
+ '--text-base': type.size.base,
48
+ '--text-md': type.size.md,
49
+ '--text-lg': type.size.lg,
50
+ '--text-xl': type.size.xl,
51
+ '--font-weight-regular': String(type.weight.regular),
52
+ '--font-weight-medium': String(type.weight.medium),
53
+ '--font-weight-semibold': String(type.weight.semibold),
54
+ '--leading-tight': String(type.leading.tight),
55
+ '--leading-normal': String(type.leading.normal),
56
+ '--space-0': space[0],
57
+ '--space-1': space[1],
58
+ '--space-2': space[2],
59
+ '--space-3': space[3],
60
+ '--space-4': space[4],
61
+ '--space-5': space[5],
62
+ '--space-6': space[6],
63
+ '--space-7': space[7],
64
+ '--space-8': space[8],
65
+ '--space-9': space[9],
66
+ '--radius-none': radii.none,
67
+ '--radius-sm': radii.sm,
68
+ '--radius-md': radii.md,
69
+ '--radius-lg': radii.lg,
70
+ '--radius-xl': radii.xl,
71
+ '--radius-pill': radii.pill,
72
+ '--radius-circle': radii.circle,
73
+ };
74
+ }
75
+ export function declarations(vars, indent = ' ') {
76
+ return Object.entries(vars)
77
+ .map(([name, value]) => `${indent}${name}: ${value};`)
78
+ .join('\n');
79
+ }
80
+ /**
81
+ * Full stylesheet for a pack: mode-invariant tokens + light tokens under
82
+ * `:root`, dark tokens under `[data-theme='dark']`. Category colours are NOT
83
+ * emitted here — they come from `@nesso-how/relation-types` `PALETTES`, applied
84
+ * separately so the palette switch stays orthogonal.
85
+ */
86
+ export function themeCss(pack) {
87
+ const root = { ...baseVars(pack), ...modeVars(pack, 'light') };
88
+ return [
89
+ `:root {\n${declarations(root)}\n}`,
90
+ `[data-theme='dark'] {\n${declarations(modeVars(pack, 'dark'))}\n}`,
91
+ ].join('\n\n');
92
+ }
@@ -0,0 +1,12 @@
1
+ import type { ThemePack } from './types.js';
2
+ /**
3
+ * The default Nesso theme — Notion surface + Oxblood accent. This is the base
4
+ * pack every other pack derives from via `defineTheme`. Values mirror the tokens
5
+ * that previously lived as raw CSS in the app's `index.css`; this file is now
6
+ * their single source of truth.
7
+ *
8
+ * `dark` is a *diff* over `light`: it omits `recall`, which is shared across
9
+ * modes, and the mode-invariant axes (`font`, `type`, `space`, `radii`) are not
10
+ * repeated per mode at all.
11
+ */
12
+ export declare const defaultTheme: ThemePack;
@@ -0,0 +1,97 @@
1
+ /**
2
+ * The default Nesso theme — Notion surface + Oxblood accent. This is the base
3
+ * pack every other pack derives from via `defineTheme`. Values mirror the tokens
4
+ * that previously lived as raw CSS in the app's `index.css`; this file is now
5
+ * their single source of truth.
6
+ *
7
+ * `dark` is a *diff* over `light`: it omits `recall`, which is shared across
8
+ * modes, and the mode-invariant axes (`font`, `type`, `space`, `radii`) are not
9
+ * repeated per mode at all.
10
+ */
11
+ export const defaultTheme = {
12
+ id: 'default',
13
+ name: 'Notion · Oxblood',
14
+ categoryPalette: 'default',
15
+ font: {
16
+ display: "'Fraunces', Georgia, serif",
17
+ sans: "'Inter', ui-sans-serif, system-ui, sans-serif",
18
+ mono: "'JetBrains Mono', ui-monospace, monospace",
19
+ featureSettings: "'ss01', 'cv11'",
20
+ },
21
+ type: {
22
+ size: {
23
+ xs: '10.5px',
24
+ sm: '11px',
25
+ base: '12px',
26
+ md: '13px',
27
+ lg: '15px',
28
+ xl: '18px',
29
+ },
30
+ weight: { regular: 400, medium: 500, semibold: 600 },
31
+ leading: { tight: 1.2, normal: 1.5 },
32
+ },
33
+ space: {
34
+ 0: '0',
35
+ 1: '2px',
36
+ 2: '4px',
37
+ 3: '6px',
38
+ 4: '8px',
39
+ 5: '10px',
40
+ 6: '12px',
41
+ 7: '16px',
42
+ 8: '20px',
43
+ 9: '24px',
44
+ },
45
+ radii: {
46
+ none: '0',
47
+ sm: '6px',
48
+ md: '8px',
49
+ lg: '14px',
50
+ xl: '20px',
51
+ pill: '999px',
52
+ circle: '50%',
53
+ },
54
+ light: {
55
+ color: {
56
+ paper: '#ffffff',
57
+ paperDeep: '#f3f1ed',
58
+ ink: { 1: '#1a1814', 2: '#3c3830', 3: '#6a6356', 4: '#9a9080', 5: '#c4b9a4' },
59
+ bgElev: '#fbfaf8',
60
+ bgCard: '#ffffff',
61
+ line: 'rgba(26, 24, 20, 0.1)',
62
+ lineStrong: 'rgba(26, 24, 20, 0.2)',
63
+ gridDot: 'rgba(26, 24, 20, 0.18)',
64
+ accent: '#6e2730',
65
+ highlight: '#6e2730',
66
+ highlightSoft: 'rgba(110, 39, 48, 0.08)',
67
+ highlightSelection: 'rgba(110, 39, 48, 0.22)',
68
+ recall: { 1: '#b14a4a', 2: '#c47a3a', 3: '#b89a3a', 4: '#75752d', 5: '#467d2c' },
69
+ },
70
+ shadow: {
71
+ md: '0 8px 28px -10px rgba(26, 24, 20, 0.18), 0 2px 8px rgba(26, 24, 20, 0.06)',
72
+ lg: '0 28px 60px -16px rgba(26, 24, 20, 0.22), 0 8px 20px -8px rgba(26, 24, 20, 0.1)',
73
+ dropLg: 'drop-shadow(0 28px 60px rgba(26, 24, 20, 0.22)) drop-shadow(0 8px 20px rgba(26, 24, 20, 0.1))',
74
+ },
75
+ },
76
+ dark: {
77
+ color: {
78
+ paper: '#1a1714',
79
+ paperDeep: '#221e19',
80
+ ink: { 1: '#f0e9d8', 2: '#d4ccba', 3: '#a39c8c', 4: '#6e6859', 5: '#423e35' },
81
+ bgElev: '#211d18',
82
+ bgCard: '#262019',
83
+ line: 'rgba(240, 233, 216, 0.1)',
84
+ lineStrong: 'rgba(240, 233, 216, 0.2)',
85
+ gridDot: 'rgba(240, 233, 216, 0.15)',
86
+ accent: '#c47a82',
87
+ highlight: '#c47a82',
88
+ highlightSoft: 'rgba(196, 122, 130, 0.12)',
89
+ highlightSelection: 'rgba(196, 122, 130, 0.28)',
90
+ },
91
+ shadow: {
92
+ md: '0 8px 28px -10px rgba(0, 0, 0, 0.55), 0 2px 8px rgba(0, 0, 0, 0.4)',
93
+ lg: '0 28px 60px -16px rgba(0, 0, 0, 0.7), 0 8px 20px -8px rgba(0, 0, 0, 0.5)',
94
+ dropLg: 'drop-shadow(0 28px 60px rgba(0, 0, 0, 0.7)) drop-shadow(0 8px 20px rgba(0, 0, 0, 0.5))',
95
+ },
96
+ },
97
+ };
@@ -0,0 +1,6 @@
1
+ export type { ColorTokens, ShadowTokens, ModeTokens, FontTokens, TypeScale, TypeStep, SpaceScale, SpaceStep, RadiiScale, ThemePack, ThemeOverride, ThemeMode, DeepPartial, } from './types.js';
2
+ export { defaultTheme } from './default.js';
3
+ export { defineTheme, themes, getTheme } from './registry.js';
4
+ export { resolveMode, modeVars, baseVars, themeCss, declarations } from './css.js';
5
+ export { starlightCss } from './starlight.js';
6
+ export { deepMerge } from './merge.js';
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { defaultTheme } from './default.js';
2
+ export { defineTheme, themes, getTheme } from './registry.js';
3
+ export { resolveMode, modeVars, baseVars, themeCss, declarations } from './css.js';
4
+ export { starlightCss } from './starlight.js';
5
+ export { deepMerge } from './merge.js';
@@ -0,0 +1,4 @@
1
+ import type { DeepPartial } from './types.js';
2
+ export declare function isPlainObject(value: unknown): value is Record<string, unknown>;
3
+ /** Recursively merge `override` onto `base`; `undefined` values are skipped. */
4
+ export declare function deepMerge<T>(base: T, override: DeepPartial<T>): T;
package/dist/merge.js ADDED
@@ -0,0 +1,20 @@
1
+ export function isPlainObject(value) {
2
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
3
+ }
4
+ /** Recursively merge `override` onto `base`; `undefined` values are skipped. */
5
+ export function deepMerge(base, override) {
6
+ if (!isPlainObject(base) || !isPlainObject(override)) {
7
+ return override ?? base;
8
+ }
9
+ const out = { ...base };
10
+ for (const [key, value] of Object.entries(override)) {
11
+ if (value === undefined)
12
+ continue;
13
+ const prev = out[key];
14
+ out[key] =
15
+ isPlainObject(prev) && isPlainObject(value)
16
+ ? deepMerge(prev, value)
17
+ : value;
18
+ }
19
+ return out;
20
+ }
@@ -0,0 +1,10 @@
1
+ import type { ThemeOverride, ThemePack } from './types.js';
2
+ /**
3
+ * Derive a new theme pack from a base by overriding only what changes. This is
4
+ * how the registry stays extensible: a new pack declares its diff, not a full
5
+ * copy. The default pack is the conventional base.
6
+ */
7
+ export declare function defineTheme(base: ThemePack, override: ThemeOverride): ThemePack;
8
+ /** All built-in packs, keyed by `id`. Add new packs here once defined. */
9
+ export declare const themes: Record<string, ThemePack>;
10
+ export declare function getTheme(id: string): ThemePack;
@@ -0,0 +1,18 @@
1
+ // SPDX-License-Identifier: MIT
2
+ import { defaultTheme } from './default.js';
3
+ import { deepMerge } from './merge.js';
4
+ /**
5
+ * Derive a new theme pack from a base by overriding only what changes. This is
6
+ * how the registry stays extensible: a new pack declares its diff, not a full
7
+ * copy. The default pack is the conventional base.
8
+ */
9
+ export function defineTheme(base, override) {
10
+ return deepMerge(base, override);
11
+ }
12
+ /** All built-in packs, keyed by `id`. Add new packs here once defined. */
13
+ export const themes = {
14
+ [defaultTheme.id]: defaultTheme,
15
+ };
16
+ export function getTheme(id) {
17
+ return themes[id] ?? defaultTheme;
18
+ }
@@ -0,0 +1,7 @@
1
+ import type { ThemePack } from './types.js';
2
+ /**
3
+ * Full Starlight stylesheet for a pack. Starlight's default theme is dark
4
+ * (`:root`); light lives under `:root[data-theme='light']`. `--font-display` is
5
+ * mode-invariant, so it is emitted once under `:root`.
6
+ */
7
+ export declare function starlightCss(pack: ThemePack): string;
@@ -0,0 +1,44 @@
1
+ // SPDX-License-Identifier: MIT
2
+ import { declarations, resolveMode } from './css.js';
3
+ /**
4
+ * Map the theme tokens that the Starlight docs/landing site shares with the app
5
+ * onto Starlight's `--sl-color-*` namespace. Only the cleanly-shared tokens are
6
+ * emitted (surface, ink scale, accent, hairline); docs-specific chrome that does
7
+ * not derive from a theme token (admonition colours, the deeper-than-surface
8
+ * code background, the lighter/darker accent variants, the near-black `gray-7`)
9
+ * stays owned by `docs/src/styles/custom.css`.
10
+ */
11
+ function starlightVars(pack, mode) {
12
+ const { color: c } = resolveMode(pack, mode);
13
+ return {
14
+ '--sl-color-bg': c.paper,
15
+ '--sl-color-bg-nav': c.paper,
16
+ '--sl-color-bg-sidebar': c.paper,
17
+ '--sl-color-text': c.ink[1],
18
+ '--sl-color-text-accent': c.accent,
19
+ '--sl-color-white': c.ink[1],
20
+ '--sl-color-black': c.paper,
21
+ '--sl-color-accent': c.accent,
22
+ '--sl-color-hairline': c.line,
23
+ '--sl-color-gray-1': c.ink[1],
24
+ '--sl-color-gray-2': c.ink[2],
25
+ '--sl-color-gray-3': c.ink[3],
26
+ '--sl-color-gray-4': c.ink[4],
27
+ '--sl-color-gray-5': c.ink[5],
28
+ '--sl-color-gray-6': c.paperDeep,
29
+ // App-named tokens consumed directly by docs structural CSS:
30
+ '--highlight-selection': c.highlightSelection,
31
+ };
32
+ }
33
+ /**
34
+ * Full Starlight stylesheet for a pack. Starlight's default theme is dark
35
+ * (`:root`); light lives under `:root[data-theme='light']`. `--font-display` is
36
+ * mode-invariant, so it is emitted once under `:root`.
37
+ */
38
+ export function starlightCss(pack) {
39
+ const dark = { ...starlightVars(pack, 'dark'), '--font-display': pack.font.display };
40
+ return [
41
+ `:root {\n${declarations(dark)}\n}`,
42
+ `:root[data-theme='light'] {\n${declarations(starlightVars(pack, 'light'))}\n}`,
43
+ ].join('\n\n');
44
+ }
@@ -0,0 +1,109 @@
1
+ import type { CategoryPalette } from '@nesso-how/relation-types';
2
+ /**
3
+ * Tokens that change between light and dark mode. A theme pack ships a full
4
+ * `light` set and a `dark` *diff* (see {@link ThemePack.dark}). Category colours
5
+ * are intentionally absent: they belong to the relation-type vocabulary
6
+ * (`@nesso-how/relation-types` `PALETTES`) and are referenced by name via
7
+ * {@link ThemePack.categoryPalette}, never duplicated here.
8
+ */
9
+ export interface ColorTokens {
10
+ paper: string;
11
+ paperDeep: string;
12
+ /** 1 = strongest ink, 5 = faintest. Emitted as `--ink`, `--ink-2` … `--ink-5`. */
13
+ ink: {
14
+ 1: string;
15
+ 2: string;
16
+ 3: string;
17
+ 4: string;
18
+ 5: string;
19
+ };
20
+ bgElev: string;
21
+ bgCard: string;
22
+ line: string;
23
+ lineStrong: string;
24
+ gridDot: string;
25
+ accent: string;
26
+ highlight: string;
27
+ highlightSoft: string;
28
+ highlightSelection: string;
29
+ /** FSRS recall heatmap / model-status dots, cool→warm. Emitted as `--conf-1` … `--conf-5`. */
30
+ recall: {
31
+ 1: string;
32
+ 2: string;
33
+ 3: string;
34
+ 4: string;
35
+ 5: string;
36
+ };
37
+ }
38
+ export interface ShadowTokens {
39
+ md: string;
40
+ lg: string;
41
+ dropLg: string;
42
+ }
43
+ /** The mode-varying half of a theme: colour + elevation. */
44
+ export interface ModeTokens {
45
+ color: ColorTokens;
46
+ shadow: ShadowTokens;
47
+ }
48
+ export interface FontTokens {
49
+ display: string;
50
+ sans: string;
51
+ mono: string;
52
+ featureSettings: string;
53
+ }
54
+ export type TypeStep = 'xs' | 'sm' | 'base' | 'md' | 'lg' | 'xl';
55
+ export interface TypeScale {
56
+ size: Record<TypeStep, string>;
57
+ weight: {
58
+ regular: number;
59
+ medium: number;
60
+ semibold: number;
61
+ };
62
+ leading: {
63
+ tight: number;
64
+ normal: number;
65
+ };
66
+ }
67
+ export type SpaceStep = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
68
+ /** Spacing ramp. Step `0` is `0`; subsequent steps grow on the audited rhythm. */
69
+ export type SpaceScale = Record<SpaceStep, string>;
70
+ /**
71
+ * Border radii. `sm`…`xl` are ramp steps; `pill` and `circle` are *shape
72
+ * primitives* ("half the height" and `50%`), not rungs of the numeric scale.
73
+ */
74
+ export interface RadiiScale {
75
+ none: string;
76
+ sm: string;
77
+ md: string;
78
+ lg: string;
79
+ xl: string;
80
+ pill: string;
81
+ circle: string;
82
+ }
83
+ /**
84
+ * A complete, selectable visual identity. Mode-invariant axes (`font`, `type`,
85
+ * `space`, `radii`) sit at the top level; only `light`/`dark` vary by mode, so
86
+ * the matrix never doubles typography or spacing.
87
+ */
88
+ export interface ThemePack {
89
+ id: string;
90
+ name: string;
91
+ /** Default category palette this pack pairs with; the palette switch stays orthogonal. */
92
+ categoryPalette: CategoryPalette;
93
+ font: FontTokens;
94
+ type: TypeScale;
95
+ space: SpaceScale;
96
+ radii: RadiiScale;
97
+ light: ModeTokens;
98
+ /** Diff applied over `light` to produce dark mode; specify only what changes. */
99
+ dark: DeepPartial<ModeTokens>;
100
+ }
101
+ export type DeepPartial<T> = {
102
+ [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
103
+ };
104
+ /** Partial override accepted by `defineTheme` when deriving a pack from a base. */
105
+ export type ThemeOverride = DeepPartial<Omit<ThemePack, 'id' | 'name'>> & {
106
+ id: string;
107
+ name: string;
108
+ };
109
+ export type ThemeMode = 'light' | 'dark';
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@nesso-how/theme",
3
+ "version": "0.1.0-alpha.33",
4
+ "description": "Nesso design tokens — the single source of truth for theme packs (colour, typography, spacing, radii) shared across app, graph and docs",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/nesso-how/nesso",
9
+ "directory": "packages/theme"
10
+ },
11
+ "type": "module",
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "import": "./dist/index.js"
22
+ }
23
+ },
24
+ "dependencies": {
25
+ "@nesso-how/relation-types": "0.1.0-alpha.33"
26
+ },
27
+ "devDependencies": {
28
+ "typescript": "~5.8.3"
29
+ },
30
+ "scripts": {
31
+ "build": "rm -rf dist && tsc",
32
+ "dev": "tsc --watch"
33
+ }
34
+ }