@santi020k/lumen 0.2.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
@@ -36,3 +36,27 @@ and Elements targets.
36
36
  Run `lumen audit-tokens [path]` before incremental adoption when an existing stylesheet may already
37
37
  declare names such as `--surface`, `--ink`, or `--line`. The audit reports complete CSS colors that
38
38
  are incompatible with Lumen's HSL-channel token format and exits non-zero when it finds conflicts.
39
+
40
+ ## Coordinated consumer rollout
41
+
42
+ Inventory one or more pnpm consumers before a release:
43
+
44
+ ```bash
45
+ lumen rollout 0.2.0 ../site ../dashboard --exclude ../legacy
46
+ ```
47
+
48
+ The report separates manifest, workspace-catalog, and lockfile references; identifies each
49
+ framework; checks the declared pnpm and Node contracts; and reports the complete resolved Lumen
50
+ package graph. Add `--report ./lumen-rollout.json` for a durable JSON record.
51
+
52
+ After committing an intentional baseline in each consumer, apply the upgrade serially:
53
+
54
+ ```bash
55
+ lumen rollout 0.2.0 ../site ../dashboard --apply --report ./lumen-rollout.json
56
+ ```
57
+
58
+ The command preserves `catalog:` indirection and pinned/caret range style, temporarily admits the
59
+ new release through `minimumReleaseAgeExclude`, runs installs through Corepack from each consumer
60
+ root, removes obsolete Lumen release-age exceptions, verifies the unified resolved version, and
61
+ runs available framework checks, builds, and browser scripts. It refuses dirty repositories,
62
+ missing exact pnpm declarations, and unsupported Node runtimes unless `--allow-dirty` is explicit.
package/dist/cli.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { readdir, readFile, stat } from 'node:fs/promises';
2
2
  import { resolve } from 'node:path';
3
+ import { consumerRolloutSucceeded, formatConsumerRollout, runConsumerRollout } from './consumer-rollout.js';
3
4
  import { addLumenRegistryItem, getLumenRegistryEntries, getLumenRegistryItem, loadLumenRegistry, lumenRegistry } from './registry.js';
4
5
  import { auditLumenTokenCss } from './token-audit.js';
5
6
  const args = process.argv.slice(2);
@@ -17,7 +18,36 @@ const registryToken = registryTokenIndex >= 0 ? args[registryTokenIndex + 1] : p
17
18
  const targetIndex = args.indexOf('--target');
18
19
  const targetProvided = targetIndex >= 0;
19
20
  const targetValue = targetIndex >= 0 ? args[targetIndex + 1] : undefined;
21
+ const applyRollout = args.includes('--apply');
22
+ const allowDirty = args.includes('--allow-dirty');
23
+ const noVerify = args.includes('--no-verify');
24
+ const reportIndex = args.indexOf('--report');
25
+ const reportPath = reportIndex >= 0 ? args[reportIndex + 1] : undefined;
26
+ const excludeValues = args.flatMap((value, index) => {
27
+ const nextValue = args[index + 1];
28
+ return value === '--exclude' && nextValue ? [nextValue] : [];
29
+ });
20
30
  let conflict = 'skip';
