meno-core 1.1.0 → 1.1.2
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/dist/chunks/{chunk-NUP7H7D3.js → chunk-7ZLF4NE5.js} +2 -2
- package/dist/chunks/{chunk-QWTQZHG3.js → chunk-FQBIC2OB.js} +1 -1
- package/dist/chunks/chunk-FQBIC2OB.js.map +7 -0
- package/dist/chunks/{chunk-2IIQK7T3.js → chunk-J4IPTP5X.js} +393 -3
- package/dist/chunks/{chunk-2IIQK7T3.js.map → chunk-J4IPTP5X.js.map} +3 -3
- package/dist/lib/client/index.js +2 -2
- package/dist/lib/server/index.js +804 -218
- package/dist/lib/server/index.js.map +4 -4
- package/dist/lib/shared/index.js +6 -2
- package/dist/lib/shared/index.js.map +2 -2
- package/lib/server/index.ts +19 -0
- package/lib/server/services/ColorService.ts +38 -7
- package/lib/server/services/VariableService.ts +34 -5
- package/lib/server/ssr/htmlGenerator.ts +18 -2
- package/lib/server/ssr/ssrRenderer.test.ts +25 -0
- package/lib/server/ssr/ssrRenderer.ts +12 -2
- package/lib/server/themeCssCodec.test.ts +154 -0
- package/lib/server/themeCssCodec.ts +488 -0
- package/lib/server/themeCssStore.test.ts +139 -0
- package/lib/server/themeCssStore.ts +210 -0
- package/lib/shared/cssGeneration.test.ts +10 -0
- package/lib/shared/cssGeneration.ts +10 -8
- package/lib/shared/index.ts +6 -0
- package/lib/shared/interactiveStyles.test.ts +3 -1
- package/lib/shared/tailwindThemeScale.ts +256 -0
- package/lib/shared/types/colors.ts +6 -2
- package/lib/shared/types/variables.ts +14 -7
- package/lib/shared/utilityClassConfig.ts +116 -0
- package/lib/shared/utilityClassMapper.test.ts +148 -0
- package/lib/shared/utilityClassMapper.ts +33 -0
- package/lib/shared/utilityClassNames.ts +146 -0
- package/package.json +1 -1
- package/dist/chunks/chunk-QWTQZHG3.js.map +0 -7
- /package/dist/chunks/{chunk-NUP7H7D3.js.map → chunk-7ZLF4NE5.js.map} +0 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* theme.css store — the read/modify/write layer over a project's `src/styles/theme.css`,
|
|
3
|
+
* which is the single source of truth for design tokens (colors + variables). It replaces
|
|
4
|
+
* the old `colors.json` / `variables.json` pair for astro-format projects.
|
|
5
|
+
*
|
|
6
|
+
* Colors and variables share ONE file, so {@link saveColors} / {@link saveVariables} each
|
|
7
|
+
* re-read the current file and replace only their half (preserving the other half AND any
|
|
8
|
+
* `raw` passthrough), then re-serialize. All writes are funnelled through a single in-process
|
|
9
|
+
* queue so a colors save and a variables save can't clobber each other.
|
|
10
|
+
*
|
|
11
|
+
* On first access for a project that still carries legacy JSON, {@link migrateIfNeeded}
|
|
12
|
+
* regenerates theme.css from those JSON files (authoritative) and deletes them — a one-way,
|
|
13
|
+
* idempotent migration. Legacy JSON-format projects (no `src/pages`) are not handled here;
|
|
14
|
+
* the services keep their JSON path for those.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
18
|
+
import { rm } from 'node:fs/promises';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
import { getProjectRoot } from './projectContext';
|
|
21
|
+
import { writeFile, readTextFile, fileExists, ensureDir } from './runtime';
|
|
22
|
+
import { configService } from './services/configService';
|
|
23
|
+
import { parseThemeCss, serializeThemeCss, type ScaleConfig, type ThemeModel } from './themeCssCodec';
|
|
24
|
+
import type { ThemeConfig } from '../shared/types/colors';
|
|
25
|
+
import type { CSSVariable } from '../shared/types/variables';
|
|
26
|
+
import { createLogger } from '../shared/logger';
|
|
27
|
+
|
|
28
|
+
const log = createLogger('themeCssStore');
|
|
29
|
+
|
|
30
|
+
/** Absolute path to a project's theme stylesheet. */
|
|
31
|
+
export function themeCssPath(root: string = getProjectRoot()): string {
|
|
32
|
+
return join(root, 'src', 'styles', 'theme.css');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* True when a project keeps its tokens in `theme.css` (astro-format). Detected by an
|
|
37
|
+
* existing theme.css, an `src/pages` layout, or an explicit `"format": "astro"`. Legacy
|
|
38
|
+
* JSON-format projects return false and keep reading colors.json / variables.json.
|
|
39
|
+
*/
|
|
40
|
+
export function isThemeCssProject(root: string = getProjectRoot()): boolean {
|
|
41
|
+
if (existsSync(themeCssPath(root))) return true;
|
|
42
|
+
if (existsSync(join(root, 'src', 'pages'))) return true;
|
|
43
|
+
try {
|
|
44
|
+
const cfg = JSON.parse(readFileSync(join(root, 'project.config.json'), 'utf8')) as { format?: string };
|
|
45
|
+
if (cfg.format === 'astro') return true;
|
|
46
|
+
} catch {
|
|
47
|
+
/* no/invalid config → not astro */
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// All writes (and the migration) run serialized so concurrent color/variable saves
|
|
53
|
+
// read-modify-write the shared file without races.
|
|
54
|
+
let writeChain: Promise<unknown> = Promise.resolve();
|
|
55
|
+
function enqueue<T>(fn: () => Promise<T>): Promise<T> {
|
|
56
|
+
const run = writeChain.then(fn, fn);
|
|
57
|
+
writeChain = run.then(
|
|
58
|
+
() => undefined,
|
|
59
|
+
() => undefined,
|
|
60
|
+
);
|
|
61
|
+
return run;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function readJsonSafe<T>(path: string): Promise<T | null> {
|
|
65
|
+
try {
|
|
66
|
+
return JSON.parse(await readTextFile(path)) as T;
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function scaleConfig(): ScaleConfig {
|
|
73
|
+
return { breakpoints: configService.getBreakpoints(), responsiveScales: configService.getResponsiveScales() };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Empty-but-valid model for a project with no theme.css yet. */
|
|
77
|
+
function emptyModel(): ThemeModel {
|
|
78
|
+
return { themes: { default: 'default', themes: { default: { colors: {} } } }, variables: [], raw: [] };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Parse the on-disk theme.css (or an empty model if absent). Assumes config is loaded. */
|
|
82
|
+
async function readModel(root: string): Promise<ThemeModel> {
|
|
83
|
+
const path = themeCssPath(root);
|
|
84
|
+
if (!(await fileExists(path))) return emptyModel();
|
|
85
|
+
return parseThemeCss(await readTextFile(path), configService.getBreakpoints());
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Serialize + persist a model (creating `src/styles` as needed). Assumes config is loaded. */
|
|
89
|
+
async function writeModel(root: string, model: ThemeModel): Promise<void> {
|
|
90
|
+
const css = serializeThemeCss(model, scaleConfig());
|
|
91
|
+
await ensureDir(join(root, 'src', 'styles'));
|
|
92
|
+
await writeFile(themeCssPath(root), css);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** True when a theme has at least one color (i.e. theme.css already holds real tokens). */
|
|
96
|
+
function hasColors(themes: ThemeConfig): boolean {
|
|
97
|
+
return Object.values(themes.themes ?? {}).some((t) => Object.keys(t.colors ?? {}).length > 0);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Union of two theme configs — `current` (theme.css) wins on conflict; incoming adds keys. */
|
|
101
|
+
function mergeThemes(current: ThemeConfig, incoming: ThemeConfig | null): ThemeConfig {
|
|
102
|
+
if (!incoming?.themes) return current;
|
|
103
|
+
if (!hasColors(current)) return incoming; // empty theme.css → adopt the JSON wholesale
|
|
104
|
+
const out: ThemeConfig = { default: current.default, palette: current.palette, themes: { ...current.themes } };
|
|
105
|
+
for (const [name, theme] of Object.entries(incoming.themes)) {
|
|
106
|
+
const existing = out.themes[name];
|
|
107
|
+
out.themes[name] = existing ? { ...existing, colors: { ...theme.colors, ...existing.colors } } : theme;
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Union of two variable lists by `cssVar` — `current` wins on conflict; incoming adds new. */
|
|
113
|
+
function mergeVariables(current: CSSVariable[], incoming: CSSVariable[] | undefined): CSSVariable[] {
|
|
114
|
+
if (!incoming?.length) return current;
|
|
115
|
+
const seen = new Set(current.map((v) => v.cssVar));
|
|
116
|
+
return [...current, ...incoming.filter((v) => !seen.has(v.cssVar))];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Marker the serializer always stamps — distinguishes an authored theme.css from a derived one. */
|
|
120
|
+
const AUTHORED_MARKER = 'Meno design tokens';
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Fold a project's `colors.json` + `variables.json` into theme.css, then delete the JSON.
|
|
124
|
+
* Serves two cases:
|
|
125
|
+
* - Legacy migration — theme.css is absent or in the OLD derived format (the previous
|
|
126
|
+
* converter's DO-NOT-EDIT output). The JSON is authoritative: theme.css is rebuilt from it
|
|
127
|
+
* (we must NOT merge the derived file, whose unsectioned `:root` + duplicate `[theme]` blocks
|
|
128
|
+
* would parse into a bogus extra theme).
|
|
129
|
+
* - Overlay — theme.css is already authored (new format) and an external tool (e.g. the
|
|
130
|
+
* style-guide applier) dropped JSON additions. A UNION merge (theme.css wins on key conflict,
|
|
131
|
+
* JSON adds new keys) folds those in without clobbering anything.
|
|
132
|
+
* Foreign CSS (`raw`, e.g. @font-face) is preserved either way. No-op when no JSON is present, so
|
|
133
|
+
* it's safe to run on every load/save. Must be called inside {@link enqueue}.
|
|
134
|
+
*/
|
|
135
|
+
async function migrateIfNeeded(root: string): Promise<void> {
|
|
136
|
+
const colorsJson = join(root, 'colors.json');
|
|
137
|
+
const variablesJson = join(root, 'variables.json');
|
|
138
|
+
const hasColorsJson = existsSync(colorsJson);
|
|
139
|
+
const hasVarsJson = existsSync(variablesJson);
|
|
140
|
+
if (!hasColorsJson && !hasVarsJson) return;
|
|
141
|
+
try {
|
|
142
|
+
const jsonColors = hasColorsJson ? await readJsonSafe<ThemeConfig>(colorsJson) : null;
|
|
143
|
+
const jsonVars = hasVarsJson ? await readJsonSafe<{ variables: CSSVariable[] }>(variablesJson) : null;
|
|
144
|
+
|
|
145
|
+
let current = emptyModel();
|
|
146
|
+
let authored = false;
|
|
147
|
+
if (await fileExists(themeCssPath(root))) {
|
|
148
|
+
const text = await readTextFile(themeCssPath(root));
|
|
149
|
+
authored = text.includes(AUTHORED_MARKER);
|
|
150
|
+
current = parseThemeCss(text, configService.getBreakpoints());
|
|
151
|
+
}
|
|
152
|
+
// Authored → overlay-merge (theme.css wins). Derived/absent → JSON authoritative (base empty),
|
|
153
|
+
// but keep any foreign CSS the derived file happened to carry.
|
|
154
|
+
const base = authored ? current : emptyModel();
|
|
155
|
+
const merged: ThemeModel = {
|
|
156
|
+
themes: mergeThemes(base.themes, jsonColors?.themes ? jsonColors : null),
|
|
157
|
+
variables: mergeVariables(base.variables, jsonVars?.variables),
|
|
158
|
+
raw: current.raw,
|
|
159
|
+
};
|
|
160
|
+
await writeModel(root, merged);
|
|
161
|
+
await rm(colorsJson, { force: true });
|
|
162
|
+
await rm(variablesJson, { force: true });
|
|
163
|
+
log.info('Folded colors.json / variables.json into src/styles/theme.css');
|
|
164
|
+
} catch (err) {
|
|
165
|
+
// Leave the JSON in place so a later attempt can retry; surface but don't throw.
|
|
166
|
+
log.warn('theme.css migration failed; keeping JSON files:', err);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Load the full token model (runs the one-time JSON→theme.css migration first). */
|
|
171
|
+
export async function loadThemeModel(root: string = getProjectRoot()): Promise<ThemeModel> {
|
|
172
|
+
await configService.load();
|
|
173
|
+
return enqueue(async () => {
|
|
174
|
+
await migrateIfNeeded(root);
|
|
175
|
+
return readModel(root);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Persist colors (themes) into theme.css, preserving the variables + raw already there. */
|
|
180
|
+
export async function saveColors(themes: ThemeConfig, root: string = getProjectRoot()): Promise<void> {
|
|
181
|
+
await configService.load();
|
|
182
|
+
return enqueue(async () => {
|
|
183
|
+
await migrateIfNeeded(root);
|
|
184
|
+
const current = await readModel(root);
|
|
185
|
+
await writeModel(root, { themes, variables: current.variables, raw: current.raw });
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Persist variables into theme.css, preserving the colors + raw already there. */
|
|
190
|
+
export async function saveVariables(variables: CSSVariable[], root: string = getProjectRoot()): Promise<void> {
|
|
191
|
+
await configService.load();
|
|
192
|
+
return enqueue(async () => {
|
|
193
|
+
await migrateIfNeeded(root);
|
|
194
|
+
const current = await readModel(root);
|
|
195
|
+
await writeModel(root, { themes: current.themes, variables, raw: current.raw });
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Re-serialize theme.css from its own current contents — used after a `project.config.json`
|
|
201
|
+
* scale-knob change to regenerate the AUTO `@media` region from the new config.
|
|
202
|
+
*/
|
|
203
|
+
export async function regenerateThemeCss(root: string = getProjectRoot()): Promise<void> {
|
|
204
|
+
await configService.load();
|
|
205
|
+
return enqueue(async () => {
|
|
206
|
+
await migrateIfNeeded(root);
|
|
207
|
+
if (!(await fileExists(themeCssPath(root)))) return;
|
|
208
|
+
await writeModel(root, await readModel(root));
|
|
209
|
+
});
|
|
210
|
+
}
|
|
@@ -694,6 +694,16 @@ describe('styleObjectToCSS', () => {
|
|
|
694
694
|
expect(styleObjectToCSS({ color: 'rgb(0,0,0)' })).toBe('color: rgb(0,0,0)');
|
|
695
695
|
});
|
|
696
696
|
|
|
697
|
+
test('named CSS colors and keywords pass through — never wrapped in var()', () => {
|
|
698
|
+
// Regression: a literal `red` was being emitted as `var(--red)` (a token
|
|
699
|
+
// that does not exist) instead of the real CSS color.
|
|
700
|
+
expect(styleObjectToCSS({ color: 'red' })).toBe('color: red');
|
|
701
|
+
expect(styleObjectToCSS({ backgroundColor: 'transparent' })).toBe('background-color: transparent');
|
|
702
|
+
expect(styleObjectToCSS({ color: 'currentColor' })).toBe('color: currentColor');
|
|
703
|
+
expect(styleObjectToCSS({ borderColor: 'rebeccapurple' })).toBe('border-color: rebeccapurple');
|
|
704
|
+
expect(styleObjectToCSS({ color: 'inherit' })).toBe('color: inherit');
|
|
705
|
+
});
|
|
706
|
+
|
|
697
707
|
test('skips _mapping (binding) values — bound props yield empty/partial output', () => {
|
|
698
708
|
expect(styleObjectToCSS({ color: { _mapping: { prop: 'x' } } as never })).toBe('');
|
|
699
709
|
expect(styleObjectToCSS({ color: { _mapping: { prop: 'x' } } as never, padding: '4px' })).toBe('padding: 4px');
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
breakpointClassPrefixes,
|
|
12
12
|
resolveRootProperty,
|
|
13
13
|
camelToKebab,
|
|
14
|
+
isBareColorTokenName,
|
|
14
15
|
} from './utilityClassNames';
|
|
15
16
|
import { getStyleValue, getDynamicStyle, isDynamicClass } from './styleValueRegistry';
|
|
16
17
|
import type { BreakpointConfig } from './breakpoints';
|
|
@@ -213,6 +214,9 @@ export function generateRuleForClass(className: string, knownTokens?: ReadonlySe
|
|
|
213
214
|
}
|
|
214
215
|
|
|
215
216
|
// Multi-property roots (Tailwind axis shorthands)
|
|
217
|
+
if (root === 'size') {
|
|
218
|
+
return `width: ${value}; height: ${value};`;
|
|
219
|
+
}
|
|
216
220
|
if (root === 'px') {
|
|
217
221
|
return `padding-left: ${value}; padding-right: ${value};`;
|
|
218
222
|
}
|
|
@@ -726,15 +730,13 @@ export function styleObjectToCSS(style: StyleObject): string {
|
|
|
726
730
|
// Convert camelCase to kebab-case
|
|
727
731
|
const cssProperty = prop.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
728
732
|
|
|
729
|
-
// Handle CSS variable references for color properties
|
|
733
|
+
// Handle CSS variable references for color properties. Only a bare Meno
|
|
734
|
+
// color *token* (e.g. `primary`) is wrapped in `var(--token)`. Named CSS
|
|
735
|
+
// colors (`red`), hex, color functions and Tailwind palette names are real
|
|
736
|
+
// values and pass through untouched — mirrors the utility-class engine so
|
|
737
|
+
// the interactive path and the static path resolve colors identically.
|
|
730
738
|
let cssValue = String(value);
|
|
731
|
-
if (
|
|
732
|
-
(prop === 'backgroundColor' || prop === 'color' || prop === 'borderColor') &&
|
|
733
|
-
!cssValue.startsWith('#') &&
|
|
734
|
-
!cssValue.startsWith('rgb') &&
|
|
735
|
-
!cssValue.startsWith('var(') &&
|
|
736
|
-
!cssValue.includes('(')
|
|
737
|
-
) {
|
|
739
|
+
if ((prop === 'backgroundColor' || prop === 'color' || prop === 'borderColor') && isBareColorTokenName(cssValue)) {
|
|
738
740
|
cssValue = `var(--${cssValue})`;
|
|
739
741
|
}
|
|
740
742
|
|
package/lib/shared/index.ts
CHANGED
|
@@ -30,6 +30,12 @@ export { splitVariantPrefix, normalizeBreakpointVariant } from './utilityClassNa
|
|
|
30
30
|
// Utility-class catalog pieces — the seed for the Styles panel's class-bar autocomplete (§13 Phase 5).
|
|
31
31
|
// Selective (not `export *`) to keep the barrel collision-free.
|
|
32
32
|
export { propertyMap, staticUtilityReverse, keywordValues, presetClassReverse } from './utilityClassConfig';
|
|
33
|
+
// Tailwind theme-scale tokens + the seed-variable generator (`text-lg` → `var(--text-lg, …)`).
|
|
34
|
+
export {
|
|
35
|
+
THEME_SCALE_ENTRIES,
|
|
36
|
+
defaultTailwindThemeVariables,
|
|
37
|
+
type ThemeScaleEntry,
|
|
38
|
+
} from './tailwindThemeScale';
|
|
33
39
|
export * from './elementClassName';
|
|
34
40
|
export * from './styleValueRegistry';
|
|
35
41
|
|
|
@@ -33,8 +33,10 @@ describe('generateInteractiveCSS', () => {
|
|
|
33
33
|
const css = generateInteractiveCSS(elementClass, interactiveStyles);
|
|
34
34
|
|
|
35
35
|
expect(css).toContain(`.${elementClass}:hover`);
|
|
36
|
+
// `primary` is a bare Meno token → wrapped in var(); `white` is a real
|
|
37
|
+
// named CSS color → passes through untouched (not var(--white)).
|
|
36
38
|
expect(css).toContain('background-color: var(--primary)');
|
|
37
|
-
expect(css).toContain('color:
|
|
39
|
+
expect(css).toContain('color: white');
|
|
38
40
|
});
|
|
39
41
|
|
|
40
42
|
test('should generate active styles for page element', () => {
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tailwind named-scale → CSS-variable theme table.
|
|
3
|
+
*
|
|
4
|
+
* Tailwind's *named* value scales (`text-lg`, `font-semibold`, `rounded-md`, `shadow-lg`,
|
|
5
|
+
* `max-w-md`, `leading-tight`, `tracking-wide`, `font-sans`) have no single numeric mapping — each
|
|
6
|
+
* name is a theme token. Meno is token-based, so the class resolves to a plain variable reference
|
|
7
|
+
* with **no baked default** — it renders the project's variable when defined, and is inert when not
|
|
8
|
+
* (Meno deliberately does NOT smuggle Tailwind's opinionated defaults into the render):
|
|
9
|
+
*
|
|
10
|
+
* `text-lg` → `font-size: var(--text-lg)`
|
|
11
|
+
*
|
|
12
|
+
* Define `--text-lg` (in `variables.json`) → `text-lg` uses it everywhere; leave it undefined →
|
|
13
|
+
* the declaration has no effect (use an arbitrary value like `text-[18px]` for a one-off). See the
|
|
14
|
+
* styling docs for the authoring guidance.
|
|
15
|
+
*
|
|
16
|
+
* This table is the single source of truth for:
|
|
17
|
+
* - the parser (`parseUtilityClass`): class → `var(--token)`
|
|
18
|
+
* - the inverse mapper (`propertyValueToClass`): `var(--token)` → compact class
|
|
19
|
+
* - `defaultTailwindThemeVariables()`: an OPT-IN starter set (Tailwind values) a user can choose
|
|
20
|
+
* to create — it is never auto-applied and never a render fallback.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { CSSVariable, VariableGroup, VariableType } from './types/variables';
|
|
24
|
+
|
|
25
|
+
/** One Tailwind theme token: the class it backs, its CSS var, default value and UI grouping. */
|
|
26
|
+
export interface ThemeScaleEntry {
|
|
27
|
+
/** Full utility class, e.g. `text-lg`, or the bare default (`rounded`, `shadow`). */
|
|
28
|
+
className: string;
|
|
29
|
+
/** The class root (`text`, `font`, `rounded`, `shadow`, `max-w`, `leading`, `tracking`). */
|
|
30
|
+
root: string;
|
|
31
|
+
/** CSS custom property, e.g. `--text-lg`. */
|
|
32
|
+
cssVar: string;
|
|
33
|
+
/** kebab-case CSS property the class targets. */
|
|
34
|
+
property: string;
|
|
35
|
+
/** Tailwind's default value — the starter value for the OPT-IN seed set, NOT a render fallback. */
|
|
36
|
+
value: string;
|
|
37
|
+
/** Variable picker group + scaling type for the seeded variable. */
|
|
38
|
+
group: VariableGroup;
|
|
39
|
+
/** Human label for the seeded variable, e.g. `Text LG`. */
|
|
40
|
+
label: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type ScaleSpec = {
|
|
44
|
+
root: string;
|
|
45
|
+
property: string;
|
|
46
|
+
group: VariableGroup;
|
|
47
|
+
/** Prefix for the css var; class is `${root}-${name}` (or bare `${root}` when name is ''). */
|
|
48
|
+
varPrefix: string;
|
|
49
|
+
/** Human label prefix for the seeded variable. */
|
|
50
|
+
labelPrefix: string;
|
|
51
|
+
/** [classSuffix, cssValue] pairs. '' suffix = the bare class (`rounded`, `shadow`). */
|
|
52
|
+
values: [string, string][];
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const SANS = 'ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif';
|
|
56
|
+
const SERIF = 'ui-serif, Georgia, Cambria, serif';
|
|
57
|
+
const MONO = 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace';
|
|
58
|
+
|
|
59
|
+
const SPECS: ScaleSpec[] = [
|
|
60
|
+
{
|
|
61
|
+
root: 'text',
|
|
62
|
+
property: 'font-size',
|
|
63
|
+
group: 'font-size',
|
|
64
|
+
varPrefix: 'text',
|
|
65
|
+
labelPrefix: 'Text',
|
|
66
|
+
values: [
|
|
67
|
+
['xs', '0.75rem'],
|
|
68
|
+
['sm', '0.875rem'],
|
|
69
|
+
['base', '1rem'],
|
|
70
|
+
['lg', '1.125rem'],
|
|
71
|
+
['xl', '1.25rem'],
|
|
72
|
+
['2xl', '1.5rem'],
|
|
73
|
+
['3xl', '1.875rem'],
|
|
74
|
+
['4xl', '2.25rem'],
|
|
75
|
+
['5xl', '3rem'],
|
|
76
|
+
['6xl', '3.75rem'],
|
|
77
|
+
['7xl', '4.5rem'],
|
|
78
|
+
['8xl', '6rem'],
|
|
79
|
+
['9xl', '8rem'],
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
// Only the weights Meno's static utilities don't already cover (normal/bold stay static).
|
|
84
|
+
root: 'font',
|
|
85
|
+
property: 'font-weight',
|
|
86
|
+
group: 'font-weight',
|
|
87
|
+
varPrefix: 'font-weight',
|
|
88
|
+
labelPrefix: 'Font Weight',
|
|
89
|
+
values: [
|
|
90
|
+
['thin', '100'],
|
|
91
|
+
['extralight', '200'],
|
|
92
|
+
['light', '300'],
|
|
93
|
+
['medium', '500'],
|
|
94
|
+
['semibold', '600'],
|
|
95
|
+
['extrabold', '800'],
|
|
96
|
+
['black', '900'],
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
root: 'font',
|
|
101
|
+
property: 'font-family',
|
|
102
|
+
group: 'font-family',
|
|
103
|
+
varPrefix: 'font',
|
|
104
|
+
labelPrefix: 'Font',
|
|
105
|
+
values: [
|
|
106
|
+
['sans', SANS],
|
|
107
|
+
['serif', SERIF],
|
|
108
|
+
['mono', MONO],
|
|
109
|
+
],
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
root: 'rounded',
|
|
113
|
+
property: 'border-radius',
|
|
114
|
+
group: 'border-radius',
|
|
115
|
+
varPrefix: 'radius',
|
|
116
|
+
labelPrefix: 'Radius',
|
|
117
|
+
values: [
|
|
118
|
+
['', '0.25rem'],
|
|
119
|
+
['sm', '0.125rem'],
|
|
120
|
+
['md', '0.375rem'],
|
|
121
|
+
['lg', '0.5rem'],
|
|
122
|
+
['xl', '0.75rem'],
|
|
123
|
+
['2xl', '1rem'],
|
|
124
|
+
['3xl', '1.5rem'],
|
|
125
|
+
],
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
root: 'shadow',
|
|
129
|
+
property: 'box-shadow',
|
|
130
|
+
group: 'shadow',
|
|
131
|
+
varPrefix: 'shadow',
|
|
132
|
+
labelPrefix: 'Shadow',
|
|
133
|
+
values: [
|
|
134
|
+
['', '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)'],
|
|
135
|
+
['sm', '0 1px 2px 0 rgb(0 0 0 / 0.05)'],
|
|
136
|
+
['md', '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)'],
|
|
137
|
+
['lg', '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)'],
|
|
138
|
+
['xl', '0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)'],
|
|
139
|
+
['2xl', '0 25px 50px -12px rgb(0 0 0 / 0.25)'],
|
|
140
|
+
],
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
root: 'max-w',
|
|
144
|
+
property: 'max-width',
|
|
145
|
+
group: 'size',
|
|
146
|
+
varPrefix: 'container',
|
|
147
|
+
labelPrefix: 'Container',
|
|
148
|
+
values: [
|
|
149
|
+
['xs', '20rem'],
|
|
150
|
+
['sm', '24rem'],
|
|
151
|
+
['md', '28rem'],
|
|
152
|
+
['lg', '32rem'],
|
|
153
|
+
['xl', '36rem'],
|
|
154
|
+
['2xl', '42rem'],
|
|
155
|
+
['3xl', '48rem'],
|
|
156
|
+
['4xl', '56rem'],
|
|
157
|
+
['5xl', '64rem'],
|
|
158
|
+
['6xl', '72rem'],
|
|
159
|
+
['7xl', '80rem'],
|
|
160
|
+
['prose', '65ch'],
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
root: 'leading',
|
|
165
|
+
property: 'line-height',
|
|
166
|
+
group: 'line-height',
|
|
167
|
+
varPrefix: 'leading',
|
|
168
|
+
labelPrefix: 'Leading',
|
|
169
|
+
values: [
|
|
170
|
+
['none', '1'],
|
|
171
|
+
['tight', '1.25'],
|
|
172
|
+
['snug', '1.375'],
|
|
173
|
+
['relaxed', '1.625'],
|
|
174
|
+
['loose', '2'],
|
|
175
|
+
],
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
root: 'tracking',
|
|
179
|
+
property: 'letter-spacing',
|
|
180
|
+
group: 'letter-spacing',
|
|
181
|
+
varPrefix: 'tracking',
|
|
182
|
+
labelPrefix: 'Tracking',
|
|
183
|
+
values: [
|
|
184
|
+
['tighter', '-0.05em'],
|
|
185
|
+
['tight', '-0.025em'],
|
|
186
|
+
['wide', '0.025em'],
|
|
187
|
+
['wider', '0.05em'],
|
|
188
|
+
['widest', '0.1em'],
|
|
189
|
+
],
|
|
190
|
+
},
|
|
191
|
+
];
|
|
192
|
+
|
|
193
|
+
/** Size codes read better uppercased (`LG`, `2XL`); word names get title case (`Semibold`). */
|
|
194
|
+
function labelSuffix(suffix: string): string {
|
|
195
|
+
if (/^(xs|sm|md|lg|xl|\dxl)$/.test(suffix)) return suffix.toUpperCase();
|
|
196
|
+
return suffix.charAt(0).toUpperCase() + suffix.slice(1);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** All theme entries, flattened. */
|
|
200
|
+
export const THEME_SCALE_ENTRIES: ThemeScaleEntry[] = SPECS.flatMap((spec) =>
|
|
201
|
+
spec.values.map(([suffix, value]) => {
|
|
202
|
+
const className = suffix ? `${spec.root}-${suffix}` : spec.root;
|
|
203
|
+
const cssVar = suffix ? `--${spec.varPrefix}-${suffix}` : `--${spec.varPrefix}`;
|
|
204
|
+
const label = suffix ? `${spec.labelPrefix} ${labelSuffix(suffix)}` : spec.labelPrefix;
|
|
205
|
+
return { className, root: spec.root, cssVar, property: spec.property, value, group: spec.group, label };
|
|
206
|
+
}),
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
/** className → entry (parser lookup). e.g. `text-lg`, `rounded`, `font-sans`. */
|
|
210
|
+
export const THEME_SCALE_BY_CLASS: ReadonlyMap<string, ThemeScaleEntry> = new Map(
|
|
211
|
+
THEME_SCALE_ENTRIES.map((e) => [e.className, e]),
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
/** cssVar → entry (inverse-mapper lookup). e.g. `--text-lg` → `text-lg`. */
|
|
215
|
+
export const THEME_SCALE_BY_CSSVAR: ReadonlyMap<string, ThemeScaleEntry> = new Map(
|
|
216
|
+
THEME_SCALE_ENTRIES.map((e) => [e.cssVar, e]),
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* The CSS value a themed class resolves to: a plain `var(--token)` reference with no fallback.
|
|
221
|
+
* Renders the project variable when defined, inert otherwise (token-based by design — no baked
|
|
222
|
+
* Tailwind default in the render path).
|
|
223
|
+
*/
|
|
224
|
+
export function themeScaleValue(entry: ThemeScaleEntry): string {
|
|
225
|
+
return `var(${entry.cssVar})`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Map a variable group to its responsive-scaling type (mirrors getDefaultScalingType). */
|
|
229
|
+
function scalingTypeFor(group: VariableGroup): VariableType {
|
|
230
|
+
switch (group) {
|
|
231
|
+
case 'font-size':
|
|
232
|
+
return 'fontSize';
|
|
233
|
+
case 'border-radius':
|
|
234
|
+
return 'borderRadius';
|
|
235
|
+
case 'size':
|
|
236
|
+
return 'size';
|
|
237
|
+
default:
|
|
238
|
+
return 'none';
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* The full set of Tailwind theme tokens as Meno `CSSVariable`s, ready to merge into a project's
|
|
244
|
+
* `variables.json`. Seeding makes the tokens visible/editable in the variable picker (filed under
|
|
245
|
+
* their group); rendering does NOT require seeding — the class carries the Tailwind default as the
|
|
246
|
+
* `var()` fallback. Idempotent merge is the caller's job (skip a `cssVar` that already exists).
|
|
247
|
+
*/
|
|
248
|
+
export function defaultTailwindThemeVariables(): CSSVariable[] {
|
|
249
|
+
return THEME_SCALE_ENTRIES.map((e) => ({
|
|
250
|
+
name: e.label,
|
|
251
|
+
cssVar: e.cssVar,
|
|
252
|
+
value: e.value,
|
|
253
|
+
type: scalingTypeFor(e.group),
|
|
254
|
+
group: e.group,
|
|
255
|
+
}));
|
|
256
|
+
}
|
|
@@ -12,10 +12,14 @@ export interface ColorVariables {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* Theme configuration with color set and metadata
|
|
15
|
+
* Theme configuration with color set and metadata.
|
|
16
|
+
*
|
|
17
|
+
* `label` is an editor-only display string. The theme.css codec does not persist
|
|
18
|
+
* it (a theme is identified by its name — the `[theme="<name>"]` selector), so it
|
|
19
|
+
* is optional; consumers fall back to the (prettified) theme name.
|
|
16
20
|
*/
|
|
17
21
|
export interface Theme {
|
|
18
|
-
label
|
|
22
|
+
label?: string;
|
|
19
23
|
colors: Record<string, string>;
|
|
20
24
|
}
|
|
21
25
|
|
|
@@ -146,22 +146,29 @@ export function getGroupForProperty(prop: string): VariableGroup | null {
|
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
/**
|
|
149
|
-
* A single CSS custom property definition
|
|
149
|
+
* A single CSS custom property definition.
|
|
150
|
+
*
|
|
151
|
+
* Source of truth is `src/styles/theme.css` (the `theme.css` codec), so the CSS
|
|
152
|
+
* variable name (`cssVar`) IS a token's identity — `name`/`prop_name` are
|
|
153
|
+
* editor-only labels that the codec no longer persists, and `group`/`type` are
|
|
154
|
+
* DERIVED on parse (from the var's comment-section header, falling back to a
|
|
155
|
+
* value heuristic), not stored. They stay on the in-memory object because the
|
|
156
|
+
* pickers and the responsive-scaling generator still consult them.
|
|
150
157
|
*/
|
|
151
158
|
export interface CSSVariable {
|
|
152
|
-
/**
|
|
153
|
-
name
|
|
154
|
-
/** Optional prop name alias for organizational purposes */
|
|
159
|
+
/** Optional display label. Not persisted by the theme.css codec — defaults to `cssVar`. */
|
|
160
|
+
name?: string;
|
|
161
|
+
/** Optional prop name alias for organizational purposes (legacy; not persisted). */
|
|
155
162
|
prop_name?: string;
|
|
156
|
-
/** CSS custom property name (e.g., "--h1-fs") */
|
|
163
|
+
/** CSS custom property name (e.g., "--h1-fs") — the token's persistent identity. */
|
|
157
164
|
cssVar: string;
|
|
158
165
|
/** Base value (e.g., "48px") */
|
|
159
166
|
value: string;
|
|
160
|
-
/** Category for responsive scaling */
|
|
167
|
+
/** Category for responsive scaling (derived from {@link group} via getDefaultScalingType). */
|
|
161
168
|
type: VariableType;
|
|
162
169
|
/** Optional per-variable breakpoint scale overrides */
|
|
163
170
|
scales?: Record<string, string>;
|
|
164
|
-
/**
|
|
171
|
+
/** UI filtering group for the variable picker (derived from the theme.css comment section). */
|
|
165
172
|
group?: VariableGroup;
|
|
166
173
|
}
|
|
167
174
|
|