elcrm 1.1.27 → 1.1.34
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/index.js +256 -7
- package/dist/vite/discover-lib.d.ts +6 -0
- package/dist/vite/discover-lib.js +59 -11
- package/dist/vite/minify-css-vars.js +8 -8
- package/dist/vite/plugin-css-scoped.d.ts +18 -4
- package/dist/vite/plugin-css-scoped.js +80 -41
- package/package.json +2 -2
- package/templates/elcrm-docs/CLI.elCRM.md +4 -3
- package/templates/elcrm-docs/COMPONENTS.elCRM.md +63 -21
- package/templates/elcrm-docs/FORM.elCRM.md +1 -1
- package/templates/orbit/web/src/style/theme-dark.css +11 -0
- package/templates/orbit/web/src/style/theme-light.css +11 -0
- package/templates/orbit/web/src/style/theme.css +19 -0
- package/templates/panel/web/src/style/theme-dark.css +11 -0
- package/templates/panel/web/src/style/theme-light.css +11 -0
- package/templates/panel/web/src/style/theme.css +19 -0
package/dist/index.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
// @bun
|
|
3
3
|
|
|
4
4
|
// src/index.ts
|
|
5
|
-
import { readFileSync as
|
|
6
|
-
import { dirname as dirname4, join as
|
|
5
|
+
import { readFileSync as readFileSync29 } from "fs";
|
|
6
|
+
import { dirname as dirname4, join as join26 } from "path";
|
|
7
7
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
8
8
|
|
|
9
9
|
// src/parseArgs.ts
|
|
@@ -37,7 +37,10 @@ function parseArgs(argv = process.argv.slice(2)) {
|
|
|
37
37
|
"folder",
|
|
38
38
|
"out",
|
|
39
39
|
"api",
|
|
40
|
-
"web"
|
|
40
|
+
"web",
|
|
41
|
+
"access",
|
|
42
|
+
"tag",
|
|
43
|
+
"otp"
|
|
41
44
|
]);
|
|
42
45
|
if (valueOpts.has(body)) {
|
|
43
46
|
options[body] = next;
|
|
@@ -913,6 +916,70 @@ function rewriteImports(source) {
|
|
|
913
916
|
hits++;
|
|
914
917
|
return `import { Stack } from "@elcrm/components/Stack"`;
|
|
915
918
|
});
|
|
919
|
+
next = next.replace(/\bPricingGroupItem\b/g, () => {
|
|
920
|
+
hits++;
|
|
921
|
+
return "PricingPlan";
|
|
922
|
+
});
|
|
923
|
+
next = next.replace(/import\s*\{([^}]+)\}\s*from\s*["']@elcrm\/components(?:\/Toolbar)?["']/g, (full, list) => {
|
|
924
|
+
if (!/\bapplyToolbarSize\b/.test(list))
|
|
925
|
+
return full;
|
|
926
|
+
hits++;
|
|
927
|
+
const parts = list.split(",").map((p) => p.trim()).filter((p) => p && !/\bapplyToolbarSize\b/.test(p));
|
|
928
|
+
if (parts.length === 0)
|
|
929
|
+
return "";
|
|
930
|
+
const from = /from\s*(["'][^"']+["'])/.exec(full)?.[1] ?? `"@elcrm/components"`;
|
|
931
|
+
return `import { ${parts.join(", ")} } from ${from}`;
|
|
932
|
+
});
|
|
933
|
+
next = next.replace(/\bapplyToolbarSize\s*\(\s*([^,]+)\s*,\s*[^)]+\)/g, (_m, node) => {
|
|
934
|
+
hits++;
|
|
935
|
+
return node.trim();
|
|
936
|
+
});
|
|
937
|
+
let needChatFrame = false;
|
|
938
|
+
let needChatFrameProps = false;
|
|
939
|
+
next = next.replace(/import\s*(type\s+)?\{([^}]+)\}\s*from\s*["']@elcrm\/components["']/g, (full, typeKw, list) => {
|
|
940
|
+
if (!/\bChatFrame(?:Props)?\b/.test(list))
|
|
941
|
+
return full;
|
|
942
|
+
hits++;
|
|
943
|
+
const parts = list.split(",").map((p) => p.trim()).filter(Boolean);
|
|
944
|
+
const kept = [];
|
|
945
|
+
for (const p of parts) {
|
|
946
|
+
const isType = Boolean(typeKw) || /^type\s+/.test(p);
|
|
947
|
+
const bare = p.replace(/^type\s+/, "").replace(/\s+as\s+\w+$/, "").trim();
|
|
948
|
+
if (bare === "ChatFrame") {
|
|
949
|
+
needChatFrame = true;
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
if (bare === "ChatFrameProps") {
|
|
953
|
+
needChatFrameProps = true;
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
956
|
+
kept.push(p);
|
|
957
|
+
}
|
|
958
|
+
if (kept.length === 0)
|
|
959
|
+
return "/* elcrm: ChatFrame \u2192 deep */";
|
|
960
|
+
const head = typeKw ? `import type { ` : `import { `;
|
|
961
|
+
return `${head}${kept.join(", ")} } from "@elcrm/components"`;
|
|
962
|
+
});
|
|
963
|
+
if (needChatFrame || needChatFrameProps) {
|
|
964
|
+
const inject = [];
|
|
965
|
+
if (needChatFrame) {
|
|
966
|
+
inject.push(`import { ChatFrame } from "@elcrm/components/ChatFrame";`);
|
|
967
|
+
}
|
|
968
|
+
if (needChatFrameProps) {
|
|
969
|
+
inject.push(`import type { ChatFrameProps } from "@elcrm/components/ChatFrame";`);
|
|
970
|
+
}
|
|
971
|
+
const block = inject.join(`
|
|
972
|
+
`);
|
|
973
|
+
if (/from\s*["']@elcrm\/components/.test(next)) {
|
|
974
|
+
next = next.replace(/(from\s*["']@elcrm\/components(?:\/[^"']*)?["'];?)/, `$1
|
|
975
|
+
${block}`);
|
|
976
|
+
} else {
|
|
977
|
+
next = `${block}
|
|
978
|
+
${next}`;
|
|
979
|
+
}
|
|
980
|
+
next = next.replace(/\n?\/\* elcrm: ChatFrame \u2192 deep \*\/\n?/g, `
|
|
981
|
+
`);
|
|
982
|
+
}
|
|
916
983
|
return { next, hits };
|
|
917
984
|
}
|
|
918
985
|
function rewriteJsx(source) {
|
|
@@ -967,6 +1034,34 @@ function rewriteSectionOnChange(source) {
|
|
|
967
1034
|
});
|
|
968
1035
|
return { next, hits };
|
|
969
1036
|
}
|
|
1037
|
+
function rewriteGroupCallbacks(source) {
|
|
1038
|
+
if (!/@elcrm\/components/.test(source) && !/\b(Menu|Sidebar|Dropdown|Breadcrumb|AvatarGroup|ActionGroup)\b/.test(source)) {
|
|
1039
|
+
return { next: source, hits: 0 };
|
|
1040
|
+
}
|
|
1041
|
+
let hits = 0;
|
|
1042
|
+
let next = source;
|
|
1043
|
+
next = next.replace(/\bonFooterSelect=/g, () => {
|
|
1044
|
+
hits++;
|
|
1045
|
+
return "onFooterValueChange=";
|
|
1046
|
+
});
|
|
1047
|
+
next = next.replace(/\bonItemClick=/g, () => {
|
|
1048
|
+
hits++;
|
|
1049
|
+
return "onValueChange=";
|
|
1050
|
+
});
|
|
1051
|
+
next = next.replace(/\bonSelect=/g, () => {
|
|
1052
|
+
hits++;
|
|
1053
|
+
return "onValueChange=";
|
|
1054
|
+
});
|
|
1055
|
+
next = next.replace(/(<ActionGroup\b[^>]*?)\sitems=/g, (_m, pre) => {
|
|
1056
|
+
hits++;
|
|
1057
|
+
return `${pre} actions=`;
|
|
1058
|
+
});
|
|
1059
|
+
next = next.replace(/(<Sidebar\b[^>]*?)\sitems=/g, (_m, pre) => {
|
|
1060
|
+
hits++;
|
|
1061
|
+
return `${pre} groups=`;
|
|
1062
|
+
});
|
|
1063
|
+
return { next, hits };
|
|
1064
|
+
}
|
|
970
1065
|
function rewriteItemKeys(source) {
|
|
971
1066
|
if (!/\b(Menu|NavSections|TabSections|MenuItem|NavSectionsItem|TabSectionsItem)\b/.test(source)) {
|
|
972
1067
|
return { next: source, hits: 0 };
|
|
@@ -1001,6 +1096,9 @@ function rewriteComponentsTsx(source) {
|
|
|
1001
1096
|
const oc = rewriteSectionOnChange(next);
|
|
1002
1097
|
next = oc.next;
|
|
1003
1098
|
hits += oc.hits;
|
|
1099
|
+
const cb = rewriteGroupCallbacks(next);
|
|
1100
|
+
next = cb.next;
|
|
1101
|
+
hits += cb.hits;
|
|
1004
1102
|
const keys = rewriteItemKeys(next);
|
|
1005
1103
|
next = keys.next;
|
|
1006
1104
|
hits += keys.hits;
|
|
@@ -1430,6 +1528,7 @@ function syncElcrmCss(options) {
|
|
|
1430
1528
|
skippedPkgs.push(name);
|
|
1431
1529
|
allDefs.push(...defs);
|
|
1432
1530
|
}
|
|
1531
|
+
const pkgsWithTokens = new Set(cssExports.filter((e) => e.tokens).map((e) => e.name));
|
|
1433
1532
|
const added = {
|
|
1434
1533
|
"theme.css": [],
|
|
1435
1534
|
"theme-light.css": [],
|
|
@@ -1445,7 +1544,7 @@ function syncElcrmCss(options) {
|
|
|
1445
1544
|
continue;
|
|
1446
1545
|
let css = readFileSync7(file, "utf8");
|
|
1447
1546
|
const have = existingTokenNames(css);
|
|
1448
|
-
const missing = allDefs.filter((d) => d.bucket === bucket && !have.has(d.name) && !d.name.startsWith("--search-") && !TOKEN_DEPRECATED.has(d.name) && !isButtonShadowNoise(d.name, d.value));
|
|
1547
|
+
const missing = allDefs.filter((d) => d.bucket === bucket && !have.has(d.name) && !d.name.startsWith("--search-") && !TOKEN_DEPRECATED.has(d.name) && !isButtonShadowNoise(d.name, d.value) && !(bucket === "geometry" && pkgsWithTokens.has(d.pkg)));
|
|
1449
1548
|
const uniq = [];
|
|
1450
1549
|
const seen = new Set;
|
|
1451
1550
|
for (const d of missing) {
|
|
@@ -5071,15 +5170,158 @@ async function cmdKit(parsed) {
|
|
|
5071
5170
|
process.exit(1);
|
|
5072
5171
|
}
|
|
5073
5172
|
|
|
5173
|
+
// src/commands/publish.ts
|
|
5174
|
+
import { copyFileSync, existsSync as existsSync20, readFileSync as readFileSync28, unlinkSync, writeFileSync as writeFileSync12 } from "fs";
|
|
5175
|
+
import { join as join25, resolve as resolve13 } from "path";
|
|
5176
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
5177
|
+
var PUBLIC_NAME = "package.public.json";
|
|
5178
|
+
var PKG_NAME = "package.json";
|
|
5179
|
+
var BACKUP_NAME = ".package.json.elcrm-publish.bak";
|
|
5180
|
+
async function cmdPublish(parsed) {
|
|
5181
|
+
const cwd = resolve13(process.cwd(), parsed.options.dir || ".");
|
|
5182
|
+
const dry = hasFlag(parsed, "dry");
|
|
5183
|
+
const noCheck = hasFlag(parsed, "no-check", "noCheck");
|
|
5184
|
+
const pkgPath = join25(cwd, PKG_NAME);
|
|
5185
|
+
const publicPath = join25(cwd, PUBLIC_NAME);
|
|
5186
|
+
const backupPath = join25(cwd, BACKUP_NAME);
|
|
5187
|
+
if (!existsSync20(pkgPath)) {
|
|
5188
|
+
throw new Error(`\u041D\u0435\u0442 ${PKG_NAME} \u0432 ${cwd}`);
|
|
5189
|
+
}
|
|
5190
|
+
if (!existsSync20(publicPath)) {
|
|
5191
|
+
throw new Error(`\u041D\u0435\u0442 ${PUBLIC_NAME} \u0432 ${cwd}
|
|
5192
|
+
` + `\u0421\u043E\u0437\u0434\u0430\u0439\u0442\u0435 \u043C\u0430\u043D\u0438\u0444\u0435\u0441\u0442 \u0431\u0435\u0437 scripts/devDependencies \u0434\u043B\u044F npm.`);
|
|
5193
|
+
}
|
|
5194
|
+
const local = readPackageJson(cwd);
|
|
5195
|
+
if (!local?.name || !local.version) {
|
|
5196
|
+
throw new Error(`${PKG_NAME}: \u043D\u0443\u0436\u043D\u044B \u043F\u043E\u043B\u044F name \u0438 version`);
|
|
5197
|
+
}
|
|
5198
|
+
let pub;
|
|
5199
|
+
try {
|
|
5200
|
+
pub = JSON.parse(readFileSync28(publicPath, "utf8"));
|
|
5201
|
+
} catch {
|
|
5202
|
+
throw new Error(`${PUBLIC_NAME}: \u043D\u0435\u0432\u0430\u043B\u0438\u0434\u043D\u044B\u0439 JSON`);
|
|
5203
|
+
}
|
|
5204
|
+
pub.version = local.version;
|
|
5205
|
+
if (typeof pub.name !== "string")
|
|
5206
|
+
pub.name = local.name;
|
|
5207
|
+
const access = parsed.options.access || (typeof pub.publishConfig?.access === "string" ? pub.publishConfig.access : "public");
|
|
5208
|
+
console.log(`[publish] ${local.name}@${local.version}`);
|
|
5209
|
+
console.log(`[publish] \u043C\u0430\u043D\u0438\u0444\u0435\u0441\u0442: ${PUBLIC_NAME} \u2192 ${PKG_NAME}`);
|
|
5210
|
+
if (!noCheck && !dry) {
|
|
5211
|
+
await runPreChecks(cwd, local.scripts);
|
|
5212
|
+
} else if (noCheck) {
|
|
5213
|
+
console.log("[publish] \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u043F\u0440\u043E\u043F\u0443\u0449\u0435\u043D\u044B (--no-check)");
|
|
5214
|
+
}
|
|
5215
|
+
const npmArgs = buildNpmArgs(parsed, access);
|
|
5216
|
+
console.log(`[publish] npm ${npmArgs.join(" ")}`);
|
|
5217
|
+
if (dry) {
|
|
5218
|
+
console.log("[publish] --dry: \u043F\u043E\u0434\u043C\u0435\u043D\u0430 \u0438 publish \u043D\u0435 \u0432\u044B\u043F\u043E\u043B\u043D\u044F\u043B\u0438\u0441\u044C");
|
|
5219
|
+
return;
|
|
5220
|
+
}
|
|
5221
|
+
const originalBytes = readFileSync28(pkgPath);
|
|
5222
|
+
copyFileSync(pkgPath, backupPath);
|
|
5223
|
+
const indent = detectIndent2(readFileSync28(publicPath, "utf8"));
|
|
5224
|
+
writeFileSync12(pkgPath, JSON.stringify(pub, null, indent) + `
|
|
5225
|
+
`, "utf8");
|
|
5226
|
+
let publishStatus = 1;
|
|
5227
|
+
try {
|
|
5228
|
+
const r = spawnSync3("npm", npmArgs, {
|
|
5229
|
+
cwd,
|
|
5230
|
+
stdio: "inherit",
|
|
5231
|
+
env: process.env
|
|
5232
|
+
});
|
|
5233
|
+
publishStatus = r.status ?? 1;
|
|
5234
|
+
if (r.error) {
|
|
5235
|
+
console.error(r.error.message);
|
|
5236
|
+
}
|
|
5237
|
+
} finally {
|
|
5238
|
+
writeFileSync12(pkgPath, originalBytes);
|
|
5239
|
+
if (existsSync20(backupPath)) {
|
|
5240
|
+
try {
|
|
5241
|
+
unlinkSync(backupPath);
|
|
5242
|
+
} catch {}
|
|
5243
|
+
}
|
|
5244
|
+
console.log(`[publish] \u0432\u043E\u0441\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D ${PKG_NAME}`);
|
|
5245
|
+
}
|
|
5246
|
+
if (publishStatus !== 0) {
|
|
5247
|
+
process.exit(publishStatus);
|
|
5248
|
+
}
|
|
5249
|
+
console.log(`[publish] \u0433\u043E\u0442\u043E\u0432\u043E: ${local.name}@${local.version}`);
|
|
5250
|
+
}
|
|
5251
|
+
async function runPreChecks(cwd, scripts) {
|
|
5252
|
+
const list = scripts ?? {};
|
|
5253
|
+
const chain = [];
|
|
5254
|
+
if (list.prepublishOnly)
|
|
5255
|
+
chain.push("prepublishOnly");
|
|
5256
|
+
else {
|
|
5257
|
+
if (list["check:css"])
|
|
5258
|
+
chain.push("check:css");
|
|
5259
|
+
if (list.test)
|
|
5260
|
+
chain.push("test");
|
|
5261
|
+
if (list.build)
|
|
5262
|
+
chain.push("build");
|
|
5263
|
+
}
|
|
5264
|
+
if (chain.length === 0) {
|
|
5265
|
+
console.log("[publish] scripts \u0434\u043B\u044F \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u043D\u0435\u0442 \u2014 \u0441\u0440\u0430\u0437\u0443 publish");
|
|
5266
|
+
return;
|
|
5267
|
+
}
|
|
5268
|
+
for (const script of chain) {
|
|
5269
|
+
console.log(`[publish] bun run ${script}`);
|
|
5270
|
+
const r = spawnSync3("bun", ["run", script], {
|
|
5271
|
+
cwd,
|
|
5272
|
+
stdio: "inherit",
|
|
5273
|
+
env: process.env
|
|
5274
|
+
});
|
|
5275
|
+
if ((r.status ?? 1) !== 0) {
|
|
5276
|
+
throw new Error(`\u0421\u043A\u0440\u0438\u043F\u0442 "${script}" \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0441\u044F \u0441 \u043A\u043E\u0434\u043E\u043C ${r.status}`);
|
|
5277
|
+
}
|
|
5278
|
+
}
|
|
5279
|
+
}
|
|
5280
|
+
function buildNpmArgs(parsed, access) {
|
|
5281
|
+
const args = ["publish", `--access=${access}`, "--ignore-scripts"];
|
|
5282
|
+
if (parsed.options.tag)
|
|
5283
|
+
args.push(`--tag=${parsed.options.tag}`);
|
|
5284
|
+
if (parsed.options.otp)
|
|
5285
|
+
args.push(`--otp=${parsed.options.otp}`);
|
|
5286
|
+
const reserved = new Set([
|
|
5287
|
+
"dir",
|
|
5288
|
+
"access",
|
|
5289
|
+
"tag",
|
|
5290
|
+
"otp",
|
|
5291
|
+
"dry",
|
|
5292
|
+
"no-check",
|
|
5293
|
+
"noCheck"
|
|
5294
|
+
]);
|
|
5295
|
+
for (const [key, value] of Object.entries(parsed.options)) {
|
|
5296
|
+
if (reserved.has(key))
|
|
5297
|
+
continue;
|
|
5298
|
+
args.push(`--${key}=${value}`);
|
|
5299
|
+
}
|
|
5300
|
+
for (const flag of parsed.flags) {
|
|
5301
|
+
if (reserved.has(flag) || flag === "dry")
|
|
5302
|
+
continue;
|
|
5303
|
+
if (flag === "no-check" || flag === "noCheck")
|
|
5304
|
+
continue;
|
|
5305
|
+
args.push(`--${flag}`);
|
|
5306
|
+
}
|
|
5307
|
+
return args;
|
|
5308
|
+
}
|
|
5309
|
+
function detectIndent2(jsonText) {
|
|
5310
|
+
const m = jsonText.match(/\n([ \t]+)"/);
|
|
5311
|
+
if (!m?.[1])
|
|
5312
|
+
return 4;
|
|
5313
|
+
return m[1].includes("\t") ? 4 : m[1].length;
|
|
5314
|
+
}
|
|
5315
|
+
|
|
5074
5316
|
// src/index.ts
|
|
5075
5317
|
function cliVersion() {
|
|
5076
5318
|
const here = dirname4(fileURLToPath2(import.meta.url));
|
|
5077
5319
|
for (const p of [
|
|
5078
|
-
|
|
5079
|
-
|
|
5320
|
+
join26(here, "..", "package.json"),
|
|
5321
|
+
join26(here, "package.json")
|
|
5080
5322
|
]) {
|
|
5081
5323
|
try {
|
|
5082
|
-
const v = JSON.parse(
|
|
5324
|
+
const v = JSON.parse(readFileSync29(p, "utf8")).version;
|
|
5083
5325
|
if (typeof v === "string" && v)
|
|
5084
5326
|
return v;
|
|
5085
5327
|
} catch {}
|
|
@@ -5143,6 +5385,8 @@ function showHelp() {
|
|
|
5143
5385
|
init [--template app|lib|panel|orbit] [--force] [--no-install]
|
|
5144
5386
|
build [--client] [--url=\u2026] [--name=\u2026] [--dir=\u2026] [--folder=\u2026]
|
|
5145
5387
|
postbuild [--dir=./dist]
|
|
5388
|
+
publish [--dry] [--no-check] [--dir=\u2026] [--access=\u2026] [--tag=\u2026] [--otp=\u2026]
|
|
5389
|
+
package.public.json \u2192 package.json \u2192 npm publish \u2192 \u043E\u0442\u043A\u0430\u0442
|
|
5146
5390
|
uuid
|
|
5147
5391
|
|
|
5148
5392
|
\u0424\u043B\u0430\u0433\u0438: -v/--version, -h/--help
|
|
@@ -5150,6 +5394,8 @@ function showHelp() {
|
|
|
5150
5394
|
\u041F\u0440\u0438\u043C\u0435\u0440\u044B:
|
|
5151
5395
|
elcrm doctor
|
|
5152
5396
|
elcrm doctor --docs --test
|
|
5397
|
+
elcrm publish
|
|
5398
|
+
elcrm publish --dry
|
|
5153
5399
|
elcrm update --fix --test && elcrm css && elcrm docs && elcrm cursor
|
|
5154
5400
|
elcrm css # ENOENT @elcrm/\u2026/light.css \u2192 \u0441\u043D\u0438\u043C\u0435\u0442 \u0431\u0438\u0442\u044B\u0439 import
|
|
5155
5401
|
elcrm audit
|
|
@@ -5209,6 +5455,9 @@ async function main() {
|
|
|
5209
5455
|
case "kit":
|
|
5210
5456
|
await cmdKit(parsed);
|
|
5211
5457
|
break;
|
|
5458
|
+
case "publish":
|
|
5459
|
+
await cmdPublish(parsed);
|
|
5460
|
+
break;
|
|
5212
5461
|
case "uuid":
|
|
5213
5462
|
await cmdUuid();
|
|
5214
5463
|
break;
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
+
import type { PrefixGroups } from "./plugin-css-scoped";
|
|
2
|
+
|
|
1
3
|
export type DiscoverLibOptions = {
|
|
2
4
|
libDir: string;
|
|
3
5
|
letterOverrides?: Record<string, string>;
|
|
6
|
+
/** Дефолтная семья; @default "c" */
|
|
7
|
+
prefix?: string;
|
|
8
|
+
prefixGroups?: PrefixGroups;
|
|
9
|
+
prefixOverrides?: Record<string, string>;
|
|
4
10
|
};
|
|
5
11
|
|
|
6
12
|
export type DiscoverLibResult = {
|
|
@@ -1,42 +1,90 @@
|
|
|
1
1
|
// src/vite/discover-lib.ts
|
|
2
2
|
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
// src/vite/css-prefix.ts
|
|
6
|
+
function assertPrefixLetter(value, label) {
|
|
7
|
+
if (!/^[a-z]$/.test(value)) {
|
|
8
|
+
throw new Error(`[css-scoped] ${label} должен быть одной буквой a-z, получено: "${value}"`);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function buildPrefixMap(options) {
|
|
12
|
+
const map = {};
|
|
13
|
+
const groups = options.prefixGroups ?? {};
|
|
14
|
+
for (const [pfx, names] of Object.entries(groups)) {
|
|
15
|
+
assertPrefixLetter(pfx, `prefixGroups["${pfx}"]`);
|
|
16
|
+
if (!Array.isArray(names)) {
|
|
17
|
+
throw new Error(`[css-scoped] prefixGroups["${pfx}"] должен быть массивом имён`);
|
|
18
|
+
}
|
|
19
|
+
for (const name of names) {
|
|
20
|
+
if (map[name] != null && map[name] !== pfx) {
|
|
21
|
+
throw new Error(`[css-scoped] "${name}" в двух семьях: ${map[name]} и ${pfx}`);
|
|
22
|
+
}
|
|
23
|
+
map[name] = pfx;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
for (const [name, pfx] of Object.entries(options.prefixOverrides ?? {})) {
|
|
27
|
+
assertPrefixLetter(pfx, `prefixOverrides["${name}"]`);
|
|
28
|
+
map[name] = pfx;
|
|
29
|
+
}
|
|
30
|
+
return map;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// src/vite/discover-lib.ts
|
|
34
|
+
var LETTER_ALPHABET = "abcdefghijklmnopqrstuvwxyz";
|
|
4
35
|
function listComponentDirs(libDir) {
|
|
5
36
|
return readdirSync(libDir).filter((name) => {
|
|
6
37
|
const dir = join(libDir, name);
|
|
7
38
|
return statSync(dir).isDirectory() && existsSync(join(dir, "index.ts"));
|
|
8
39
|
}).sort();
|
|
9
40
|
}
|
|
10
|
-
function
|
|
11
|
-
|
|
41
|
+
function slotFor(used, prefix) {
|
|
42
|
+
let set = used.get(prefix);
|
|
43
|
+
if (!set) {
|
|
44
|
+
set = new Set;
|
|
45
|
+
used.set(prefix, set);
|
|
46
|
+
}
|
|
47
|
+
return set;
|
|
48
|
+
}
|
|
49
|
+
function assignLetters(names, overrides, prefixOf) {
|
|
50
|
+
const used = new Map;
|
|
12
51
|
const letters = {};
|
|
13
52
|
for (const [name, letter] of Object.entries(overrides)) {
|
|
14
|
-
if (!/^[a-
|
|
15
|
-
throw new Error(`[discover-lib] override "${name}" → "${letter}" (нужна a-
|
|
53
|
+
if (!/^[a-z]$/.test(letter)) {
|
|
54
|
+
throw new Error(`[discover-lib] override "${name}" → "${letter}" (нужна одна буква a-z)`);
|
|
16
55
|
}
|
|
17
|
-
|
|
18
|
-
|
|
56
|
+
const pfx = prefixOf(name);
|
|
57
|
+
const slot = slotFor(used, pfx);
|
|
58
|
+
if (slot.has(letter)) {
|
|
59
|
+
throw new Error(`[discover-lib] дубль буквы "${letter}" в семье "${pfx}" (overrides)`);
|
|
19
60
|
}
|
|
20
|
-
|
|
61
|
+
slot.add(letter);
|
|
21
62
|
if (names.includes(name))
|
|
22
63
|
letters[name] = letter;
|
|
23
64
|
}
|
|
24
65
|
for (const name of names) {
|
|
25
66
|
if (letters[name])
|
|
26
67
|
continue;
|
|
68
|
+
const slot = slotFor(used, prefixOf(name));
|
|
27
69
|
const base = name.replace(/[^a-zA-Z]/g, "").toLowerCase();
|
|
28
|
-
|
|
70
|
+
const letter = [...base].find((c) => /[a-z]/.test(c) && !slot.has(c)) ?? [...LETTER_ALPHABET].find((c) => !slot.has(c));
|
|
29
71
|
if (!letter) {
|
|
30
|
-
throw new Error(`[discover-lib] нет свободной
|
|
72
|
+
throw new Error(`[discover-lib] нет свободной буквы a-z для "${name}" в семье "${prefixOf(name)}" — заведи новую семью в prefixGroups`);
|
|
31
73
|
}
|
|
32
|
-
|
|
74
|
+
slot.add(letter);
|
|
33
75
|
letters[name] = letter;
|
|
34
76
|
}
|
|
35
77
|
return letters;
|
|
36
78
|
}
|
|
37
79
|
function discoverLibComponents(options) {
|
|
38
80
|
const names = listComponentDirs(options.libDir);
|
|
39
|
-
const
|
|
81
|
+
const defaultPrefix = options.prefix ?? "c";
|
|
82
|
+
const prefixMap = buildPrefixMap({
|
|
83
|
+
prefixGroups: options.prefixGroups,
|
|
84
|
+
prefixOverrides: options.prefixOverrides
|
|
85
|
+
});
|
|
86
|
+
const prefixOf = (name) => prefixMap[name] ?? defaultPrefix;
|
|
87
|
+
const letters = assignLetters(names, options.letterOverrides ?? {}, prefixOf);
|
|
40
88
|
const entries = {
|
|
41
89
|
index: join(options.libDir, "index.ts")
|
|
42
90
|
};
|
|
@@ -269,13 +269,13 @@ function minifyCssVars(options) {
|
|
|
269
269
|
};
|
|
270
270
|
}
|
|
271
271
|
export {
|
|
272
|
-
|
|
273
|
-
replaceCssVarToken,
|
|
274
|
-
namespaceSize,
|
|
275
|
-
minifyCssVars,
|
|
276
|
-
isVendorJsChunk,
|
|
277
|
-
encodeName,
|
|
278
|
-
collectCssVarNames,
|
|
272
|
+
allocateUnique,
|
|
279
273
|
applyVarDictionary,
|
|
280
|
-
|
|
274
|
+
collectCssVarNames,
|
|
275
|
+
encodeName,
|
|
276
|
+
isVendorJsChunk,
|
|
277
|
+
minifyCssVars,
|
|
278
|
+
namespaceSize,
|
|
279
|
+
replaceCssVarToken,
|
|
280
|
+
toIndex
|
|
281
281
|
};
|
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Vite helper: короткие unique className для CSS Modules elCRM.
|
|
3
|
-
* Формат: 3
|
|
3
|
+
* Формат: 3 буквы `{prefix}{буква}{первая буква локального}`.
|
|
4
|
+
* В исходнике `.root` / `.icon`; в DOM `slr` / `llr`.
|
|
5
|
+
* Семьи: `prefixGroups` (буква уникальна внутри prefix).
|
|
4
6
|
* @see createCssScoped
|
|
5
7
|
*/
|
|
8
|
+
export type PrefixGroups = Record<string, string[]>;
|
|
9
|
+
|
|
10
|
+
export declare function buildPrefixMap(options: {
|
|
11
|
+
prefixGroups?: PrefixGroups;
|
|
12
|
+
prefixOverrides?: Record<string, string>;
|
|
13
|
+
}): Record<string, string>;
|
|
14
|
+
|
|
15
|
+
/** 3-й знак scoped: первая буква локального имени (`.root` → `r`). */
|
|
16
|
+
export declare function localChar(name: string): string;
|
|
17
|
+
|
|
6
18
|
export type CssScopedOptions = {
|
|
7
|
-
/**
|
|
19
|
+
/** Дефолтная семья (c = chrome/components, b = button, …) */
|
|
8
20
|
prefix: string;
|
|
9
|
-
/**
|
|
21
|
+
/** Семьи: prefix → папки компонентов */
|
|
22
|
+
prefixGroups?: PrefixGroups;
|
|
23
|
+
/** Свой prefix у компонента (перебивает группу) */
|
|
10
24
|
prefixOverrides?: Record<string, string>;
|
|
11
|
-
/** Папка компонента → одна буква */
|
|
25
|
+
/** Папка компонента → одна буква a–z */
|
|
12
26
|
components: Record<string, string>;
|
|
13
27
|
/** Каталог с папками компонентов (абсолютный путь) */
|
|
14
28
|
libDir: string;
|
|
@@ -1,6 +1,44 @@
|
|
|
1
1
|
// src/vite/plugin-css-scoped.ts
|
|
2
2
|
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
// src/vite/css-prefix.ts
|
|
6
|
+
function assertPrefixLetter(value, label) {
|
|
7
|
+
if (!/^[a-z]$/.test(value)) {
|
|
8
|
+
throw new Error(`[css-scoped] ${label} должен быть одной буквой a-z, получено: "${value}"`);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function buildPrefixMap(options) {
|
|
12
|
+
const map = {};
|
|
13
|
+
const groups = options.prefixGroups ?? {};
|
|
14
|
+
for (const [pfx, names] of Object.entries(groups)) {
|
|
15
|
+
assertPrefixLetter(pfx, `prefixGroups["${pfx}"]`);
|
|
16
|
+
if (!Array.isArray(names)) {
|
|
17
|
+
throw new Error(`[css-scoped] prefixGroups["${pfx}"] должен быть массивом имён`);
|
|
18
|
+
}
|
|
19
|
+
for (const name of names) {
|
|
20
|
+
if (map[name] != null && map[name] !== pfx) {
|
|
21
|
+
throw new Error(`[css-scoped] "${name}" в двух семьях: ${map[name]} и ${pfx}`);
|
|
22
|
+
}
|
|
23
|
+
map[name] = pfx;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
for (const [name, pfx] of Object.entries(options.prefixOverrides ?? {})) {
|
|
27
|
+
assertPrefixLetter(pfx, `prefixOverrides["${name}"]`);
|
|
28
|
+
map[name] = pfx;
|
|
29
|
+
}
|
|
30
|
+
return map;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// src/vite/css-local.ts
|
|
34
|
+
function localChar(name) {
|
|
35
|
+
if (!/^[a-z][a-z0-9]*$/.test(name)) {
|
|
36
|
+
throw new Error(`[css-scoped] локальный класс — [a-z][a-z0-9]*: ".${name}"`);
|
|
37
|
+
}
|
|
38
|
+
return name[0];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// src/vite/plugin-css-scoped.ts
|
|
4
42
|
var DEFAULT_MODULE_RE = /\/(?:src\/)?lib\/([^/]+)\/[^/]+\.module\.css$/;
|
|
5
43
|
function listModuleCss(dir) {
|
|
6
44
|
const out = [];
|
|
@@ -14,24 +52,20 @@ function listModuleCss(dir) {
|
|
|
14
52
|
return out;
|
|
15
53
|
}
|
|
16
54
|
function localClasses(css) {
|
|
55
|
+
const stripped = css.replace(/\/\*[\s\S]*?\*\//g, " ");
|
|
17
56
|
const set = new Set;
|
|
18
|
-
for (const m of
|
|
57
|
+
for (const m of stripped.matchAll(/(?:^|[,{\s>+~])\.([a-z][a-z0-9]*)\b/gi)) {
|
|
19
58
|
set.add(m[1].toLowerCase());
|
|
20
59
|
}
|
|
21
60
|
return [...set];
|
|
22
61
|
}
|
|
23
|
-
function assertPrefix(value, label) {
|
|
24
|
-
if (!/^[a-z]$/.test(value)) {
|
|
25
|
-
throw new Error(`[css-scoped] ${label} должен быть одной буквой a-z, получено: "${value}"`);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
62
|
function createCssScoped(options) {
|
|
29
63
|
const prefix = options.prefix;
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
64
|
+
assertPrefixLetter(prefix, "prefix");
|
|
65
|
+
const prefixMap = buildPrefixMap({
|
|
66
|
+
prefixGroups: options.prefixGroups,
|
|
67
|
+
prefixOverrides: options.prefixOverrides
|
|
68
|
+
});
|
|
35
69
|
const components = options.components;
|
|
36
70
|
const libDir = options.libDir;
|
|
37
71
|
const modulePathRe = options.modulePathRe ?? DEFAULT_MODULE_RE;
|
|
@@ -45,23 +79,21 @@ function createCssScoped(options) {
|
|
|
45
79
|
return m[1];
|
|
46
80
|
}
|
|
47
81
|
function prefixFor(component) {
|
|
48
|
-
return
|
|
82
|
+
return prefixMap[component] ?? prefix;
|
|
49
83
|
}
|
|
50
84
|
function generateScopedName(name, filename) {
|
|
51
|
-
|
|
52
|
-
throw new Error(`[css-scoped] локальный класс — 1 знак [a-z0-9] (scoped = 3 символа): ".${name}" (${filename})`);
|
|
53
|
-
}
|
|
85
|
+
const key = localChar(name);
|
|
54
86
|
const component = componentFromFilename(filename);
|
|
55
87
|
const letter = components[component];
|
|
56
88
|
if (!letter) {
|
|
57
89
|
throw new Error(`[css-scoped] нет буквы для компонента "${component}". Добавьте в components.`);
|
|
58
90
|
}
|
|
59
|
-
if (!/^[a-
|
|
60
|
-
throw new Error(`[css-scoped] ключ компонента "${component}" должен быть a-
|
|
91
|
+
if (!/^[a-z]$/.test(letter)) {
|
|
92
|
+
throw new Error(`[css-scoped] ключ компонента "${component}" должен быть a-z: "${letter}"`);
|
|
61
93
|
}
|
|
62
|
-
const scoped = `${prefixFor(component)}${letter}${
|
|
63
|
-
if (scoped
|
|
64
|
-
throw new Error(`[css-scoped] "${scoped}" ≠ 3
|
|
94
|
+
const scoped = `${prefixFor(component)}${letter}${key}`;
|
|
95
|
+
if (!/^[a-z]{3}$/.test(scoped)) {
|
|
96
|
+
throw new Error(`[css-scoped] "${scoped}" ≠ 3 буквы a-z (${component}.${name})`);
|
|
65
97
|
}
|
|
66
98
|
const owner = `${component}.${name}`;
|
|
67
99
|
const prev = registry.get(scoped);
|
|
@@ -77,6 +109,11 @@ function createCssScoped(options) {
|
|
|
77
109
|
function snapshot() {
|
|
78
110
|
return new Map(registry);
|
|
79
111
|
}
|
|
112
|
+
function groupedNames() {
|
|
113
|
+
const fromGroups = Object.values(options.prefixGroups ?? {}).flat();
|
|
114
|
+
const fromOverrides = Object.keys(options.prefixOverrides ?? {});
|
|
115
|
+
return [...new Set([...fromGroups, ...fromOverrides])];
|
|
116
|
+
}
|
|
80
117
|
function check() {
|
|
81
118
|
reset();
|
|
82
119
|
const folders = readdirSync(libDir).filter((n) => statSync(join(libDir, n)).isDirectory());
|
|
@@ -85,16 +122,9 @@ function createCssScoped(options) {
|
|
|
85
122
|
if (missingMap.length) {
|
|
86
123
|
throw new Error(`[css-scoped] нет буквы в components: ${missingMap.join(", ")}`);
|
|
87
124
|
}
|
|
88
|
-
const
|
|
89
|
-
if (
|
|
90
|
-
throw new Error(`[css-scoped] prefixOverrides без компонента: ${
|
|
91
|
-
}
|
|
92
|
-
const letters = Object.values(components);
|
|
93
|
-
const dupLetter = [
|
|
94
|
-
...new Set(letters.filter((l, i) => letters.indexOf(l) !== i))
|
|
95
|
-
];
|
|
96
|
-
if (dupLetter.length) {
|
|
97
|
-
throw new Error(`[css-scoped] дубли букв компонентов: ${dupLetter.join(", ")}`);
|
|
125
|
+
const unknownFamily = groupedNames().filter((n) => !mapped.has(n));
|
|
126
|
+
if (unknownFamily.length) {
|
|
127
|
+
throw new Error(`[css-scoped] prefixGroups/prefixOverrides без компонента: ${unknownFamily.join(", ")}`);
|
|
98
128
|
}
|
|
99
129
|
const namespaces = Object.entries(components).map(([name, letter]) => `${prefixFor(name)}${letter}`);
|
|
100
130
|
const dupNs = [
|
|
@@ -106,20 +136,27 @@ function createCssScoped(options) {
|
|
|
106
136
|
const files = listModuleCss(libDir);
|
|
107
137
|
for (const file of files) {
|
|
108
138
|
const css = readFileSync(file, "utf8");
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
139
|
+
const locals = localClasses(css);
|
|
140
|
+
const seen = new Map;
|
|
141
|
+
for (const local of locals) {
|
|
142
|
+
const ch = localChar(local);
|
|
143
|
+
const prev = seen.get(ch);
|
|
144
|
+
if (prev && prev !== local) {
|
|
145
|
+
const component = componentFromFilename(file);
|
|
146
|
+
throw new Error(`[css-scoped] ${component}: ".${prev}" и ".${local}" дают один 3-й знак "${ch}"`);
|
|
147
|
+
}
|
|
148
|
+
seen.set(ch, local);
|
|
117
149
|
generateScopedName(local, file);
|
|
118
150
|
}
|
|
119
151
|
}
|
|
120
152
|
const snap = snapshot();
|
|
121
|
-
const
|
|
122
|
-
|
|
153
|
+
const familyCounts = new Map;
|
|
154
|
+
for (const name of Object.keys(components)) {
|
|
155
|
+
const p = prefixFor(name);
|
|
156
|
+
familyCounts.set(p, (familyCounts.get(p) ?? 0) + 1);
|
|
157
|
+
}
|
|
158
|
+
const families = [...familyCounts.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([p, n]) => `${p}:${n}`).join(", ");
|
|
159
|
+
console.log(`[css-scoped] OK: ${files.length} module.css, ${snap.size} классов (${families})`);
|
|
123
160
|
return snap;
|
|
124
161
|
}
|
|
125
162
|
function plugin() {
|
|
@@ -148,5 +185,7 @@ function createCssScoped(options) {
|
|
|
148
185
|
};
|
|
149
186
|
}
|
|
150
187
|
export {
|
|
151
|
-
|
|
188
|
+
buildPrefixMap,
|
|
189
|
+
createCssScoped,
|
|
190
|
+
localChar
|
|
152
191
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "elcrm",
|
|
3
|
-
"version": "1.1.
|
|
4
|
-
"description": "CLI @elcrm/*: doctor --fix (порядок проекта), update, css, docs, cursor, kit, migrate",
|
|
3
|
+
"version": "1.1.34",
|
|
4
|
+
"description": "CLI @elcrm/*: doctor --fix (порядок проекта), update, css, docs, cursor, kit, migrate, publish",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "MaSkal <dev@elcrm.online>",
|
|
@@ -18,7 +18,7 @@ elcrm cursor
|
|
|
18
18
|
| # | Команда | Что делает |
|
|
19
19
|
| --- | --- | --- |
|
|
20
20
|
| 1 | `elcrm update --fix --test` | Обновляет глобальный `elcrm` + все `@elcrm/*` в `package.json` → install → **migrate front** (API/токены/css-sanitize) → внутри `--fix` ещё раз **sync CSS** → `elcrm test` |
|
|
21
|
-
| 2 | `elcrm css` | Недостающие
|
|
21
|
+
| 2 | `elcrm css` | Недостающие **цвета** → `theme-light` / `theme-dark`. Геометрию из пакетов с `tokens.css` **не** дублирует в `theme.css` (дефолты через `@import …/tokens.css`). **Не дописывает** `--button-*-shadow: none` и снимает такие строки. В `elcrm.css`: **добавляет** рабочие `@import`, **удаляет битые**. Затем css-sanitize |
|
|
22
22
|
| 3 | `elcrm docs` | Перезаписывает `docs/*.elCRM.md` (шпаргалки пакетов) |
|
|
23
23
|
| 4 | `elcrm cursor` | `.cursor/rules` + `.cursor/docs` + `AGENTS.md` для агента |
|
|
24
24
|
|
|
@@ -70,7 +70,7 @@ elcrm migrate front # пакет (то же, что doctor --fix без
|
|
|
70
70
|
elcrm migrate form-native # снять prop native у @elcrm/form (не трогает pages/Auth.tsx)
|
|
71
71
|
elcrm migrate native-html # <input|textarea> → StringField/CheckField/…
|
|
72
72
|
elcrm migrate css-sanitize # осиротевшие in srgb / transparent);
|
|
73
|
-
elcrm migrate components # Block/Row →
|
|
73
|
+
elcrm migrate components # Block/Row→Stack; onSelect→onValueChange; ActionGroup actions; --page-*/--item-*→--shell-*
|
|
74
74
|
elcrm migrate form-tokens
|
|
75
75
|
elcrm migrate form-aliases
|
|
76
76
|
elcrm migrate size-sml # size sm|md → s|m|l
|
|
@@ -111,7 +111,7 @@ minifyCssVars({ var: 2 })
|
|
|
111
111
|
|
|
112
112
|
Короткие имена **уникальны и подряд**: `--aa`, `--ab`, `--ac`… Занятые слоты пропускаются; `--field` не затирает `--field-border`. Чанки JSZip не минифицируются (декремент `--n`). Если слотов нет — сборка падает.
|
|
113
113
|
|
|
114
|
-
Также: `elcrm/vite/plugin-css-scoped` (`prefix` + `
|
|
114
|
+
Также: `elcrm/vite/plugin-css-scoped` (`prefix` + `prefixGroups`, scoped = 3 буквы a–z: `.root` → 3-й знак `r`; буква компонента уникальна внутри семьи), `discover-lib`, `concat-css`, `check-tokens`, `postbuild`.
|
|
115
115
|
|
|
116
116
|
### Прочее
|
|
117
117
|
|
|
@@ -127,6 +127,7 @@ elcrm init --template panel
|
|
|
127
127
|
elcrm init --template orbit --force --no-install
|
|
128
128
|
elcrm build --client [--url=…] [--name=…] [--dir=…] [--folder=…]
|
|
129
129
|
elcrm postbuild [--dir=./dist] # lib: склейка CSS после vite build
|
|
130
|
+
elcrm publish [--dry] [--no-check] # lib: package.public.json → npm publish → откат
|
|
130
131
|
elcrm uuid
|
|
131
132
|
elcrm -v / --help
|
|
132
133
|
```
|
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
# `@elcrm/components`
|
|
4
4
|
|
|
5
5
|
Shell UI: оболочка, шапка, секции, списки, Dropdown / RadioGroup / Pricing* / Avatar* / Breadcrumb / Stack.
|
|
6
|
-
**Цвета и размеры задаёт приложение** (`theme.css` + `theme-light` / `theme-dark`). Fallback в пакете нет.
|
|
6
|
+
**Цвета и размеры задаёт приложение** (`theme.css` + `theme-light` / `theme-dark`). Fallback в пакете нет.
|
|
7
|
+
**Локаль:** дефолтные aria/подписи — русский; переопределяй `navLabel` / `label` / `copyLabel` / `aria-label` (без i18n-runtime в пакете).
|
|
7
8
|
|
|
8
9
|
Токены как у form: сначала общие **`--shell-*`** (+ `--control-*`, `--popup-shadow`), потом узкие (`--header-height`, `--section-padding`, `--avatar-size-*`, …).
|
|
9
10
|
Дефолты подтягивает `elcrm css` из `@elcrm/components/tokens.css` + `light.css` / `dark.css`.
|
|
10
11
|
|
|
11
|
-
Группы — через `items` / `groups`, идентификатор пункта — **`value
|
|
12
|
+
Группы — через `items` / `groups`, идентификатор пункта — **`value`**, выбор — **`onValueChange`** (+ `defaultValue` где нужно).
|
|
13
|
+
`ActionGroup` — слоты `actions` / `children`, не декларативные `items`. Sidebar — только `groups` (не alias `items`).
|
|
12
14
|
|
|
13
15
|
```ts
|
|
14
16
|
import {
|
|
@@ -17,51 +19,85 @@ import {
|
|
|
17
19
|
Card, Stat, ActionGroup, Toolbar, TextGroup, ItemColumn, List, Item, PageHead, PageShell, ChatList, ChatMessages, ChatSplit, Sidebar, Popover,
|
|
18
20
|
Healthmap, PanelInfo,
|
|
19
21
|
Loading, EmptyState, RadioGroup, PricingGroup, PricingTable,
|
|
20
|
-
Table, CodeBlock, SplitPane, Banner, IconButton,
|
|
22
|
+
Table, CodeBlock, SplitPane, Banner, IconButton, Text,
|
|
21
23
|
createLazyResolver,
|
|
24
|
+
acquireOverlayZIndex,
|
|
22
25
|
} from "@elcrm/components";
|
|
23
|
-
import type { PricingPlan, HealthmapData } from "@elcrm/components";
|
|
26
|
+
import type { PricingPlan, HealthmapData, NavTreeItem } from "@elcrm/components";
|
|
24
27
|
```
|
|
25
28
|
|
|
29
|
+
Версия пакета: **≥ 0.5.x** (`onValueChange`, без Block/Row/Column).
|
|
30
|
+
## Роли (кто когда)
|
|
31
|
+
|
|
32
|
+
Не сливаем компоненты — выбирай по слою:
|
|
33
|
+
|
|
34
|
+
| Слой | Компоненты | Не путать с |
|
|
35
|
+
| --- | --- | --- |
|
|
36
|
+
| Оболочка app | `Layout` (+ Header / Footer / Sidebar) | PageShell |
|
|
37
|
+
| Каркас модуля | `PageShell` | Section |
|
|
38
|
+
| Шапка страницы | `PageHead` | Section title |
|
|
39
|
+
| Блок контента | `Section` (`direction` / `gap` / `columns` как у Stack) | PageHead |
|
|
40
|
+
| Нав app | `Sidebar` | NavSections (страница + lazy-панель) |
|
|
41
|
+
| Вкладки | `Menu variant="line"` = только табы; `TabSections` = табы + lazy-панель | |
|
|
42
|
+
| Две колонки | `SplitPane` = resize; `ChatSplit` = чат без resize | |
|
|
43
|
+
| Чат | `ChatList` / `ChatMessages` / `ChatSplit`; `ChatFrame` — каркас колонки | |
|
|
44
|
+
| Планы | `PricingGroup` / `PricingTable`; `RadioGroup variant="split"` — radio-UI | |
|
|
45
|
+
| Toolbar | `Toolbar` = фильтры; `ActionGroup` = иконки в карточке | |
|
|
46
|
+
| Текст | `Text` / `TextGroup` / `AvatarName` (+аватар) | |
|
|
47
|
+
| Статус | `Badge` метка; `Banner` полоса; `EmptyState` пустой экран | |
|
|
48
|
+
| Таблица | `Table` = `<table>`; `PricingTable` = матрица тарифов | |
|
|
49
|
+
| Раскладка | `Stack` (или `Section` с `direction`) | Block / Row / Column |
|
|
50
|
+
|
|
26
51
|
## Роли layout
|
|
27
52
|
|
|
28
53
|
- **PageShell** — каркас модуля (`scroll` в body + footer). `scroll={false}` — без внутреннего скролла.
|
|
29
54
|
- **ChatList** — колонка диалогов: слоты `header` / `footer`, `children` = `List` + `Item`. Токены `--chat-list-width`, `--chat-*` (алиасы `--shell-*`).
|
|
30
55
|
- **ChatMessages** — колонка переписки. `bodyRef` / `onBodyScroll`. Паддинги `--chat-messages-*-padding`.
|
|
31
|
-
- **ChatSplit** — ряд `list` + `messages` (`--chat-split-gap`).
|
|
56
|
+
- **ChatSplit** — ряд `list` + `messages` (`--chat-split-gap`). Не `SplitPane` (нет resize).
|
|
32
57
|
- **PageHead** — title + actions. `sup` — текст или Badge сразу после title (надстрочно; `0` скрыт). `sticky` — шапка прилипает к верху скролла (фон `--shell-bg`). `children` — фильтры/поиск под заголовком, отступ `--shell-gap`.
|
|
33
|
-
- **Section** — блок контента / auth.
|
|
58
|
+
- **Section** — блок контента / auth / подраздел. Корень по умолчанию `<section>` (не `<main>` — его даёт Layout). Тело: `direction` / `gap` / `columns` как у Stack. Ряд кнопок в `actions` — `<Stack direction="row">`.
|
|
34
59
|
- **Layout / Header** — оболочка приложения. `Layout sidebar={<Sidebar />}` — ряд: меню слева + контент. `fixed={["header", "footer", "sidebar"]}` (по умолчанию) — эти слоты не скроллятся, крутится только `children`. `fixed={[]}` — поток страницы. Высота каркаса: `--layout-height` (`100dvh`).
|
|
35
|
-
- **Sidebar** — левое
|
|
36
|
-
- **NavSections** — сайдбар
|
|
37
|
-
- **
|
|
60
|
+
- **Sidebar** — левое меню приложения. `variant="cards"` (карточки) или `"plain"` (плоский backoffice). Только `groups` + `value` / `defaultValue` + `onValueChange`. Подменю: у пункта `items` + `defaultOpen`. Контроль раскрытия: `open` / `onOpenChange`. Клик по родителю только раскрывает; выбор — у листьев. Слоты: `brand`, `extra`, `footer`. Меню аккаунта: `footer` + `footerItems` + `onFooterValueChange` — chip + панель в цветах темы (`Dropdown tone="account"`, токены `--sidebar-account-*`). `version` под chip. Отступ вложенных: `--sidebar-indent`.
|
|
61
|
+
- **NavSections** — сайдбар **страницы** + панель (не замена Sidebar в Layout). У группы обязателен `value`. Подменю как у Sidebar. Панель: `resolveSection` (lazy) **или** готовые `children`. Клик по родителю только раскрывает.
|
|
62
|
+
- **TabSections** — горизонтальные вкладки + панель (`resolveSection` или `children`). Только табы без панели — `Menu variant="line"`.
|
|
63
|
+
- **Stack** — ряд/колонка/сетка (`direction="row"|"column"|"grid"`). `Block` / `Row` / `Column` **удалены** из пакета → `elcrm migrate components`. Сетка: `auto-fit` + `--stack-grid-min`, либо `columns={4}` / `columns={["1fr", "100px", "auto"]}`. С `columns` ряды `stretch`; в одну линию — `style={{ alignItems: "center" }}`.
|
|
38
64
|
- **Card** — курсор: `--card-pointer` (`auto` / `pointer`). Сетка Stack: `--stack-grid-min`.
|
|
39
65
|
- **Stat** — метрика: `label` + `value`, `tone` как у Badge, `labelMuted`. Не собирать из `span` в приложении.
|
|
40
|
-
- **Healthmap** — теплокарта
|
|
41
|
-
- **PanelInfo** — выезд
|
|
42
|
-
- **ActionGroup** — ряд иконок в карточке / строке: `label` + `
|
|
43
|
-
- **Toolbar** — фильтры над списком. `size`
|
|
66
|
+
- **Healthmap** — теплокарта (`data.days`). Слоты `brandName` / `brandSub` / `unit` / `hint`; locale: `months` / `weekdays` / `locale` / `loadingLabel` / `lastYearLabel`. Токены `--healthmap-cell`, `--healthmap-gap`, `--healthmap-l0`…`l4`.
|
|
67
|
+
- **PanelInfo** — выезд справа. Desktop — `aside`; узкий (`max-width: 56rem`) — `dialog` + `aria-modal`, focus trap, Escape не перебивает вложенный Popover. `open` / `defaultOpen` + `onOpenChange` (`onClose` — alias). Слоты: `children`, `footer`, `headerExtra`, `close`. Токены `--panel-info-*`.
|
|
68
|
+
- **ActionGroup** — ряд иконок в карточке / строке: `label` + `actions` и/или `children` (не group-`items`). Кнопки — `@elcrm/button`.
|
|
69
|
+
- **Toolbar** — фильтры над списком. `size` — токены `data-size` + `useToolbar()` (не cloneElement). Поля `@elcrm/form` ≥ 0.1.20 и `Dropdown` читают контекст. `align` / `width`. Токены `--toolbar-gap`, `--select-height` на `data-size`. Не `ActionGroup`.
|
|
70
|
+
- **Popover** — portal у якоря. Канон: `trigger={(api) => <button {...api.anchorProps}>…</button>}` (без cloneElement). Узел-trigger — клик/ARIA на обёртке.
|
|
44
71
|
- **TextGroup** — две строки: `title` + `description` (без аватара; с аватаром — AvatarName).
|
|
45
|
-
- **
|
|
72
|
+
- **Text** — короткий текст. `tone="muted"` → `--shell-color-muted`. `as` — тег (`p`/`span`/…). Не `className="muted"` в приложении.
|
|
73
|
+
- **Loading** — индикатор ожидания. Мин. высота: `--loading-min-height` (`min(50vh, 24rem)`).
|
|
74
|
+
- **EmptyState** — заглушка / ошибка страницы. `--empty-max-width`, `--empty-padding`.
|
|
75
|
+
- **Item** — строка списка. По умолчанию `<li>`; кликабельный — `as="button"` / `as="a"`. `active`. `variant="card"` — рамка и muted-фон. Не дублировать `.desk-row` в приложении.
|
|
76
|
+
- **Brand** — логотип в шапке. По умолчанию `<strong>`; кликабельный — `as="button"` / `as="a"`.
|
|
46
77
|
- **ItemColumn** — внутри Item: `icon` слева, столбик `title` / `description` / `footer` или `meta`, `extra` — бейджи справа сверху, `aside` — колонка справа. `unread` — жирный title. Title: `--item-column-title-size` / `--item-column-title-line` (в console = `--button-size-s` / `--button-line-height`).
|
|
47
|
-
- **Table** — `<table>` в обёртке со скроллом. `layout="fixed"
|
|
78
|
+
- **Table** — `<table>` в обёртке со скроллом. `stickyHeader` — липкий thead. `layout="fixed"`. Подпись — `<caption>`. Токены `--table-*`.
|
|
79
|
+
- **ChatFrame** — внутренний каркас ChatList/Messages; **не** в корневом экспорте → `@elcrm/components/ChatFrame` (`elcrm migrate components`).
|
|
48
80
|
- **CodeBlock** — bar + `<pre>`, `onCopy`, `children` для подсветки. Токены `--code-block-*`.
|
|
49
|
-
- **SplitPane** — две панели + drag handle. `direction`, `ratio`, `onRatioChange`, `minFirst` / `minSecond`. Не `ChatSplit` (только чат). Токены `--split-handle-*`.
|
|
81
|
+
- **SplitPane** — две панели + drag handle. `direction`, `ratio` / `defaultRatio`, `onRatioChange`, `minFirst` / `minSecond`. Не `ChatSplit` (только чат). Токены `--split-handle-*`.
|
|
50
82
|
- **Banner** — статусная полоса (`tone`: info / warning / danger / success). Не toast — не `Notice`.
|
|
51
83
|
- **IconButton** — `name` (`Icons.Line`) + обязательный `label` + `Tooltip` по умолчанию. `tooltip={false}` без подсказки. Низкий уровень — `@elcrm/button/IconButton` (`icon` + `label`).
|
|
52
84
|
|
|
53
85
|
## Меню
|
|
54
86
|
|
|
55
|
-
- **Menu** — inline-навигация в шапке. `items
|
|
56
|
-
- **Dropdown** — выпадающее меню по `items`/`groups` (portal)
|
|
57
|
-
- **Popover** — свой попап в portal (якорь + `children`).
|
|
87
|
+
- **Menu** — inline-навигация в шапке. `items` + `value` / `defaultValue` + `onValueChange`. `badge` (`0` скрыт). `variant="line"` — вкладки с чертой снизу.
|
|
88
|
+
- **Dropdown** — выпадающее меню по `items`/`groups` (portal) + `onValueChange`. `placement="auto"|"top"|"bottom"`. `size` — высота триггера; в `Toolbar` берётся сам. `item.submenu` — только шеврон (намёк); вложенное меню — снаружи.
|
|
89
|
+
- **Popover** — свой попап в portal (якорь + `children`). По умолчанию **без** `role="menu"`; меню пунктов — `Dropdown` (`role="menu"`). ARIA на триггере. z-index: `acquireOverlayZIndex`. Канон `trigger={(api) => …}`.
|
|
90
|
+
- **Breadcrumb** — `items` + `separator` / `collapseAfter`. `item.dropdown` — только ↕-намёк; меню — обернуть в `Dropdown`.
|
|
91
|
+
- Native `<button>` внутри chrome пакета — **внутренний primitive**; в приложении — `@elcrm/button`.
|
|
58
92
|
|
|
59
93
|
## Avatar
|
|
60
94
|
|
|
61
95
|
- **Avatar** — фото / инициалы (`name` только для инициалов). Скругление: `radius={10}` или `--avatar-radius` (дефолт `999px` — круг).
|
|
62
96
|
- **Badge** — метка: `tone`, `size` (`s`/`m`/`l`), `dot` (точка слева). Не Button.
|
|
63
97
|
- **AvatarName** — `title` + `description` (+ `name` для инициалов). Тот же `radius`. Цвета: `--avatar-name-title-color`, `--avatar-name-description-color`.
|
|
64
|
-
- **AvatarGroup** — `items` с `value`
|
|
98
|
+
- **AvatarGroup** — `items` с обязательным `value` + `title` / `name` / `src`; клик — `onValueChange(value)`.
|
|
99
|
+
- **Breadcrumb** — `items` + `onValueChange(value)`; `separator`, `collapseAfter`.
|
|
100
|
+
- **createLazyResolver** — кэш lazy-чанков; при ошибке загрузки слот сбрасывается; `.clear(segment?)` для retry.
|
|
65
101
|
|
|
66
102
|
## Pricing
|
|
67
103
|
|
|
@@ -79,12 +115,18 @@ import type { PricingPlan, HealthmapData } from "@elcrm/components";
|
|
|
79
115
|
- `--popup-shadow` общий с `@elcrm/form`.
|
|
80
116
|
- `elcrm css` — недостающие `--shell-*`.
|
|
81
117
|
- Импорт `@elcrm/components/style.css` — опционально.
|
|
118
|
+
- CSS Modules: в DOM всегда **3 буквы** `{семья}{компонент}{первая буква локального}` (короткий код, все a–z): List `.root` → `llr`, Layout `.root` → `slr`. В исходнике — слова. Семьи: `l` lists, `n` nav, `s` shell, `k` kit, `t` type, `a` avatar, `o` overlay, `g` pricing, `h` healthmap, `d` data. Не опирайся на эти className в приложении.
|
|
82
119
|
|
|
83
120
|
## Нельзя
|
|
84
121
|
|
|
85
122
|
- `import "@elcrm/…/themes.css"`.
|
|
86
|
-
- `Block` / `Row` / `Column` — `Stack` (`elcrm migrate components
|
|
123
|
+
- `Block` / `Row` / `Column` — удалены; только `Stack` (или `Section` с `direction`). `elcrm migrate components`.
|
|
124
|
+
- `applyToolbarSize` — удалён; размер — `useToolbar` / `data-size`.
|
|
125
|
+
- `PricingGroupItem` — удалён; тип `PricingPlan`.
|
|
87
126
|
- `key` / `activeKey` / `defaultActiveKey` в группах — `value` / `defaultValue` (`elcrm migrate components`).
|
|
127
|
+
- `onSelect` / `onItemClick` / `onFooterSelect` у Menu / Sidebar / Dropdown / Breadcrumb / AvatarGroup — `onValueChange` / `onFooterValueChange` (`elcrm migrate components`).
|
|
128
|
+
- `ActionGroup items=` — `actions=`; `Sidebar items=` (группы) — `groups=`.
|
|
129
|
+
- `ChatFrame` из `@elcrm/components` — deep `@elcrm/components/ChatFrame` (`elcrm migrate components`).
|
|
88
130
|
- Нативные `<button>` / инпуты вместо `@elcrm/button` / `@elcrm/form` в приложении.
|
|
89
131
|
- `size="sm"|"md"` → `"s"|"m"`.
|
|
90
132
|
- Хардкод цветов в модулях приложения, если есть токен.
|
|
@@ -32,7 +32,7 @@ const form = useForm({ login: "", password: "" });
|
|
|
32
32
|
Канонические поля: `StringField`, `PasswordField`, `TextareaField`, `NumberField`, `PercentField`, `MoneyField`, `MaskField`, `PhoneField`, `EmailField`, `UrlField`, `DateField`, `TimeField`, `SelectField`, `OptionsField`, `ModalField`, `RangeField`, `CheckField`, `RadioField`, `TagsField`, `RatingField`, `CodeField`, `FileField`, `DragDropField`, `RichTextField`, `ColorField`, `HiddenField`, `DisplayField`, `SearchField`, `TabsField`.
|
|
33
33
|
|
|
34
34
|
`disabled`, `size` (`"s"` | `"m"` | `"l"`) — у всех видимых `*Field` (кроме `HiddenField`).
|
|
35
|
-
В ряду фильтров — `<Toolbar size="s">` из `@elcrm/components
|
|
35
|
+
В ряду фильтров — `<Toolbar size="s">` из `@elcrm/components` (дефолт тулбара уже `"s"`): без своего `size` поля берут размер через `useToolbar` (`@elcrm/form` ≥ 0.1.20). Явный `size` у поля важнее. `--select-height` = `var(--field-height)` — селект той же высоты, что капсула.
|
|
36
36
|
|
|
37
37
|
`PasswordField`: `generate` — кнопка случайного пароля. `true` → `Az09#` (A–Z, a–z, 0–9, символы). `generate="A"` только заглавные, `"59"` цифры 5–9, `"#"` только символы. Длина — `maxLength` или 12.
|
|
38
38
|
|
|
@@ -139,6 +139,17 @@ body[data-theme="dark"] {
|
|
|
139
139
|
--sidebar-item-active: var(--shell-color);
|
|
140
140
|
--sidebar-item-active-bg: var(--shell-background-selected);
|
|
141
141
|
--sidebar-icon-active: var(--shell-color-accent);
|
|
142
|
+
--sidebar-account-menu-bg: var(--shell-background);
|
|
143
|
+
--sidebar-account-menu-color: var(--shell-color);
|
|
144
|
+
--sidebar-account-menu-border: var(--shell-border);
|
|
145
|
+
--sidebar-account-menu-shadow: var(--popup-shadow, var(--shell-shadow));
|
|
146
|
+
--sidebar-account-item-hover: var(--shell-background-hover);
|
|
147
|
+
--sidebar-account-item-icon: var(--shell-color-muted);
|
|
148
|
+
--sidebar-account-danger-hover: color-mix(
|
|
149
|
+
in srgb,
|
|
150
|
+
var(--shell-color-danger) 14%,
|
|
151
|
+
transparent
|
|
152
|
+
);
|
|
142
153
|
--shell-background-hover: color-mix(in srgb, var(--text) 8%, transparent);
|
|
143
154
|
--shell-background-selected: color-mix(in srgb, var(--accent) 14%, transparent);
|
|
144
155
|
--shell-shadow: var(--shadow);
|
|
@@ -139,6 +139,17 @@ body[data-theme="light"] {
|
|
|
139
139
|
--sidebar-item-active: var(--shell-color);
|
|
140
140
|
--sidebar-item-active-bg: var(--shell-background-selected);
|
|
141
141
|
--sidebar-icon-active: var(--shell-color-accent);
|
|
142
|
+
--sidebar-account-menu-bg: var(--shell-background);
|
|
143
|
+
--sidebar-account-menu-color: var(--shell-color);
|
|
144
|
+
--sidebar-account-menu-border: var(--shell-border);
|
|
145
|
+
--sidebar-account-menu-shadow: var(--popup-shadow, var(--shell-shadow));
|
|
146
|
+
--sidebar-account-item-hover: var(--shell-background-hover);
|
|
147
|
+
--sidebar-account-item-icon: var(--shell-color-muted);
|
|
148
|
+
--sidebar-account-danger-hover: color-mix(
|
|
149
|
+
in srgb,
|
|
150
|
+
var(--shell-color-danger) 12%,
|
|
151
|
+
transparent
|
|
152
|
+
);
|
|
142
153
|
--shell-background-hover: color-mix(in srgb, var(--text) 6%, transparent);
|
|
143
154
|
--shell-background-selected: color-mix(in srgb, var(--accent) 10%, transparent);
|
|
144
155
|
--shell-shadow: var(--shadow);
|
|
@@ -64,6 +64,11 @@
|
|
|
64
64
|
/* Layout: внутренний отступ и внешний margin каркаса */
|
|
65
65
|
--layout-padding: 0;
|
|
66
66
|
--layout-margin: 0;
|
|
67
|
+
--layout-gap: 0;
|
|
68
|
+
--layout-main-radius: 0;
|
|
69
|
+
--layout-main-background: transparent;
|
|
70
|
+
--layout-main-shadow: none;
|
|
71
|
+
--layout-main-border: none;
|
|
67
72
|
/* none — на всю ширину; иначе например 1440px */
|
|
68
73
|
--layout-max-width: none;
|
|
69
74
|
--elcrm-z-popover: 1000;
|
|
@@ -114,6 +119,7 @@
|
|
|
114
119
|
--item-column-icon-radius: var(--avatar-radius);
|
|
115
120
|
--item-column-title-size: var(--shell-font-size);
|
|
116
121
|
--item-column-title-line: 1.3;
|
|
122
|
+
--item-column-meta-size: var(--shell-font-size-sm);
|
|
117
123
|
--stat-value-size: 1.75rem;
|
|
118
124
|
--action-group-gap: var(--shell-gap-sm);
|
|
119
125
|
--toolbar-gap: var(--shell-gap-sm);
|
|
@@ -126,6 +132,14 @@
|
|
|
126
132
|
--sidebar-icon-size: 16px;
|
|
127
133
|
--sidebar-group-font-size: 11px;
|
|
128
134
|
--sidebar-indent: 14px;
|
|
135
|
+
--sidebar-account-chip-radius: 14px;
|
|
136
|
+
--sidebar-account-chip-padding: 8px;
|
|
137
|
+
--sidebar-account-chip-gap: 10px;
|
|
138
|
+
--sidebar-account-menu-radius: 18px;
|
|
139
|
+
--sidebar-account-menu-padding: 6px;
|
|
140
|
+
--sidebar-account-item-radius: 12px;
|
|
141
|
+
--sidebar-account-item-padding: 10px 12px;
|
|
142
|
+
--sidebar-account-item-gap: 12px;
|
|
129
143
|
--pricing-card-min-width: 16rem;
|
|
130
144
|
--pricing-table-label-width: 10rem;
|
|
131
145
|
--chat-list-width: 340px;
|
|
@@ -152,6 +166,8 @@
|
|
|
152
166
|
--table-cell-padding: 0.55rem 0.7rem;
|
|
153
167
|
--table-head-font-size: 0.75rem;
|
|
154
168
|
--table-code-font-size: 0.8rem;
|
|
169
|
+
--table-head-background: var(--shell-background);
|
|
170
|
+
--table-max-height: none;
|
|
155
171
|
--code-block-bg: var(--shell-background);
|
|
156
172
|
--code-block-bar-padding: 0.4rem 0.75rem;
|
|
157
173
|
--code-block-padding: 0.85rem 1rem;
|
|
@@ -168,6 +184,9 @@
|
|
|
168
184
|
--banner-padding: 0.55rem 0.85rem;
|
|
169
185
|
--banner-bg: color-mix(in srgb, var(--shell-color-accent) 8%, var(--shell-bg));
|
|
170
186
|
--banner-border: color-mix(in srgb, var(--shell-color-accent) 35%, var(--shell-border));
|
|
187
|
+
--loading-min-height: min(50vh, 24rem);
|
|
188
|
+
--empty-max-width: 36rem;
|
|
189
|
+
--empty-padding: 2rem;
|
|
171
190
|
}
|
|
172
191
|
|
|
173
192
|
/* —— база приложения (без захардкоженных цветов) —— */
|
|
@@ -144,6 +144,17 @@ body[data-theme="dark"] {
|
|
|
144
144
|
--sidebar-item-active: var(--shell-color);
|
|
145
145
|
--sidebar-item-active-bg: var(--shell-background-selected);
|
|
146
146
|
--sidebar-icon-active: var(--shell-color-accent);
|
|
147
|
+
--sidebar-account-menu-bg: var(--shell-background);
|
|
148
|
+
--sidebar-account-menu-color: var(--shell-color);
|
|
149
|
+
--sidebar-account-menu-border: var(--shell-border);
|
|
150
|
+
--sidebar-account-menu-shadow: var(--popup-shadow, var(--shell-shadow));
|
|
151
|
+
--sidebar-account-item-hover: var(--shell-background-hover);
|
|
152
|
+
--sidebar-account-item-icon: var(--shell-color-muted);
|
|
153
|
+
--sidebar-account-danger-hover: color-mix(
|
|
154
|
+
in srgb,
|
|
155
|
+
var(--shell-color-danger) 14%,
|
|
156
|
+
transparent
|
|
157
|
+
);
|
|
147
158
|
--shell-background-hover: color-mix(in srgb, var(--text) 8%, transparent);
|
|
148
159
|
--shell-background-selected: color-mix(in srgb, var(--accent) 14%, transparent);
|
|
149
160
|
--shell-shadow: var(--shadow);
|
|
@@ -144,6 +144,17 @@ body[data-theme="light"] {
|
|
|
144
144
|
--sidebar-item-active: var(--shell-color);
|
|
145
145
|
--sidebar-item-active-bg: var(--shell-background-selected);
|
|
146
146
|
--sidebar-icon-active: var(--shell-color-accent);
|
|
147
|
+
--sidebar-account-menu-bg: var(--shell-background);
|
|
148
|
+
--sidebar-account-menu-color: var(--shell-color);
|
|
149
|
+
--sidebar-account-menu-border: var(--shell-border);
|
|
150
|
+
--sidebar-account-menu-shadow: var(--popup-shadow, var(--shell-shadow));
|
|
151
|
+
--sidebar-account-item-hover: var(--shell-background-hover);
|
|
152
|
+
--sidebar-account-item-icon: var(--shell-color-muted);
|
|
153
|
+
--sidebar-account-danger-hover: color-mix(
|
|
154
|
+
in srgb,
|
|
155
|
+
var(--shell-color-danger) 12%,
|
|
156
|
+
transparent
|
|
157
|
+
);
|
|
147
158
|
--shell-background-hover: color-mix(in srgb, var(--text) 6%, transparent);
|
|
148
159
|
--shell-background-selected: color-mix(in srgb, var(--accent) 10%, transparent);
|
|
149
160
|
--shell-shadow: var(--shadow);
|
|
@@ -64,6 +64,11 @@
|
|
|
64
64
|
/* Layout: внутренний отступ и внешний margin каркаса */
|
|
65
65
|
--layout-padding: 0;
|
|
66
66
|
--layout-margin: 0;
|
|
67
|
+
--layout-gap: 0;
|
|
68
|
+
--layout-main-radius: 0;
|
|
69
|
+
--layout-main-background: transparent;
|
|
70
|
+
--layout-main-shadow: none;
|
|
71
|
+
--layout-main-border: none;
|
|
67
72
|
/* none — на всю ширину; иначе например 1440px */
|
|
68
73
|
--layout-max-width: none;
|
|
69
74
|
--elcrm-z-popover: 1000;
|
|
@@ -125,6 +130,7 @@
|
|
|
125
130
|
--item-column-icon-radius: var(--avatar-radius);
|
|
126
131
|
--item-column-title-size: var(--shell-font-size);
|
|
127
132
|
--item-column-title-line: 1.3;
|
|
133
|
+
--item-column-meta-size: var(--shell-font-size-sm);
|
|
128
134
|
--stat-value-size: 1.75rem;
|
|
129
135
|
--action-group-gap: var(--shell-gap-sm);
|
|
130
136
|
--toolbar-gap: var(--shell-gap-sm);
|
|
@@ -137,6 +143,14 @@
|
|
|
137
143
|
--sidebar-icon-size: 16px;
|
|
138
144
|
--sidebar-group-font-size: 11px;
|
|
139
145
|
--sidebar-indent: 14px;
|
|
146
|
+
--sidebar-account-chip-radius: 14px;
|
|
147
|
+
--sidebar-account-chip-padding: 8px;
|
|
148
|
+
--sidebar-account-chip-gap: 10px;
|
|
149
|
+
--sidebar-account-menu-radius: 18px;
|
|
150
|
+
--sidebar-account-menu-padding: 6px;
|
|
151
|
+
--sidebar-account-item-radius: 12px;
|
|
152
|
+
--sidebar-account-item-padding: 10px 12px;
|
|
153
|
+
--sidebar-account-item-gap: 12px;
|
|
140
154
|
--pricing-card-min-width: 16rem;
|
|
141
155
|
--pricing-table-label-width: 10rem;
|
|
142
156
|
--chat-list-width: 340px;
|
|
@@ -163,6 +177,8 @@
|
|
|
163
177
|
--table-cell-padding: 0.55rem 0.7rem;
|
|
164
178
|
--table-head-font-size: 0.75rem;
|
|
165
179
|
--table-code-font-size: 0.8rem;
|
|
180
|
+
--table-head-background: var(--shell-background);
|
|
181
|
+
--table-max-height: none;
|
|
166
182
|
--code-block-bg: var(--shell-background);
|
|
167
183
|
--code-block-bar-padding: 0.4rem 0.75rem;
|
|
168
184
|
--code-block-padding: 0.85rem 1rem;
|
|
@@ -179,6 +195,9 @@
|
|
|
179
195
|
--banner-padding: 0.55rem 0.85rem;
|
|
180
196
|
--banner-bg: color-mix(in srgb, var(--shell-color-accent) 8%, var(--shell-bg));
|
|
181
197
|
--banner-border: color-mix(in srgb, var(--shell-color-accent) 35%, var(--shell-border));
|
|
198
|
+
--loading-min-height: min(50vh, 24rem);
|
|
199
|
+
--empty-max-width: 36rem;
|
|
200
|
+
--empty-padding: 2rem;
|
|
182
201
|
}
|
|
183
202
|
|
|
184
203
|
/* —— база приложения (без захардкоженных цветов) —— */
|