31
+ const getRolloutArguments = () => {
32
+ const values = [];
33
+ const optionsWithValues = new Set(['--exclude', '--report']);
34
+ for (let index = 1; index < args.length; index += 1) {
35
+ const value = args[index];
36
+ if (!value)
37
+ continue;
38
+ if (optionsWithValues.has(value)) {
39
+ index += 1;
40
+ continue;
41
+ }
42
+ if (!value.startsWith('--'))
43
+ values.push(value);
44
+ }
45
+ const [targetVersion, ...repositories] = values;
46
+ return {
47
+ repositories: repositories.length ? repositories : [process.cwd()],
48
+ ...(targetVersion ? { targetVersion } : {})
49
+ };
50
+ };
21
51
  const getAddTarget = (value, provided = false) => {
22
52
  if (provided && !value) {
23
53
  throw new Error('Missing Lumen add target. Use astro, react, or elements.');
@@ -123,6 +153,7 @@ const help = [
123
153
  ' lumen add <name> Install a recipe or component into the current project',
124
154
  ' lumen install Print install commands',
125
155
  ' lumen audit-tokens Report incompatible semantic color custom properties',
156
+ ' lumen rollout [version] [repositories...] Inventory or upgrade pnpm consumers',
126
157
  '',
127
158
  'Options:',
128
159
  ' --cwd <path> Target directory for lumen add',
@@ -133,6 +164,11 @@ const help = [
133
164
  ' --registry <source> Load list/show data from a local or remote registry JSON file',
134
165
  ' --registry-token <key> Use a bearer token for private remote registries',
135
166
  ' --target <framework> Generate wrappers or recipe starters for astro, react, or elements',
167
+ ' --apply Apply a rollout (requires a target version)',
168
+ ' --allow-dirty Allow rollout after recording an uncommitted baseline',
169
+ ' --exclude <path> Exclude a repository (repeatable)',
170
+ ' --no-verify Skip downstream check, typecheck, build, and browser scripts',
171
+ ' --report <path> Write the machine-readable rollout report as JSON',
136
172
  ''
137
173
  ].join('\n');
138
174
  // eslint-disable-next-line complexity -- Command dispatch keeps CLI exit behavior and output in one place.
@@ -159,6 +195,22 @@ const run = async () => {
159
195
  output = formatList(registry);
160
196
  break;
161
197
  }
198
+ case 'rollout': {
199
+ const rollout = getRolloutArguments();
200
+ const report = await runConsumerRollout({
201
+ allowDirty,
202
+ apply: applyRollout,
203
+ exclude: excludeValues,
204
+ ...(reportPath ? { report: reportPath } : {}),
205
+ repositories: rollout.repositories,
206
+ ...(rollout.targetVersion ? { targetVersion: rollout.targetVersion } : {}),
207
+ verify: !noVerify
208
+ });
209
+ output = formatConsumerRollout(report);
210
+ if (!consumerRolloutSucceeded(report))
211
+ process.exitCode = 1;
212
+ break;
213
+ }
162
214
  case 'show': {
163
215
  if (name)
164
216
  output = formatItem(name, registry);
@@ -0,0 +1,52 @@
1
+ export type LumenFramework = 'astro' | 'elements' | 'react';
2
+ export interface ConsumerReference {
3
+ file: string;
4
+ packageName: string;
5
+ source: 'catalog' | 'manifest' | 'lockfile';
6
+ version: string;
7
+ }
8
+ export interface ConsumerInventory {
9
+ branch: string | undefined;
10
+ currentCommit: string | undefined;
11
+ dirty: boolean;
12
+ frameworks: LumenFramework[];
13
+ nodeEngine: string | undefined;
14
+ nodeEngineSatisfied: boolean | undefined;
15
+ packageManager: string | undefined;
16
+ references: ConsumerReference[];
17
+ repository: string;
18
+ resolvedVersions: Record<string, string[]>;
19
+ targetVersion?: string;
20
+ valid: boolean;
21
+ warnings: string[];
22
+ }
23
+ export interface ConsumerRolloutOptions {
24
+ allowDirty?: boolean;
25
+ apply?: boolean;
26
+ exclude?: string[];
27
+ report?: string;
28
+ repositories: string[];
29
+ targetVersion?: string;
30
+ verify?: boolean;
31
+ }
32
+ export interface ConsumerRolloutReport {
33
+ generatedAt: string;
34
+ mode: 'apply' | 'inventory';
35
+ repositories: (ConsumerInventory & {
36
+ commands: {
37
+ command: string;
38
+ ok: boolean;
39
+ output: string;
40
+ }[];
41
+ })[];
42
+ targetVersion?: string;
43
+ }
44
+ export declare const consumerRolloutSucceeded: (report: ConsumerRolloutReport) => boolean;
45
+ export declare const satisfiesNodeEngine: (engine: string | undefined, currentVersion?: string) => boolean | undefined;
46
+ export declare const inspectLumenConsumer: (repository: string, targetVersion?: string) => Promise<ConsumerInventory>;
47
+ export declare const updateLumenManifestSource: (source: string, targetVersion: string) => string;
48
+ export declare const updateLumenWorkspaceSource: (source: string, targetVersion: string) => string;
49
+ export declare const addReleaseAgeTarget: (source: string, packages: string[], version: string) => string;
50
+ export declare const runConsumerRollout: (options: ConsumerRolloutOptions) => Promise<ConsumerRolloutReport>;
51
+ export declare const formatConsumerRollout: (report: ConsumerRolloutReport) => string;
52
+ //# sourceMappingURL=consumer-rollout.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consumer-rollout.d.ts","sourceRoot":"","sources":["../src/consumer-rollout.ts"],"names":[],"mappings":"AAmBA,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,UAAU,GAAG,OAAO,CAAA;AAE3D,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,SAAS,GAAG,UAAU,GAAG,UAAU,CAAA;IAC3C,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAA;IAC1B,aAAa,EAAE,MAAM,GAAG,SAAS,CAAA;IACjC,KAAK,EAAE,OAAO,CAAA;IACd,UAAU,EAAE,cAAc,EAAE,CAAA;IAC5B,UAAU,EAAE,MAAM,GAAG,SAAS,CAAA;IAC9B,mBAAmB,EAAE,OAAO,GAAG,SAAS,CAAA;IACxC,cAAc,EAAE,MAAM,GAAG,SAAS,CAAA;IAClC,UAAU,EAAE,iBAAiB,EAAE,CAAA;IAC/B,UAAU,EAAE,MAAM,CAAA;IAClB,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAA;IAC1C,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,KAAK,EAAE,OAAO,CAAA;IACd,QAAQ,EAAE,MAAM,EAAE,CAAA;CACnB;AAED,MAAM,WAAW,sBAAsB;IACrC,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAClB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,YAAY,EAAE,MAAM,EAAE,CAAA;IACtB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB;AAED,MAAM,WAAW,qBAAqB;IACpC,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,EAAE,OAAO,GAAG,WAAW,CAAA;IAC3B,YAAY,EAAE,CAAC,iBAAiB,GAAG;QACjC,QAAQ,EAAE;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,EAAE,EAAE,OAAO,CAAC;YAAC,MAAM,EAAE,MAAM,CAAA;SAAE,EAAE,CAAA;KAC7D,CAAC,EAAE,CAAA;IACJ,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,eAAO,MAAM,wBAAwB,GAAI,QAAQ,qBAAqB,KAAG,OAGtE,CAAA;AA8DH,eAAO,MAAM,mBAAmB,GAC9B,QAAQ,MAAM,GAAG,SAAS,EAC1B,uBAAsC,KACrC,OAAO,GAAG,SAIZ,CAAA;AA+DD,eAAO,MAAM,oBAAoB,GAC/B,YAAY,MAAM,EAClB,gBAAgB,MAAM,KACrB,OAAO,CAAC,iBAAiB,CAiG3B,CAAA;AAUD,eAAO,MAAM,yBAAyB,GACpC,QAAQ,MAAM,EACd,eAAe,MAAM,KACpB,MAYF,CAAA;AAED,eAAO,MAAM,0BAA0B,GACrC,QAAQ,MAAM,EACd,eAAe,MAAM,KACpB,MAIF,CAAA;AAuFD,eAAO,MAAM,mBAAmB,GAAI,QAAQ,MAAM,EAAE,UAAU,MAAM,EAAE,EAAE,SAAS,MAAM,KAAG,MAoCzF,CAAA;AAqGD,eAAO,MAAM,kBAAkB,GAC7B,SAAS,sBAAsB,KAC9B,OAAO,CAAC,qBAAqB,CAmD/B,CAAA;AAED,eAAO,MAAM,qBAAqB,GAAI,QAAQ,qBAAqB,KAAG,MAyBrE,CAAA"}
@@ -0,0 +1,397 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { readdir, readFile, stat, writeFile } from 'node:fs/promises';
3
+ import { join, relative, resolve } from 'node:path';
4
+ import { promisify } from 'node:util';
5
+ import { satisfies } from 'semver';
6
+ /* eslint-disable complexity -- Consumer inventory, semver checks, mutation, and verification intentionally evaluate full repository contracts. */
7
+ const execFileAsync = promisify(execFile);
8
+ const lumenPackagePattern = /^@santi020k\/lumen(?:-(?:astro|core|elements|mcp|react))?$/;
9
+ const dependencyFields = [
10
+ 'dependencies',
11
+ 'devDependencies',
12
+ 'optionalDependencies',
13
+ 'peerDependencies'
14
+ ];
15
+ export const consumerRolloutSucceeded = (report) => report.repositories.every(repository => repository.valid && repository.commands.every(command => command.ok));
16
+ const isLumenPackage = (name) => lumenPackagePattern.test(name);
17
+ const pathExists = async (path) => {
18
+ try {
19
+ await stat(path);
20
+ return true;
21
+ }
22
+ catch {
23
+ return false;
24
+ }
25
+ };
26
+ const findPackageManifests = async (root) => {
27
+ const results = [];
28
+ const visit = async (directory) => {
29
+ const entries = await readdir(directory, { withFileTypes: true });
30
+ for (const entry of entries) {
31
+ if (entry.name === 'node_modules' || entry.name === '.git' || entry.name.startsWith('.turbo'))
32
+ continue;
33
+ const path = join(directory, entry.name);
34
+ if (entry.isDirectory()) {
35
+ await visit(path);
36
+ }
37
+ else if (entry.name === 'package.json') {
38
+ results.push(path);
39
+ }
40
+ }
41
+ };
42
+ await visit(root);
43
+ return results;
44
+ };
45
+ const readManifest = async (path) => JSON.parse(await readFile(path, 'utf8'));
46
+ const readGitValue = async (root, args) => {
47
+ try {
48
+ const { stdout } = await execFileAsync('git', ['-C', root, ...args]);
49
+ return stdout.trim() || undefined;
50
+ }
51
+ catch {
52
+ return undefined;
53
+ }
54
+ };
55
+ export const satisfiesNodeEngine = (engine, currentVersion = process.versions.node) => {
56
+ if (!engine)
57
+ return undefined;
58
+ return satisfies(currentVersion, engine);
59
+ };
60
+ const collectManifestReferences = (manifest, file, root) => dependencyFields.flatMap(field => Object.entries(manifest[field] ?? {})
61
+ .filter(([packageName]) => isLumenPackage(packageName))
62
+ .map(([packageName, version]) => ({
63
+ file: relative(root, file),
64
+ packageName,
65
+ source: 'manifest',
66
+ version
67
+ })));
68
+ const collectYamlReferences = (source, file) => {
69
+ const references = [];
70
+ for (const line of source.split(/\r?\n/)) {
71
+ const match = /^\s*['"]?(@santi020k\/lumen(?:-(?:astro|core|elements|mcp|react))?)['"]?\s*:\s*['"]?([^'"\s#]+)['"]?/.exec(line);
72
+ if (match?.[1] && match[2]) {
73
+ references.push({ file, packageName: match[1], source: 'catalog', version: match[2] });
74
+ }
75
+ }
76
+ return references;
77
+ };
78
+ const collectLockReferences = (source) => {
79
+ const references = [];
80
+ const pattern = /@santi020k\/(lumen(?:-(?:astro|core|elements|mcp|react))?)@(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)/g;
81
+ for (const match of source.matchAll(pattern)) {
82
+ if (match[1] && match[2]) {
83
+ references.push({
84
+ file: 'pnpm-lock.yaml',
85
+ packageName: `@santi020k/${match[1]}`,
86
+ source: 'lockfile',
87
+ version: match[2]
88
+ });
89
+ }
90
+ }
91
+ return references;
92
+ };
93
+ const getResolvedVersions = (references) => {
94
+ const graph = {};
95
+ for (const reference of references.filter(item => item.source === 'lockfile')) {
96
+ graph[reference.packageName] ??= new Set();
97
+ graph[reference.packageName]?.add(reference.version);
98
+ }
99
+ return Object.fromEntries(Object.entries(graph).map(([name, versions]) => [name, [...versions].sort()]));
100
+ };
101
+ export const inspectLumenConsumer = async (repository, targetVersion) => {
102
+ const root = resolve(repository);
103
+ const rootManifestPath = join(root, 'package.json');
104
+ if (!await pathExists(rootManifestPath)) {
105
+ throw new Error(`No package.json found at consumer root: ${root}`);
106
+ }
107
+ const manifestPaths = await findPackageManifests(root);
108
+ const manifests = await Promise.all(manifestPaths.map(async (file) => ({
109
+ file,
110
+ manifest: await readManifest(file)
111
+ })));
112
+ const rootManifest = await readManifest(rootManifestPath);
113
+ const references = manifests.flatMap(({ file, manifest }) => collectManifestReferences(manifest, file, root));
114
+ const workspacePath = join(root, 'pnpm-workspace.yaml');
115
+ const lockPath = join(root, 'pnpm-lock.yaml');
116
+ if (await pathExists(workspacePath)) {
117
+ references.push(...collectYamlReferences(await readFile(workspacePath, 'utf8'), 'pnpm-workspace.yaml'));
118
+ }
119
+ if (await pathExists(lockPath)) {
120
+ references.push(...collectLockReferences(await readFile(lockPath, 'utf8')));
121
+ }
122
+ const resolvedVersions = getResolvedVersions(references);
123
+ const frameworks = ([
124
+ references.some(item => item.packageName === '@santi020k/lumen-astro') && 'astro',
125
+ references.some(item => item.packageName === '@santi020k/lumen-elements') && 'elements',
126
+ references.some(item => item.packageName === '@santi020k/lumen-react') && 'react'
127
+ ].filter(Boolean));
128
+ const warnings = [];
129
+ const nodeEngineSatisfied = satisfiesNodeEngine(rootManifest.engines?.node);
130
+ const status = await readGitValue(root, ['status', '--porcelain']);
131
+ const dirty = Boolean(status);
132
+ if (!/^pnpm@\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(rootManifest.packageManager ?? '')) {
133
+ warnings.push('Missing an exact pnpm packageManager declaration.');
134
+ }
135
+ if (nodeEngineSatisfied === false) {
136
+ warnings.push(`Current Node ${process.versions.node} does not satisfy ${rootManifest.engines?.node ?? 'the declared engine'}.`);
137
+ }
138
+ if (dirty) {
139
+ warnings.push('Repository has uncommitted changes; create an intentional baseline or use --allow-dirty.');
140
+ }
141
+ if (targetVersion) {
142
+ for (const reference of references.filter(item => item.source !== 'lockfile')) {
143
+ const declaredVersion = reference.version.replace(/^[~^]/, '');
144
+ if (reference.version !== 'catalog:' &&
145
+ !reference.version.startsWith('workspace:') &&
146
+ declaredVersion !== targetVersion) {
147
+ warnings.push(`${reference.packageName} declares ${reference.version} in ${reference.file} instead of ${targetVersion}.`);
148
+ }
149
+ }
150
+ for (const [packageName, versions] of Object.entries(resolvedVersions)) {
151
+ if (versions.some(version => version !== targetVersion)) {
152
+ warnings.push(`${packageName} resolves ${versions.join(', ')} instead of only ${targetVersion}.`);
153
+ }
154
+ }
155
+ }
156
+ return {
157
+ branch: await readGitValue(root, ['branch', '--show-current']),
158
+ currentCommit: await readGitValue(root, ['rev-parse', 'HEAD']),
159
+ dirty,
160
+ frameworks,
161
+ nodeEngine: rootManifest.engines?.node,
162
+ nodeEngineSatisfied,
163
+ packageManager: rootManifest.packageManager,
164
+ references,
165
+ repository: root,
166
+ resolvedVersions,
167
+ ...(targetVersion ? { targetVersion } : {}),
168
+ valid: warnings.length === 0,
169
+ warnings
170
+ };
171
+ };
172
+ const replaceRange = (range, targetVersion) => {
173
+ if (range === 'catalog:' || range.startsWith('workspace:') || range.startsWith('file:'))
174
+ return range;
175
+ const prefix = range.startsWith('^') || range.startsWith('~') ? (range.at(0) ?? '') : '';
176
+ return `${prefix}${targetVersion}`;
177
+ };
178
+ export const updateLumenManifestSource = (source, targetVersion) => {
179
+ const manifest = JSON.parse(source);
180
+ for (const field of dependencyFields) {
181
+ const dependencies = manifest[field];
182
+ for (const [name, range] of Object.entries(dependencies ?? {})) {
183
+ if (isLumenPackage(name) && dependencies)
184
+ dependencies[name] = replaceRange(range, targetVersion);
185
+ }
186
+ }
187
+ return `${JSON.stringify(manifest, undefined, 2)}\n`;
188
+ };
189
+ export const updateLumenWorkspaceSource = (source, targetVersion) => source.replaceAll(/^(\s*['"]?@santi020k\/lumen(?:-(?:astro|core|elements|mcp|react))?['"]?\s*:\s*['"]?)([~^]?)(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)(['"]?\s*(?:#.*)?)$/gm, (_match, before, prefix, _version, after) => `${before}${prefix}${targetVersion}${after}`);
190
+ const getVerificationScripts = (manifest, frameworks) => {
191
+ const scripts = manifest.scripts ?? {};
192
+ const preferred = [
193
+ frameworks.includes('astro') && 'check',
194
+ (frameworks.includes('react') || frameworks.includes('elements')) && 'typecheck',
195
+ 'build',
196
+ scripts['test:browser'] && 'test:browser',
197
+ scripts['test:e2e'] && 'test:e2e'
198
+ ].filter((script) => Boolean(script && scripts[script]));
199
+ return [...new Set(preferred)];
200
+ };
201
+ const runCommand = async (root, executable, args) => {
202
+ const command = [executable, ...args].join(' ');
203
+ try {
204
+ const { stderr, stdout } = await execFileAsync(executable, args, {
205
+ cwd: root,
206
+ maxBuffer: 10 * 1024 * 1024
207
+ });
208
+ return { command, ok: true, output: `${stdout}${stderr}`.trim() };
209
+ }
210
+ catch (error) {
211
+ const detail = error;
212
+ return {
213
+ command,
214
+ ok: false,
215
+ output: `${detail.stdout ?? ''}${detail.stderr ?? ''}`.trim() || detail.message || 'Command failed.'
216
+ };
217
+ }
218
+ };
219
+ const parseFlowSequence = (source) => {
220
+ const match = /^\[([\s\S]*)\]\s*(?:#.*)?$/.exec(source.trim());
221
+ if (!match)
222
+ return undefined;
223
+ const values = [];
224
+ let current = '';
225
+ let quote;
226
+ for (const character of match[1] ?? '') {
227
+ if (quote) {
228
+ current += character;
229
+ if (character === quote)
230
+ quote = undefined;
231
+ }
232
+ else if (character === '"' || character === "'") {
233
+ current += character;
234
+ quote = character;
235
+ }
236
+ else if (character === ',') {
237
+ values.push(current);
238
+ current = '';
239
+ }
240
+ else {
241
+ current += character;
242
+ }
243
+ }
244
+ if (current.trim())
245
+ values.push(current);
246
+ return values.map(value => {
247
+ const trimmed = value.trim();
248
+ if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
249
+ (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
250
+ return trimmed.slice(1, -1);
251
+ }
252
+ return trimmed;
253
+ }).filter(Boolean);
254
+ };
255
+ export const addReleaseAgeTarget = (source, packages, version) => {
256
+ if (!/^\s*minimumReleaseAgeExclude\s*:/m.test(source))
257
+ return source;
258
+ const lines = source.split(/\r?\n/);
259
+ const index = lines.findIndex(line => /^\s*minimumReleaseAgeExclude\s*:/.test(line));
260
+ const declaration = lines[index] ?? '';
261
+ const rootIndent = /^\s*/.exec(declaration)?.[0] ?? '';
262
+ const indent = `${rootIndent} `;
263
+ const declarationValue = declaration.slice(declaration.indexOf(':') + 1);
264
+ const inlineValues = parseFlowSequence(declarationValue);
265
+ if (declarationValue.trim() && !inlineValues)
266
+ return source;
267
+ const existing = new Set([
268
+ ...(inlineValues ?? []),
269
+ ...lines.flatMap(line => {
270
+ const value = /^\s*-\s*['"]?([^'"]+)['"]?\s*$/.exec(line)?.[1];
271
+ return value ? [value] : [];
272
+ })
273
+ ]);
274
+ const additions = packages
275
+ .map(packageName => `${packageName}@${version}`)
276
+ .filter(value => !existing.has(value))
277
+ .map(value => `${indent}- "${value}"`);
278
+ if (declaration.slice(declaration.indexOf(':') + 1).trim()) {
279
+ lines[index] = `${rootIndent}minimumReleaseAgeExclude:`;
280
+ additions.unshift(...(inlineValues ?? []).map(value => `${indent}- "${value}"`));
281
+ }
282
+ lines.splice(index + 1, 0, ...additions);
283
+ return lines.join('\n');
284
+ };
285
+ const removeObsoleteReleaseAgeEntries = (source, targetVersion) => source.split(/\r?\n/)
286
+ .filter(line => {
287
+ const match = /^\s*-\s*['"]?(@santi020k\/lumen(?:-(?:astro|core|elements|mcp|react))?)@([^'"]+)['"]?\s*$/.exec(line);
288
+ return !match || match[2] === targetVersion;
289
+ })
290
+ .join('\n');
291
+ const upgradeConsumer = async (inventory, targetVersion, allowDirty, verify) => {
292
+ if (inventory.dirty && !allowDirty) {
293
+ throw new Error(`${inventory.repository} is dirty. Commit a baseline or pass --allow-dirty.`);
294
+ }
295
+ if (!/^pnpm@\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(inventory.packageManager ?? '')) {
296
+ throw new Error(`${inventory.repository} must declare an exact pnpm packageManager version.`);
297
+ }
298
+ if (inventory.nodeEngineSatisfied === false) {
299
+ throw new Error(`${inventory.repository} must be upgraded under Node ${inventory.nodeEngine ?? 'matching its engine declaration'}.`);
300
+ }
301
+ const manifestPaths = await findPackageManifests(inventory.repository);
302
+ const desiredManifests = new Map();
303
+ for (const path of manifestPaths) {
304
+ const source = await readFile(path, 'utf8');
305
+ const updated = updateLumenManifestSource(source, targetVersion);
306
+ desiredManifests.set(path, updated);
307
+ if (updated !== source)
308
+ await writeFile(path, updated);
309
+ }
310
+ const workspacePath = join(inventory.repository, 'pnpm-workspace.yaml');
311
+ if (await pathExists(workspacePath)) {
312
+ const source = await readFile(workspacePath, 'utf8');
313
+ const packages = [...new Set(inventory.references.map(item => item.packageName))];
314
+ const updated = addReleaseAgeTarget(updateLumenWorkspaceSource(source, targetVersion), packages, targetVersion);
315
+ if (updated !== source)
316
+ await writeFile(workspacePath, updated);
317
+ }
318
+ const packageNames = [...new Set(inventory.references.map(item => item.packageName))];
319
+ const commands = [await runCommand(inventory.repository, 'corepack', [
320
+ 'pnpm',
321
+ 'update',
322
+ '--recursive',
323
+ ...packageNames.map(packageName => `${packageName}@${targetVersion}`)
324
+ ])];
325
+ if (!commands[0]?.ok)
326
+ return commands;
327
+ for (const [path, source] of desiredManifests) {
328
+ await writeFile(path, source);
329
+ }
330
+ commands.push(await runCommand(inventory.repository, 'corepack', ['pnpm', 'install']));
331
+ if (!commands.at(-1)?.ok)
332
+ return commands;
333
+ if (await pathExists(workspacePath)) {
334
+ const source = await readFile(workspacePath, 'utf8');
335
+ const updated = removeObsoleteReleaseAgeEntries(source, targetVersion);
336
+ if (updated !== source) {
337
+ await writeFile(workspacePath, updated);
338
+ commands.push(await runCommand(inventory.repository, 'corepack', ['pnpm', 'install']));
339
+ }
340
+ }
341
+ if (verify && commands.every(command => command.ok)) {
342
+ const manifest = await readManifest(join(inventory.repository, 'package.json'));
343
+ for (const script of getVerificationScripts(manifest, inventory.frameworks)) {
344
+ const result = await runCommand(inventory.repository, 'corepack', ['pnpm', 'run', script]);
345
+ commands.push(result);
346
+ if (!result.ok)
347
+ break;
348
+ }
349
+ }
350
+ return commands;
351
+ };
352
+ export const runConsumerRollout = async (options) => {
353
+ if (options.apply && !options.targetVersion) {
354
+ throw new Error('A target version is required when applying a consumer rollout.');
355
+ }
356
+ const excluded = new Set((options.exclude ?? []).map(path => resolve(path)));
357
+ const repositories = options.repositories.map(path => resolve(path))
358
+ .filter(path => !excluded.has(path));
359
+ const report = {
360
+ generatedAt: new Date().toISOString(),
361
+ mode: options.apply ? 'apply' : 'inventory',
362
+ repositories: [],
363
+ ...(options.targetVersion ? { targetVersion: options.targetVersion } : {})
364
+ };
365
+ for (const repository of repositories) {
366
+ const before = await inspectLumenConsumer(repository, options.targetVersion);
367
+ const commands = options.apply
368
+ ? await upgradeConsumer(before, options.targetVersion ?? '', options.allowDirty ?? false, options.verify ?? true)
369
+ : [];
370
+ const after = options.apply
371
+ ? await inspectLumenConsumer(repository, options.targetVersion)
372
+ : before;
373
+ if (options.apply) {
374
+ after.warnings = after.warnings.filter(warning => !warning.startsWith('Repository has uncommitted changes'));
375
+ after.valid = after.warnings.length === 0;
376
+ }
377
+ report.repositories.push({ ...after, commands });
378
+ if (commands.some(command => !command.ok))
379
+ break;
380
+ }
381
+ if (options.report) {
382
+ await writeFile(resolve(options.report), `${JSON.stringify(report, undefined, 2)}\n`);
383
+ }
384
+ return report;
385
+ };
386
+ export const formatConsumerRollout = (report) => {
387
+ const lines = [
388
+ `Lumen consumer ${report.mode} (${report.repositories.length} repositories)`,
389
+ ...(report.targetVersion ? [`Target: ${report.targetVersion}`] : [])
390
+ ];
391
+ for (const repository of report.repositories) {
392
+ lines.push('', repository.repository, ` package manager: ${repository.packageManager ?? 'missing'}`, ` node engine: ${repository.nodeEngine ?? 'unspecified'}`, ` frameworks: ${repository.frameworks.join(', ') || 'none'}`, ` resolved graph: ${JSON.stringify(repository.resolvedVersions)}`, ` status: ${repository.valid && repository.commands.every(command => command.ok) ? 'ok' : 'attention required'}`);
393
+ lines.push(...repository.warnings.map(warning => ` warning: ${warning}`));
394
+ lines.push(...repository.commands.map(command => ` ${command.ok ? 'pass' : 'fail'}: ${command.command}`));
395
+ }
396
+ return lines.join('\n');
397
+ };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export { type ConsumerInventory, type ConsumerReference, type ConsumerRolloutOptions, type ConsumerRolloutReport, formatConsumerRollout, inspectLumenConsumer, type LumenFramework, runConsumerRollout, updateLumenManifestSource, updateLumenWorkspaceSource } from './consumer-rollout.js';
1
2
  export { addLumenRegistryItem, getLumenRegistryEntries, getLumenRegistryItem, loadLumenRegistry, type LumenAddOptions, type LumenAddResult, type LumenAddTarget, type LumenMergeConflict, type LumenRegistry, lumenRegistry, type LumenRegistryComponent, type LumenRegistryEntry, type LumenRegistryFile, type LumenRegistryItem, type LumenRegistryLoadOptions } from './registry.js';
2
3
  export { applyDataViewState, canPlaceScheduleEvent, coerceThemeBuilderExportFormat, coerceThemeBuilderMode, coerceThemeBuilderScheme, composeClassName, createDataViewRequestUrl, createDataViewSearchParams, createDataViewServerRequest, createDataViewState, createDataViewStorageKey, createScheduleSlots, createScheduleStorageKey, createThemeBuilderTokens, createThemePalette, expandRecurringScheduleEvent, exportThemeBuilderCss, exportThemeBuilderValue, exportThemeCss, filterDataRecords, getContrastRatio, getScheduleConflicts, getVirtualRange, loadDataViewState, loadScheduleEvents, type LumenCodeToken, lumenCodeTokenClassNames, type LumenCodeTokenKind, lumenColors, type LumenColorTokenName, lumenColorTokenNames, type LumenComponentName, lumenComponentNames, type LumenContrastScore, lumenDarkTheme, type LumenDataRecord, type LumenDataViewServerRequest, type LumenDataViewState, lumenGlass, type LumenGlassColorTokenName, lumenGlassColorTokenNames, type LumenGlassEffectTokenName, lumenGlassEffectTokenNames, type LumenGlassTokenName, lumenGlassTokenNames, type LumenKeyValueReader, type LumenKeyValueStorage, type LumenKeyValueWriter, lumenLightTheme, lumenPackages, type LumenPackageTarget, type LumenScheduleConflict, type LumenScheduleEvent, type LumenScheduleKeyValueReader, type LumenScheduleKeyValueStorage, type LumenScheduleKeyValueWriter, type LumenScheduleResizeEdge, type LumenScheduleResizeOptions, type LumenScheduleResource, type LumenScheduleSlot, type LumenSemanticColorTokenName, lumenSemanticColorTokenNames, type LumenSortDirection, lumenThemeAttribute, type LumenThemeBuilderExportFormat, type LumenThemeBuilderMode, type LumenThemeBuilderOptions, type LumenThemeBuilderResult, type LumenThemeBuilderScheme, type LumenThemeContrastSuggestion, type LumenThemeTokens, lumenTokenNames, mergeThemeTokens, moveScheduleEvent, normalizeLumenCode, normalizeThemeBuilderHex, normalizeThemeBuilderHue, paginateDataRecords, parseDataViewState, parseScheduleEvents, parseThemeCss, pinDataViewColumn, renderLumenCodeHtml, resizeScheduleEvent, resizeScheduleEvents, saveDataViewState, saveScheduleEvents, scheduleEventsOverlap, scoreThemeContrast, serializeDataViewState, serializeScheduleEvents, sortDataRecords, suggestReadableInk, themeBuilderHexToHsl, toggleDataViewSelection, tokenizeLumenCode, tuneThemeContrast, unpinDataViewColumn } from '@santi020k/lumen-core';
3
4
  export declare const lumen: {
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,oBAAoB,EACpB,uBAAuB,EACvB,oBAAoB,EACpB,iBAAiB,EACjB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,aAAa,EAClB,aAAa,EACb,KAAK,sBAAsB,EAC3B,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EACtB,KAAK,wBAAwB,EAC9B,MAAM,eAAe,CAAA;AACtB,OAAO,EACL,kBAAkB,EAClB,qBAAqB,EACrB,8BAA8B,EAC9B,sBAAsB,EACtB,wBAAwB,EACxB,gBAAgB,EAChB,wBAAwB,EACxB,0BAA0B,EAC1B,2BAA2B,EAC3B,mBAAmB,EACnB,wBAAwB,EACxB,mBAAmB,EACnB,wBAAwB,EACxB,wBAAwB,EACxB,kBAAkB,EAClB,4BAA4B,EAC5B,qBAAqB,EACrB,uBAAuB,EACvB,cAAc,EACd,iBAAiB,EACjB,gBAAgB,EAChB,oBAAoB,EACpB,eAAe,EACf,iBAAiB,EACjB,kBAAkB,EAClB,KAAK,cAAc,EACnB,wBAAwB,EACxB,KAAK,kBAAkB,EACvB,WAAW,EACX,KAAK,mBAAmB,EACxB,oBAAoB,EACpB,KAAK,kBAAkB,EACvB,mBAAmB,EACnB,KAAK,kBAAkB,EACvB,cAAc,EACd,KAAK,eAAe,EACpB,KAAK,0BAA0B,EAC/B,KAAK,kBAAkB,EACvB,UAAU,EACV,KAAK,wBAAwB,EAC7B,yBAAyB,EACzB,KAAK,yBAAyB,EAC9B,0BAA0B,EAC1B,KAAK,mBAAmB,EACxB,oBAAoB,EACpB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,EACxB,eAAe,EACf,aAAa,EACb,KAAK,kBAAkB,EACvB,KAAK,qBAAqB,EAC1B,KAAK,kBAAkB,EACvB,KAAK,2BAA2B,EAChC,KAAK,4BAA4B,EACjC,KAAK,2BAA2B,EAChC,KAAK,uBAAuB,EAC5B,KAAK,0BAA0B,EAC/B,KAAK,qBAAqB,EAC1B,KAAK,iBAAiB,EACtB,KAAK,2BAA2B,EAChC,4BAA4B,EAC5B,KAAK,kBAAkB,EACvB,mBAAmB,EACnB,KAAK,6BAA6B,EAClC,KAAK,qBAAqB,EAC1B,KAAK,wBAAwB,EAC7B,KAAK,uBAAuB,EAC5B,KAAK,uBAAuB,EAC5B,KAAK,4BAA4B,EACjC,KAAK,gBAAgB,EACrB,eAAe,EACf,gBAAgB,EAChB,iBAAiB,EACjB,kBAAkB,EAClB,wBAAwB,EACxB,wBAAwB,EACxB,mBAAmB,EACnB,kBAAkB,EAClB,mBAAmB,EACnB,aAAa,EACb,iBAAiB,EACjB,mBAAmB,EACnB,mBAAmB,EACnB,oBAAoB,EACpB,iBAAiB,EACjB,kBAAkB,EAClB,qBAAqB,EACrB,kBAAkB,EAClB,sBAAsB,EACtB,uBAAuB,EACvB,eAAe,EACf,kBAAkB,EAClB,oBAAoB,EACpB,uBAAuB,EACvB,iBAAiB,EACjB,iBAAiB,EACjB,mBAAmB,EACpB,MAAM,uBAAuB,CAAA;AAE9B,eAAO,MAAM,KAAK;;;;CAIR,CAAA;AAEV,OAAO,EACL,kBAAkB,EAClB,KAAK,sBAAsB,EAC5B,MAAM,kBAAkB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EACtB,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAC1B,qBAAqB,EACrB,oBAAoB,EACpB,KAAK,cAAc,EACnB,kBAAkB,EAClB,yBAAyB,EACzB,0BAA0B,EAC3B,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EACL,oBAAoB,EACpB,uBAAuB,EACvB,oBAAoB,EACpB,iBAAiB,EACjB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,aAAa,EAClB,aAAa,EACb,KAAK,sBAAsB,EAC3B,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EACtB,KAAK,wBAAwB,EAC9B,MAAM,eAAe,CAAA;AACtB,OAAO,EACL,kBAAkB,EAClB,qBAAqB,EACrB,8BAA8B,EAC9B,sBAAsB,EACtB,wBAAwB,EACxB,gBAAgB,EAChB,wBAAwB,EACxB,0BAA0B,EAC1B,2BAA2B,EAC3B,mBAAmB,EACnB,wBAAwB,EACxB,mBAAmB,EACnB,wBAAwB,EACxB,wBAAwB,EACxB,kBAAkB,EAClB,4BAA4B,EAC5B,qBAAqB,EACrB,uBAAuB,EACvB,cAAc,EACd,iBAAiB,EACjB,gBAAgB,EAChB,oBAAoB,EACpB,eAAe,EACf,iBAAiB,EACjB,kBAAkB,EAClB,KAAK,cAAc,EACnB,wBAAwB,EACxB,KAAK,kBAAkB,EACvB,WAAW,EACX,KAAK,mBAAmB,EACxB,oBAAoB,EACpB,KAAK,kBAAkB,EACvB,mBAAmB,EACnB,KAAK,kBAAkB,EACvB,cAAc,EACd,KAAK,eAAe,EACpB,KAAK,0BAA0B,EAC/B,KAAK,kBAAkB,EACvB,UAAU,EACV,KAAK,wBAAwB,EAC7B,yBAAyB,EACzB,KAAK,yBAAyB,EAC9B,0BAA0B,EAC1B,KAAK,mBAAmB,EACxB,oBAAoB,EACpB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,EACxB,eAAe,EACf,aAAa,EACb,KAAK,kBAAkB,EACvB,KAAK,qBAAqB,EAC1B,KAAK,kBAAkB,EACvB,KAAK,2BAA2B,EAChC,KAAK,4BAA4B,EACjC,KAAK,2BAA2B,EAChC,KAAK,uBAAuB,EAC5B,KAAK,0BAA0B,EAC/B,KAAK,qBAAqB,EAC1B,KAAK,iBAAiB,EACtB,KAAK,2BAA2B,EAChC,4BAA4B,EAC5B,KAAK,kBAAkB,EACvB,mBAAmB,EACnB,KAAK,6BAA6B,EAClC,KAAK,qBAAqB,EAC1B,KAAK,wBAAwB,EAC7B,KAAK,uBAAuB,EAC5B,KAAK,uBAAuB,EAC5B,KAAK,4BAA4B,EACjC,KAAK,gBAAgB,EACrB,eAAe,EACf,gBAAgB,EAChB,iBAAiB,EACjB,kBAAkB,EAClB,wBAAwB,EACxB,wBAAwB,EACxB,mBAAmB,EACnB,kBAAkB,EAClB,mBAAmB,EACnB,aAAa,EACb,iBAAiB,EACjB,mBAAmB,EACnB,mBAAmB,EACnB,oBAAoB,EACpB,iBAAiB,EACjB,kBAAkB,EAClB,qBAAqB,EACrB,kBAAkB,EAClB,sBAAsB,EACtB,uBAAuB,EACvB,eAAe,EACf,kBAAkB,EAClB,oBAAoB,EACpB,uBAAuB,EACvB,iBAAiB,EACjB,iBAAiB,EACjB,mBAAmB,EACpB,MAAM,uBAAuB,CAAA;AAE9B,eAAO,MAAM,KAAK;;;;CAIR,CAAA;AAEV,OAAO,EACL,kBAAkB,EAClB,KAAK,sBAAsB,EAC5B,MAAM,kBAAkB,CAAA"}
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ export { formatConsumerRollout, inspectLumenConsumer, runConsumerRollout, updateLumenManifestSource, updateLumenWorkspaceSource } from './consumer-rollout.js';
1
2
  export { addLumenRegistryItem, getLumenRegistryEntries, getLumenRegistryItem, loadLumenRegistry, lumenRegistry } from './registry.js';
2
3
  export { applyDataViewState, canPlaceScheduleEvent, coerceThemeBuilderExportFormat, coerceThemeBuilderMode, coerceThemeBuilderScheme, composeClassName, createDataViewRequestUrl, createDataViewSearchParams, createDataViewServerRequest, createDataViewState, createDataViewStorageKey, createScheduleSlots, createScheduleStorageKey, createThemeBuilderTokens, createThemePalette, expandRecurringScheduleEvent, exportThemeBuilderCss, exportThemeBuilderValue, exportThemeCss, filterDataRecords, getContrastRatio, getScheduleConflicts, getVirtualRange, loadDataViewState, loadScheduleEvents, lumenCodeTokenClassNames, lumenColors, lumenColorTokenNames, lumenComponentNames, lumenDarkTheme, lumenGlass, lumenGlassColorTokenNames, lumenGlassEffectTokenNames, lumenGlassTokenNames, lumenLightTheme, lumenPackages, lumenSemanticColorTokenNames, lumenThemeAttribute, lumenTokenNames, mergeThemeTokens, moveScheduleEvent, normalizeLumenCode, normalizeThemeBuilderHex, normalizeThemeBuilderHue, paginateDataRecords, parseDataViewState, parseScheduleEvents, parseThemeCss, pinDataViewColumn, renderLumenCodeHtml, resizeScheduleEvent, resizeScheduleEvents, saveDataViewState, saveScheduleEvents, scheduleEventsOverlap, scoreThemeContrast, serializeDataViewState, serializeScheduleEvents, sortDataRecords, suggestReadableInk, themeBuilderHexToHsl, toggleDataViewSelection, tokenizeLumenCode, tuneThemeContrast, unpinDataViewColumn } from '@santi020k/lumen-core';
3
4
  export const lumen = {
@@ -190,6 +190,13 @@ export declare const lumenRegistry: {
190
190
  readonly category: "Data display";
191
191
  readonly files: readonly ["packages/astro/components/Code.astro", "packages/astro/styles/lumen.css", "packages/astro/runtime/UIPrimitives.astro"];
192
192
  readonly dependencies: readonly ["styles", "runtime"];
193
+ }, {
194
+ readonly name: "CodeTabs";
195
+ readonly type: "component";
196
+ readonly description: "Groups related code examples into accessible, persistent tabs.";
197
+ readonly category: "Data display";
198
+ readonly files: readonly ["packages/astro/components/CodeTabs.astro", "packages/astro/components/Code.astro", "packages/astro/components/Tabs.astro", "packages/astro/styles/lumen.css", "packages/astro/runtime/UIPrimitives.astro"];
199
+ readonly dependencies: readonly ["Code", "Tabs", "styles", "runtime"];
193
200
  }, {
194
201
  readonly name: "Combobox";
195
202
  readonly type: "component";
@@ -1 +1 @@
1
- {"version":3,"file":"registry-data.d.ts","sourceRoot":"","sources":["../src/registry-data.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyrDQ,CAAA"}
1
+ {"version":3,"file":"registry-data.d.ts","sourceRoot":"","sources":["../src/registry-data.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4sDQ,CAAA"}
@@ -393,6 +393,25 @@ export const lumenRegistry = {
393
393
  "runtime"
394
394
  ]
395
395
  },
396
+ {
397
+ "name": "CodeTabs",
398
+ "type": "component",
399
+ "description": "Groups related code examples into accessible, persistent tabs.",
400
+ "category": "Data display",
401
+ "files": [
402
+ "packages/astro/components/CodeTabs.astro",
403
+ "packages/astro/components/Code.astro",
404
+ "packages/astro/components/Tabs.astro",
405
+ "packages/astro/styles/lumen.css",
406
+ "packages/astro/runtime/UIPrimitives.astro"
407
+ ],
408
+ "dependencies": [
409
+ "Code",
410
+ "Tabs",
411
+ "styles",
412
+ "runtime"
413
+ ]
414
+ },
396
415
  {
397
416
  "name": "Combobox",
398
417
  "type": "component",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@santi020k/lumen",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -38,10 +38,12 @@
38
38
  "lumen": "./bin/lumen.mjs"
39
39
  },
40
40
  "dependencies": {
41
- "@santi020k/lumen-core": "^0.2.0"
41
+ "semver": "^7.8.0",
42
+ "@santi020k/lumen-core": "^0.3.0"
42
43
  },
43
44
  "devDependencies": {
44
45
  "@types/node": "^26.1.1",
46
+ "@types/semver": "^7.7.1",
45
47
  "@vitest/coverage-v8": "^4.1.10",
46
48
  "typescript": "^6.0.3",
47
49
  "tailwindcss": "^4.3.3",
package/styles.css CHANGED
@@ -264,7 +264,7 @@
264
264
  font-family: var(--ui-font);
265
265
  }
266
266
 
267
- :where(.ui-button, .ui-back-to-top, .ui-code__copy, .ui-input, .ui-language-toggle, .ui-link, .ui-select, .ui-skip-link, .ui-textarea, .ui-checkbox, .ui-switch, .ui-toggle, .ui-slider, .ui-color-picker, .ui-resizable__handle):focus-visible {
267
+ :where(.ui-button, .ui-back-to-top, .ui-code__copy, .ui-input, .ui-language-toggle, .ui-link, .ui-select, .ui-skip-link, .ui-textarea, .ui-checkbox, .ui-switch, .ui-toggle, .ui-theme-toggle, .ui-slider, .ui-color-picker, .ui-resizable__handle):focus-visible {
268
268
  outline: 2px solid hsl(var(--brand));
269
269
  outline-offset: 2px;
270
270
  }
@@ -1716,16 +1716,28 @@
1716
1716
 
1717
1717
  .ui-accordion {
1718
1718
  display: grid;
1719
- gap: 0.5rem;
1719
+ gap: 0.75rem;
1720
1720
  }
1721
1721
 
1722
1722
  .ui-accordion details,
1723
1723
  .ui-collapsible {
1724
+ overflow: clip;
1724
1725
  border: 1px solid hsl(var(--line));
1725
- border-radius: var(--ui-radius);
1726
+ border-radius: var(--ui-radius-lg);
1726
1727
  background: hsl(var(--surface));
1727
1728
  color: hsl(var(--ink-soft));
1728
- padding: 0.85rem 1rem;
1729
+ box-shadow: var(--ui-shadow-sm);
1730
+ transition:
1731
+ border-color var(--ui-duration-fast) var(--ui-ease),
1732
+ box-shadow var(--ui-duration-fast) var(--ui-ease);
1733
+ }
1734
+
1735
+ .ui-accordion details[open],
1736
+ .ui-collapsible[open] {
1737
+ border-color: hsl(var(--brand) / 0.38);
1738
+ box-shadow:
1739
+ 0 0 0 1px hsl(var(--brand) / 0.08),
1740
+ var(--ui-shadow-sm);
1729
1741
  }
1730
1742
 
1731
1743
  .ui-accordion--flush {
@@ -1734,11 +1746,18 @@
1734
1746
  }
1735
1747
 
1736
1748
  .ui-accordion--flush details {
1749
+ overflow: visible;
1737
1750
  border: 0;
1738
1751
  border-bottom: 1px solid hsl(var(--line));
1739
1752
  border-radius: 0;
1740
1753
  background: transparent;
1741
- padding: 1.25rem 0;
1754
+ box-shadow: none;
1755
+ }
1756
+
1757
+ .ui-accordion--flush details[open] {
1758
+ border-color: hsl(var(--line));
1759
+ background: transparent;
1760
+ box-shadow: none;
1742
1761
  }
1743
1762
 
1744
1763
  .ui-accordion summary,
@@ -1751,6 +1770,32 @@
1751
1770
  list-style: none;
1752
1771
  color: hsl(var(--ink));
1753
1772
  font-weight: 650;
1773
+ line-height: 1.35;
1774
+ min-height: 3.5rem;
1775
+ padding: 1rem 1.125rem;
1776
+ transition:
1777
+ background-color var(--ui-duration-fast) var(--ui-ease),
1778
+ color var(--ui-duration-fast) var(--ui-ease);
1779
+ }
1780
+
1781
+ .ui-accordion summary:hover,
1782
+ .ui-collapsible summary:hover {
1783
+ background: hsl(var(--surface-muted));
1784
+ }
1785
+
1786
+ .ui-accordion details[open] > summary,
1787
+ .ui-collapsible[open] > summary {
1788
+ color: hsl(var(--brand));
1789
+ }
1790
+
1791
+ .ui-accordion details[open],
1792
+ .ui-collapsible[open] {
1793
+ padding-block-end: 1rem;
1794
+ }
1795
+
1796
+ .ui-accordion details > :not(summary),
1797
+ .ui-collapsible > :not(summary) {
1798
+ margin-inline: 1.125rem;
1754
1799
  }
1755
1800
 
1756
1801
  .ui-accordion summary::-webkit-details-marker,
@@ -1765,33 +1810,90 @@
1765
1810
  border-right: 1.5px solid hsl(var(--ink-muted));
1766
1811
  border-bottom: 1.5px solid hsl(var(--ink-muted));
1767
1812
  content: "";
1813
+ border-radius: 1px;
1814
+ box-shadow: 0 0 0 0.45rem hsl(var(--surface-muted));
1768
1815
  transform: rotate(45deg) translate(-0.1rem, -0.1rem);
1769
- transition: transform var(--ui-duration-fast) var(--ui-ease-standard);
1816
+ transition:
1817
+ border-color var(--ui-duration-fast) var(--ui-ease),
1818
+ box-shadow var(--ui-duration-fast) var(--ui-ease),
1819
+ transform var(--ui-duration-fast) var(--ui-ease);
1770
1820
  }
1771
1821
 
1772
1822
  .ui-accordion details[open] > summary::after,
1773
1823
  .ui-collapsible[open] > summary::after {
1824
+ border-color: hsl(var(--brand));
1825
+ box-shadow: 0 0 0 0.45rem hsl(var(--brand-soft));
1774
1826
  transform: rotate(225deg) translate(-0.1rem, -0.1rem);
1775
1827
  }
1776
1828
 
1777
1829
  .ui-accordion summary:hover::after,
1778
1830
  .ui-collapsible summary:hover::after {
1779
1831
  border-color: hsl(var(--brand));
1832
+ box-shadow: 0 0 0 0.45rem hsl(var(--brand-soft));
1780
1833
  }
1781
1834
 
1782
1835
  .ui-accordion summary:focus-visible,
1783
1836
  .ui-collapsible summary:focus-visible {
1784
- border-radius: calc(var(--ui-radius) * 0.6);
1837
+ border-radius: calc(var(--ui-radius-lg) - 1px);
1785
1838
  outline: 2px solid hsl(var(--brand));
1786
- outline-offset: 0.35rem;
1839
+ outline-offset: -3px;
1840
+ }
1841
+
1842
+ .ui-accordion--flush details[open] {
1843
+ padding-block-end: 1.25rem;
1844
+ }
1845
+
1846
+ .ui-accordion--flush summary {
1847
+ min-height: 4rem;
1848
+ padding: 1.125rem 0.25rem;
1849
+ }
1850
+
1851
+ .ui-accordion--flush summary:hover {
1852
+ border-radius: var(--ui-radius);
1853
+ background: hsl(var(--surface-muted) / 0.72);
1854
+ box-shadow: 0 0 0 0.625rem hsl(var(--surface-muted) / 0.72);
1855
+ }
1856
+
1857
+ .ui-accordion--flush details[open] > summary {
1858
+ color: hsl(var(--ink));
1859
+ }
1860
+
1861
+ .ui-accordion--flush details > :not(summary) {
1862
+ margin-inline: 0.25rem;
1863
+ }
1864
+
1865
+ .ui-accordion--flush details[open] > summary::before {
1866
+ width: 0.2rem;
1867
+ height: 1.25rem;
1868
+ border-radius: 999px;
1869
+ background: hsl(var(--brand));
1870
+ content: "";
1871
+ position: absolute;
1872
+ inset-inline-start: -0.875rem;
1873
+ }
1874
+
1875
+ .ui-accordion--flush summary {
1876
+ position: relative;
1787
1877
  }
1788
1878
 
1789
1879
  .ui-accordion p,
1790
1880
  .ui-collapsible p {
1881
+ color: hsl(var(--ink-soft));
1791
1882
  margin-block-end: 0;
1792
1883
  line-height: 1.6;
1793
1884
  }
1794
1885
 
1886
+ @media (prefers-reduced-motion: reduce) {
1887
+ .ui-accordion details,
1888
+ .ui-collapsible,
1889
+ .ui-accordion summary,
1890
+ .ui-collapsible summary,
1891
+ .ui-accordion summary::after,
1892
+ .ui-collapsible summary::after {
1893
+ transition: none;
1894
+ }
1895
+ }
1896
+
1795
1897
  .ui-dialog,
1796
1898
  .ui-alert-dialog,
1797
1899
  .ui-drawer,
@@ -1817,6 +1919,19 @@
1817
1919
  transform: translate(-50%, -50%);
1818
1920
  }
1819
1921
 
1922
+ .ui-dialog--fullscreen {
1923
+ inset: 0;
1924
+ width: 100%;
1925
+ max-width: none;
1926
+ height: 100dvh;
1927
+ max-height: none;
1928
+ margin: 0;
1929
+ border: 0;
1930
+ border-radius: 0;
1931
+ padding: 0;
1932
+ transform: none;
1933
+ }
1934
+
1820
1935
  .ui-dialog::backdrop,
1821
1936
  .ui-alert-dialog::backdrop,
1822
1937
  .ui-drawer::backdrop,
@@ -2178,6 +2293,96 @@
2178
2293
  color: hsl(var(--ink-soft));
2179
2294
  }
2180
2295
 
2296
+ .ui-code-tabs {
2297
+ display: block;
2298
+ overflow: hidden;
2299
+ border: 1px solid hsl(var(--line));
2300
+ border-radius: var(--ui-radius-lg);
2301
+ background: hsl(var(--surface-muted));
2302
+ }
2303
+
2304
+ .ui-code-tabs .ui-code-tabs__list {
2305
+ display: flex;
2306
+ width: auto;
2307
+ gap: 0.25rem;
2308
+ border: 0;
2309
+ border-bottom: 1px solid hsl(var(--line));
2310
+ border-radius: 0;
2311
+ background: hsl(var(--surface));
2312
+ padding: 0.375rem;
2313
+ }
2314
+
2315
+ .ui-code-tabs .ui-code-tabs__tab {
2316
+ min-width: 4.25rem;
2317
+ border: 0;
2318
+ border-radius: var(--ui-radius-sm);
2319
+ background: transparent;
2320
+ color: hsl(var(--ink-muted));
2321
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
2322
+ font-size: 0.8125rem;
2323
+ font-weight: 500;
2324
+ padding: 0.5rem 0.875rem;
2325
+ transition:
2326
+ background-color 150ms ease,
2327
+ color 150ms ease;
2328
+ }
2329
+
2330
+ .ui-code-tabs .ui-code-tabs__tab:hover {
2331
+ background: hsl(var(--surface-muted));
2332
+ color: hsl(var(--ink));
2333
+ }
2334
+
2335
+ .ui-code-tabs .ui-code-tabs__tab[aria-selected="true"] {
2336
+ background: hsl(var(--brand-soft));
2337
+ box-shadow: none;
2338
+ color: hsl(var(--brand));
2339
+ }
2340
+
2341
+ .ui-code-tabs .ui-code-tabs__tab:focus-visible {
2342
+ outline: 2px solid hsl(var(--brand));
2343
+ outline-offset: 1px;
2344
+ }
2345
+
2346
+ .ui-code-tabs .ui-code-tabs__panel {
2347
+ color: hsl(var(--ink));
2348
+ }
2349
+
2350
+ .ui-code-tabs .ui-code--block {
2351
+ margin: 0;
2352
+ border: 0;
2353
+ border-radius: 0;
2354
+ background: transparent;
2355
+ box-shadow: none;
2356
+ }
2357
+
2358
+ .ui-code-tabs .ui-code__header {
2359
+ position: absolute;
2360
+ z-index: 1;
2361
+ inset-block-start: 0.75rem;
2362
+ inset-inline-end: 0.75rem;
2363
+ display: block;
2364
+ min-height: 0;
2365
+ border: 0;
2366
+ background: transparent;
2367
+ padding: 0;
2368
+ }
2369
+
2370
+ .ui-code-tabs .ui-code__dots,
2371
+ .ui-code-tabs .ui-code__meta {
2372
+ display: none;
2373
+ }
2374
+
2375
+ .ui-code-tabs .ui-code--block :where(pre) {
2376
+ min-height: 3.5rem;
2377
+ padding: 1rem 3.75rem 1rem 1.25rem;
2378
+ }
2379
+
2380
+ @media (prefers-reduced-motion: reduce) {
2381
+ .ui-code-tabs .ui-code-tabs__tab {
2382
+ transition: none;
2383
+ }
2384
+ }
2385
+
2181
2386
  .ui-table-wrap,
2182
2387
  .ui-data-table {
2183
2388
  width: 100%;
@@ -4110,6 +4315,16 @@
4110
4315
  }
4111
4316
  }
4112
4317
 
4318
+ .ui-dialog--fullscreen:not([open]) {
4319
+ transform: none;
4320
+ }
4321
+
4322
+ @starting-style {
4323
+ .ui-dialog--fullscreen[open] {
4324
+ transform: none;
4325
+ }
4326
+ }
4327
+
4113
4328
  .ui-drawer {
4114
4329
  transition:
4115
4330
  opacity var(--ui-duration) var(--ui-ease),