@montytools/cli 0.5.4 → 0.5.6
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/bin/monty.mjs +606 -677
- package/lib/compile.mjs +3 -3
- package/lib/schemaCodegen.mjs +7 -3
- package/lib/schemaPull.mjs +36 -50
- package/lib/styleLint.mjs +175 -0
- package/lib/tokens-manifest.mjs +90 -0
- package/lib/views.mjs +19 -0
- package/package.json +3 -3
- package/skills/monty-build/SKILL.md +92 -105
- package/skills/monty-design/SKILL.md +104 -55
- package/template/AGENTS.md +53 -33
- package/template/index.html +5 -5
- package/template/package.json +2 -2
- package/template/src/components/ui/badge.tsx +1 -1
- package/template/src/components/ui/button.tsx +7 -7
- package/template/src/components/ui/card.tsx +3 -3
- package/template/src/components/ui/empty.tsx +3 -3
- package/template/src/components/ui/field.tsx +5 -5
- package/template/src/components/ui/input.tsx +1 -1
- package/template/src/components/ui/label.tsx +1 -1
- package/template/src/components/ui/select.tsx +4 -4
- package/template/src/components/ui/table.tsx +3 -3
- package/template/src/index.css +7 -118
- package/template/src/main.tsx +1 -1
- package/template/src/monty.gen.ts +16 -0
- package/template/src/routes/index.tsx +1 -1
- package/template/tsconfig.json +1 -1
- package/template/monty.config.ts +0 -21
package/lib/compile.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// The ONE config-compile pipeline: esbuild-bundle a temp entry that runs the
|
|
2
2
|
// app's OWN compileApp/zod/sdk instances on monty.config.ts, execute it in a
|
|
3
|
-
// subprocess, parse the emitted metadata. Used by `monty dev`/`monty
|
|
3
|
+
// subprocess, parse the emitted metadata. Used by `monty dev`/`monty save`
|
|
4
4
|
// (bin/monty.mjs) and the demo harness (scripts/demo.mjs) — one pipeline, so
|
|
5
5
|
// what a demo installs is byte-for-byte what a deploy would send.
|
|
6
6
|
import { spawnSync } from "node:child_process";
|
|
@@ -19,7 +19,7 @@ export class CompileError extends Error {
|
|
|
19
19
|
// Throws CompileError:
|
|
20
20
|
// - CONFIG_BUNDLE_FAILED — esbuild could not bundle (usually unlinked deps)
|
|
21
21
|
// - CONFIG_COMPILE_FAILED — the config threw while loading
|
|
22
|
-
export async function compileAppConfig(appDir
|
|
22
|
+
export async function compileAppConfig(appDir) {
|
|
23
23
|
appDir = resolve(appDir); // esbuild requires an absolute absWorkingDir
|
|
24
24
|
// esbuild is a dependency of THIS package (@montytools/cli), so resolution
|
|
25
25
|
// from here works for any caller — no per-script resolution dance.
|
|
@@ -31,7 +31,7 @@ export async function compileAppConfig(appDir, { forceManifest = false } = {}) {
|
|
|
31
31
|
writeFileSync(entry, [
|
|
32
32
|
`import { app } from "../monty.config";`,
|
|
33
33
|
`import { compileApp } from "@montytools/sdk/compile";`,
|
|
34
|
-
`process.stdout.write(JSON.stringify(compileApp(app
|
|
34
|
+
`process.stdout.write(JSON.stringify(compileApp(app)));`,
|
|
35
35
|
].join("\n"));
|
|
36
36
|
try {
|
|
37
37
|
await build({
|
package/lib/schemaCodegen.mjs
CHANGED
|
@@ -220,9 +220,13 @@ export function manifestToConfig(manifest, { name, icon } = {}) {
|
|
|
220
220
|
...sdkImports.map((n) => ` ${n},`),
|
|
221
221
|
`} from "@montytools/sdk";`,
|
|
222
222
|
``,
|
|
223
|
-
`//
|
|
224
|
-
`//
|
|
225
|
-
`//
|
|
223
|
+
`// GENERATED by Monty from the app's config stored in the workspace.`,
|
|
224
|
+
`// DO NOT EDIT: edits here never land anywhere, and this file is`,
|
|
225
|
+
`// overwritten on every sync (a running \`monty dev\` regenerates it within`,
|
|
226
|
+
`// a heartbeat of a remote change; \`monty save\` and \`monty schema pull\``,
|
|
227
|
+
`// refresh it too). Change the app through the doors instead:`,
|
|
228
|
+
`// \`monty schema set\`, the MCP schema_update tool, or Configuration.`,
|
|
229
|
+
`// Import { app } from it for typed SDK hooks — that part is yours.`,
|
|
226
230
|
`export const app = defineApp({`,
|
|
227
231
|
` slug: ${JSON.stringify(manifest.slug)},`,
|
|
228
232
|
...(name ? [` name: ${JSON.stringify(name)},`] : []),
|
package/lib/schemaPull.mjs
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
// `monty schema pull` — regenerate monty.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// Distinct from `monty pull`,
|
|
1
|
+
// `monty schema pull` — regenerate src/monty.gen.ts from the app's stored
|
|
2
|
+
// manifest (the schema-as-data flow: the workspace owns the config; this
|
|
3
|
+
// file is its generated, never-hand-edited mirror, giving app code typed
|
|
4
|
+
// SDK hooks and the runtime zod schemas). Distinct from `monty pull`,
|
|
5
|
+
// which restores the whole SOURCE SNAPSHOT.
|
|
5
6
|
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
7
|
+
// No dirty check, no backup: the gen module is not an editor — nothing in
|
|
8
|
+
// it can be "local changes" — so regeneration is a plain overwrite. The
|
|
9
|
+
// same call runs from the dev-session heartbeat (a remote schema edit
|
|
10
|
+
// lands here within ~30s) and from `monty save`/`monty connect`.
|
|
10
11
|
|
|
11
|
-
import { existsSync, mkdirSync, readFileSync,
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
12
13
|
import { join } from "node:path";
|
|
13
|
-
import {
|
|
14
|
+
import { manifestToConfig } from "./schemaCodegen.mjs";
|
|
14
15
|
|
|
15
16
|
const STATE_FILE = ["schema.json"]; // .monty/schema.json
|
|
16
17
|
|
|
@@ -31,7 +32,20 @@ export function writeSchemaState(appDir, hash) {
|
|
|
31
32
|
writeFileSync(join(dir, ...STATE_FILE), JSON.stringify({ hash, syncedAt: Date.now() }) + "\n");
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
/** Write src/monty.gen.ts from a manifest. Only apps with code get the
|
|
36
|
+
* module (a config-only folder has nothing that could import it). Returns
|
|
37
|
+
* whether a file was written. */
|
|
38
|
+
export function writeGenModule(appDir, manifest, { name, icon } = {}) {
|
|
39
|
+
const srcDir = join(appDir, "src");
|
|
40
|
+
if (!existsSync(srcDir)) return false;
|
|
41
|
+
writeFileSync(
|
|
42
|
+
join(srcDir, "monty.gen.ts"),
|
|
43
|
+
manifestToConfig(manifest, { name, icon: icon ?? undefined }),
|
|
44
|
+
);
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function schemaPull({ appDir, host, key, slug, quiet, fail }) {
|
|
35
49
|
const res = await fetch(`${host}/api/schema?slug=${encodeURIComponent(slug)}`, {
|
|
36
50
|
headers: { authorization: `Bearer ${key}` },
|
|
37
51
|
});
|
|
@@ -39,45 +53,17 @@ export async function schemaPull({ appDir, host, key, slug, force, compileAppCon
|
|
|
39
53
|
if (!res.ok || !body?.ok) {
|
|
40
54
|
fail(body?.code ?? `HTTP_${res.status}`, body?.fix ?? "Could not fetch the app's schema — check the connection and `monty login`.");
|
|
41
55
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
56
|
+
// An empty registry config is still a config — generate from it.
|
|
57
|
+
const manifest = body.manifest ?? { slug, tables: {} };
|
|
58
|
+
const wrote = writeGenModule(appDir, manifest, { name: body.name, icon: body.icon });
|
|
59
|
+
if (body.hash) writeSchemaState(appDir, body.hash);
|
|
60
|
+
if (!quiet) {
|
|
61
|
+
console.log(
|
|
62
|
+
wrote
|
|
63
|
+
? `schema: pulled "${slug}" (${Object.keys(manifest.tables ?? {}).length} tables) -> src/monty.gen.ts`
|
|
64
|
+
: `schema: "${slug}" synced (config-only app — no code, no gen module; read it with \`monty schema\`)`,
|
|
46
65
|
);
|
|
66
|
+
if (body.hash) console.log(`base: ${body.hash.slice(0, 12)} (.monty/schema.json — the CAS base for the next push)`);
|
|
47
67
|
}
|
|
48
|
-
|
|
49
|
-
const configPath = join(appDir, "monty.config.ts");
|
|
50
|
-
if (existsSync(configPath) && !force) {
|
|
51
|
-
// Dirty check: does the local config compile to the manifest this
|
|
52
|
-
// checkout last synced? If not, pulling would clobber local edits.
|
|
53
|
-
const state = readSchemaState(appDir);
|
|
54
|
-
let localHash = null;
|
|
55
|
-
try {
|
|
56
|
-
const compiled = await compileAppConfig(appDir);
|
|
57
|
-
localHash = compiled.manifest ? manifestHash(compiled.manifest) : null;
|
|
58
|
-
} catch {
|
|
59
|
-
// A config that doesn't compile can't be proven clean — refuse without
|
|
60
|
-
// --force rather than silently discarding whatever it holds.
|
|
61
|
-
fail(
|
|
62
|
-
"SCHEMA_DIRTY",
|
|
63
|
-
"monty.config.ts does not compile, so local schema edits cannot be verified against the registry. Fix it and deploy, or re-run with --force to REPLACE it (a .bak is kept).",
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
const cleanAgainst = state?.hash ?? body.hash;
|
|
67
|
-
if (localHash !== null && localHash !== cleanAgainst && localHash !== body.hash) {
|
|
68
|
-
fail(
|
|
69
|
-
"SCHEMA_DIRTY",
|
|
70
|
-
"monty.config.ts has schema changes that never reached the registry. Push them first (`monty dev` save or `monty deploy`), or discard them with --force (a .bak is kept).",
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (existsSync(configPath)) {
|
|
76
|
-
renameSync(configPath, `${configPath}.bak`);
|
|
77
|
-
console.log(`backup: monty.config.ts.bak`);
|
|
78
|
-
}
|
|
79
|
-
writeFileSync(configPath, manifestToConfig(body.manifest, { name: body.name, icon: body.icon ?? undefined }));
|
|
80
|
-
writeSchemaState(appDir, body.hash);
|
|
81
|
-
console.log(`schema: pulled "${slug}" (${Object.keys(body.manifest.tables).length} tables) -> monty.config.ts`);
|
|
82
|
-
console.log(`base: ${body.hash.slice(0, 12)} (.monty/schema.json — the CAS base for the next push)`);
|
|
68
|
+
return { wrote, hash: body.hash ?? null, manifest };
|
|
83
69
|
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// The Monty style lint — the platform's stylelint-polaris equivalent for
|
|
2
|
+
// Tailwind class strings. Rules are disallow-lists grouped by category with
|
|
3
|
+
// an instruction message per finding (Polaris's coverage-plugin shape); the
|
|
4
|
+
// allowed vocabularies are IMPORTED from @monty/tokens/manifest.mjs so the
|
|
5
|
+
// lint can never drift from the tokens. It runs blocking inside `monty save`
|
|
6
|
+
// (beside the typecheck), advisory at `monty dev`, and standalone as
|
|
7
|
+
// `monty style`.
|
|
8
|
+
//
|
|
9
|
+
// Scope: agent-authored app code only — src/**/*.{ts,tsx,jsx} minus the
|
|
10
|
+
// vendored kit (src/components/ui/**), generated files, and wiring. A line
|
|
11
|
+
// carrying `monty-style-ignore` is skipped (the stylelint-disable of this
|
|
12
|
+
// system; use it for the rare sanctioned exception, never to silence a page).
|
|
13
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
14
|
+
import { dirname, join, relative } from "node:path";
|
|
15
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
16
|
+
|
|
17
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
|
|
19
|
+
let manifestPromise;
|
|
20
|
+
function loadManifest() {
|
|
21
|
+
// Published tarball: prepack copies the manifest beside this file.
|
|
22
|
+
// Repo checkout: import straight from packages/tokens — the dependency
|
|
23
|
+
// direction that keeps rules derived from tokens.
|
|
24
|
+
manifestPromise ??= (async () => {
|
|
25
|
+
for (const p of [
|
|
26
|
+
join(HERE, "tokens-manifest.mjs"),
|
|
27
|
+
join(HERE, "../../tokens/manifest.mjs"),
|
|
28
|
+
]) {
|
|
29
|
+
if (existsSync(p)) return import(pathToFileURL(p).href);
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
})();
|
|
33
|
+
return manifestPromise;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const SIDE = "(?:-(?:t|b|l|r|tl|tr|bl|br|s|e|ss|se|es|ee))?";
|
|
37
|
+
const COLOR_PROPS = "(?:bg|text|border|ring|outline|fill|stroke|from|via|to|divide|decoration|caret|accent)";
|
|
38
|
+
|
|
39
|
+
function buildRules(m) {
|
|
40
|
+
const hues = m.PALETTE_HUES.join("|");
|
|
41
|
+
return [
|
|
42
|
+
{
|
|
43
|
+
category: "type",
|
|
44
|
+
// Arbitrary sizes only — arbitrary COLOR values on text- belong to color.
|
|
45
|
+
re: /\btext-\[(?!#|rgb|hsl|oklch|var\(|color-mix)[^\]]+\]/g,
|
|
46
|
+
fix: () => "use the ladder (text-body is the default)",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
category: "type",
|
|
50
|
+
re: /\btext-(?:xs|sm|base|lg|xl|[2-9]xl)\b/g,
|
|
51
|
+
fix: (got) => `use ${m.TEXT_SUGGESTIONS[got] ?? "the ladder"}`,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
category: "weight",
|
|
55
|
+
re: /\bfont-(?:thin|extralight|light|bold|extrabold|black)\b/g,
|
|
56
|
+
fix: () => "use font-medium (or the weight the ladder style carries)",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
category: "color",
|
|
60
|
+
re: new RegExp(`\\b${COLOR_PROPS}-\\[(?:#|rgb|hsl|oklch|var\\(|color-mix)[^\\]]*\\]`, "g"),
|
|
61
|
+
fix: () => "use a semantic token class",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
category: "color",
|
|
65
|
+
re: new RegExp(`\\b${COLOR_PROPS}-(?:${hues})-\\d{2,3}(?:/\\d{1,3})?\\b`, "g"),
|
|
66
|
+
fix: (got) => {
|
|
67
|
+
const hue = got.match(new RegExp(`-(${hues})-`))?.[1];
|
|
68
|
+
return `use ${m.HUE_SUGGESTIONS[hue] ?? "a semantic token"}`;
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
category: "color",
|
|
73
|
+
re: /["'`]#[0-9a-fA-F]{6}(?:[0-9a-fA-F]{2})?["'`]/g,
|
|
74
|
+
fix: () => "use var(--chart-1…5) or a semantic class",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
category: "radius",
|
|
78
|
+
re: new RegExp(`\\brounded${SIDE}-\\[[^\\]]+\\]`, "g"),
|
|
79
|
+
fix: () => "corners are sharp — delete it",
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
category: "radius",
|
|
83
|
+
re: new RegExp(`\\brounded${SIDE}-(?:xs|sm|md|lg|xl|[234]xl)\\b`, "g"),
|
|
84
|
+
fix: () => "corners are sharp — delete it (rounded-full only for dots/avatars)",
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
category: "elevation",
|
|
88
|
+
re: /\bshadow-\[[^\]]+\]/g,
|
|
89
|
+
fix: () => `use ${m.SHADOWS_ALLOWED.slice(0, 3).join("/")}`,
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
category: "elevation",
|
|
93
|
+
re: /\bshadow-(?:2xs|xs|sm|md|lg|xl|2xl|inner)\b/g,
|
|
94
|
+
fix: () => `use ${m.SHADOWS_ALLOWED.slice(0, 3).join("/")}`,
|
|
95
|
+
},
|
|
96
|
+
];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const SKIP_FILES = new Set(["main.tsx", "vite-env.d.ts", "monty.gen.ts", "routeTree.gen.ts"]);
|
|
100
|
+
|
|
101
|
+
function* sourceFiles(dir, base = dir) {
|
|
102
|
+
let entries;
|
|
103
|
+
try {
|
|
104
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
105
|
+
} catch {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
for (const entry of entries) {
|
|
109
|
+
const p = join(dir, entry.name);
|
|
110
|
+
const rel = relative(base, p);
|
|
111
|
+
if (entry.isDirectory()) {
|
|
112
|
+
// The vendored kit is template/registry-owned — its internals may
|
|
113
|
+
// legitimately carry classes agents must not write.
|
|
114
|
+
if (rel === join("components", "ui") || entry.name === "node_modules") continue;
|
|
115
|
+
yield* sourceFiles(p, base);
|
|
116
|
+
} else if (/\.(?:tsx|jsx|ts)$/.test(entry.name)) {
|
|
117
|
+
if (SKIP_FILES.has(entry.name) || entry.name.endsWith(".gen.ts")) continue;
|
|
118
|
+
yield p;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Scan an app folder. Returns [{ file, line, category, got, fix }] sorted
|
|
124
|
+
* by file then line; [] when clean (or when src/ doesn't exist —
|
|
125
|
+
* config-only apps have nothing to lint). */
|
|
126
|
+
export async function lintStyles(appDir) {
|
|
127
|
+
const manifest = await loadManifest();
|
|
128
|
+
if (!manifest) return []; // never block a save on a broken lint install
|
|
129
|
+
const rules = buildRules(manifest);
|
|
130
|
+
const src = join(appDir, "src");
|
|
131
|
+
const violations = [];
|
|
132
|
+
for (const file of sourceFiles(src)) {
|
|
133
|
+
const rel = join("src", relative(src, file));
|
|
134
|
+
const lines = readFileSync(file, "utf8").split("\n");
|
|
135
|
+
lines.forEach((text, i) => {
|
|
136
|
+
if (text.includes("monty-style-ignore")) return;
|
|
137
|
+
for (const rule of rules) {
|
|
138
|
+
for (const match of text.matchAll(rule.re)) {
|
|
139
|
+
violations.push({
|
|
140
|
+
file: rel,
|
|
141
|
+
line: i + 1,
|
|
142
|
+
category: rule.category,
|
|
143
|
+
got: match[0],
|
|
144
|
+
fix: rule.fix(match[0]),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
return violations.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const MAX_LINES = 40;
|
|
154
|
+
|
|
155
|
+
/** Findings grouped by category, each under its token-vocabulary lede. */
|
|
156
|
+
export async function formatViolations(violations) {
|
|
157
|
+
const manifest = await loadManifest();
|
|
158
|
+
const byCategory = new Map();
|
|
159
|
+
for (const v of violations) {
|
|
160
|
+
if (!byCategory.has(v.category)) byCategory.set(v.category, []);
|
|
161
|
+
byCategory.get(v.category).push(v);
|
|
162
|
+
}
|
|
163
|
+
const out = [];
|
|
164
|
+
let shown = 0;
|
|
165
|
+
for (const [category, list] of byCategory) {
|
|
166
|
+
out.push(`${category} — ${manifest?.CATEGORY_MESSAGES?.[category] ?? ""}`);
|
|
167
|
+
for (const v of list) {
|
|
168
|
+
if (shown >= MAX_LINES) break;
|
|
169
|
+
out.push(` ${v.file}:${v.line} ${v.got} → ${v.fix}`);
|
|
170
|
+
shown++;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (violations.length > shown) out.push(` … and ${violations.length - shown} more`);
|
|
174
|
+
return out.join("\n");
|
|
175
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// The machine-readable half of the Monty design tokens (tokens.css is the
|
|
2
|
+
// stylesheet). The style lint in the CLI derives its allowed lists and
|
|
3
|
+
// suggestions from HERE — Polaris's pattern (stylelint-polaris imports
|
|
4
|
+
// polaris-tokens) — so the rules can never drift from the tokens.
|
|
5
|
+
// Keep this file in lockstep with tokens.css.
|
|
6
|
+
|
|
7
|
+
/** The six text styles a Monty page uses. */
|
|
8
|
+
export const TEXT_LADDER = [
|
|
9
|
+
"text-tick",
|
|
10
|
+
"text-meta",
|
|
11
|
+
"text-body",
|
|
12
|
+
"text-title",
|
|
13
|
+
"text-heading",
|
|
14
|
+
"text-stat",
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
/** What each off-ladder Tailwind size maps to. */
|
|
18
|
+
export const TEXT_SUGGESTIONS = {
|
|
19
|
+
"text-xs": "text-meta",
|
|
20
|
+
"text-sm": "text-body (content) or text-title (titles)",
|
|
21
|
+
"text-base": "text-body",
|
|
22
|
+
"text-lg": "text-heading",
|
|
23
|
+
"text-xl": "text-heading",
|
|
24
|
+
"text-2xl": "text-stat",
|
|
25
|
+
"text-3xl": "text-stat",
|
|
26
|
+
"text-4xl": "text-stat",
|
|
27
|
+
"text-5xl": "text-stat",
|
|
28
|
+
"text-6xl": "text-stat",
|
|
29
|
+
"text-7xl": "text-stat",
|
|
30
|
+
"text-8xl": "text-stat",
|
|
31
|
+
"text-9xl": "text-stat",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** Allowed font weights (400 default / 500 titled / 600 via heading+stat). */
|
|
35
|
+
export const WEIGHTS_ALLOWED = ["font-normal", "font-medium", "font-semibold"];
|
|
36
|
+
|
|
37
|
+
/** The radius policy: sharp everywhere; these are the only radius classes. */
|
|
38
|
+
export const RADIUS_ALLOWED = [
|
|
39
|
+
"rounded-none",
|
|
40
|
+
"rounded-full",
|
|
41
|
+
"rounded-control",
|
|
42
|
+
"rounded-control-sm",
|
|
43
|
+
"rounded-overlay",
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
/** The only three shadows (plus shadow-none). */
|
|
47
|
+
export const SHADOWS_ALLOWED = ["shadow-control", "shadow-btn-primary", "shadow-menu", "shadow-none"];
|
|
48
|
+
|
|
49
|
+
/** Tailwind default palette hues — banned; the palette is the platform's. */
|
|
50
|
+
export const PALETTE_HUES = [
|
|
51
|
+
"slate", "gray", "zinc", "neutral", "stone",
|
|
52
|
+
"red", "orange", "amber", "yellow", "lime",
|
|
53
|
+
"green", "emerald", "teal", "cyan", "sky",
|
|
54
|
+
"blue", "indigo", "violet", "purple", "fuchsia", "pink", "rose",
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
/** Where a banned hue should point instead. */
|
|
58
|
+
export const HUE_SUGGESTIONS = {
|
|
59
|
+
red: "text-destructive / bg-destructive",
|
|
60
|
+
orange: "text-warning",
|
|
61
|
+
amber: "text-warning",
|
|
62
|
+
yellow: "text-warning",
|
|
63
|
+
lime: "text-success",
|
|
64
|
+
green: "text-success",
|
|
65
|
+
emerald: "text-success",
|
|
66
|
+
teal: "text-success",
|
|
67
|
+
blue: "bg-primary / text-link",
|
|
68
|
+
sky: "text-link",
|
|
69
|
+
indigo: "bg-primary",
|
|
70
|
+
cyan: "text-link",
|
|
71
|
+
violet: "var(--chart-3)",
|
|
72
|
+
purple: "var(--chart-3)",
|
|
73
|
+
fuchsia: "var(--chart-3)",
|
|
74
|
+
pink: "var(--chart-3)",
|
|
75
|
+
rose: "text-destructive",
|
|
76
|
+
slate: "text-muted-foreground / bg-accent",
|
|
77
|
+
gray: "text-muted-foreground / bg-accent",
|
|
78
|
+
zinc: "text-muted-foreground / bg-accent",
|
|
79
|
+
neutral: "text-muted-foreground / bg-accent",
|
|
80
|
+
stone: "text-muted-foreground / bg-accent",
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/** Category ledes — the default instruction on each violation class. */
|
|
84
|
+
export const CATEGORY_MESSAGES = {
|
|
85
|
+
type: "The type ladder is text-tick/meta/body/title/heading/stat; most text needs no size class at all (text-body is on <body>).",
|
|
86
|
+
weight: "Weights are 400/500/600 — font-medium for titled text; 600 arrives via text-heading/text-stat.",
|
|
87
|
+
color: "Colors come from the semantic tokens (bg-background, text-muted-foreground, var(--chart-2), …) — the palette is the platform's.",
|
|
88
|
+
radius: "Corners are sharp by policy; rounded-full (dots, avatars) is the one exception.",
|
|
89
|
+
elevation: "Elevation is shadow-control (rest controls), shadow-btn-primary (the one primary action), or shadow-menu (overlays).",
|
|
90
|
+
};
|
package/lib/views.mjs
CHANGED
|
@@ -107,8 +107,25 @@ export function parseHiddenColumns(input) {
|
|
|
107
107
|
return [...new Set(input.split(",").map((field) => field.trim()).filter(Boolean))];
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
/** `--kanban <field>` makes the view a kanban laned by that select field's
|
|
111
|
+
* values; `--kanban none` makes it a table. */
|
|
112
|
+
export function parseKanbanFlag(input) {
|
|
113
|
+
if (input === undefined) return undefined;
|
|
114
|
+
if (input === "none") return null;
|
|
115
|
+
const groupBy = input.trim();
|
|
116
|
+
if (groupBy === "") {
|
|
117
|
+
badViewConfig('--kanban needs a select field name, or "none" to make the view a table.');
|
|
118
|
+
}
|
|
119
|
+
return { type: "kanban", groupBy };
|
|
120
|
+
}
|
|
121
|
+
|
|
110
122
|
export function mergeViewConfig(existing, patch) {
|
|
111
123
|
const base = isObject(existing) ? existing : {};
|
|
124
|
+
const kanban = Object.hasOwn(patch, "kanban")
|
|
125
|
+
? patch.kanban
|
|
126
|
+
: base.type === "kanban" && typeof base.groupBy === "string"
|
|
127
|
+
? { type: "kanban", groupBy: base.groupBy }
|
|
128
|
+
: null;
|
|
112
129
|
return {
|
|
113
130
|
filters: Object.hasOwn(patch, "filters")
|
|
114
131
|
? patch.filters
|
|
@@ -117,6 +134,7 @@ export function mergeViewConfig(existing, patch) {
|
|
|
117
134
|
hidden: Object.hasOwn(patch, "hidden")
|
|
118
135
|
? patch.hidden
|
|
119
136
|
: (Array.isArray(base.hidden) ? base.hidden : []),
|
|
137
|
+
...(kanban ? { type: "kanban", groupBy: kanban.groupBy } : {}),
|
|
120
138
|
};
|
|
121
139
|
}
|
|
122
140
|
|
|
@@ -126,6 +144,7 @@ export function validateViewColumns(config, fieldNames) {
|
|
|
126
144
|
...Object.keys(config.filters),
|
|
127
145
|
...(config.sort ? [config.sort.column] : []),
|
|
128
146
|
...config.hidden,
|
|
147
|
+
...(config.type === "kanban" && config.groupBy ? [config.groupBy] : []),
|
|
129
148
|
];
|
|
130
149
|
const unknown = [...new Set(used.filter((field) => !known.has(field)))];
|
|
131
150
|
if (unknown.length > 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@montytools/cli",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.6",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/TomasMonty/monty-v2.git",
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
},
|
|
22
22
|
"scripts": {
|
|
23
23
|
"prepack": "node scripts/bundle-template.mjs",
|
|
24
|
-
"typecheck": "node --check bin/monty.mjs && node --check lib/compile.mjs && node --check lib/schemaCodegen.mjs && node --check lib/schemaPull.mjs && node --check lib/views.mjs && node --check scripts/schema-roundtrip.mjs",
|
|
25
|
-
"test": "node --test test
|
|
24
|
+
"typecheck": "node --check bin/monty.mjs && node --check lib/compile.mjs && node --check lib/schemaCodegen.mjs && node --check lib/schemaPull.mjs && node --check lib/views.mjs && node --check scripts/schema-roundtrip.mjs && node --check lib/styleLint.mjs",
|
|
25
|
+
"test": "node --test \"test/*.test.mjs\"",
|
|
26
26
|
"postinstall": "node bin/postinstall.mjs",
|
|
27
27
|
"test:roundtrip": "node scripts/schema-roundtrip.mjs"
|
|
28
28
|
},
|