elcrm 1.1.28 → 1.1.35

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 CHANGED
@@ -2,8 +2,8 @@
2
2
  // @bun
3
3
 
4
4
  // src/index.ts
5
- import { readFileSync as readFileSync28 } from "fs";
6
- import { dirname as dirname4, join as join25 } from "path";
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;
@@ -5072,15 +5170,186 @@ async function cmdKit(parsed) {
5072
5170
  process.exit(1);
5073
5171
  }
5074
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
+ const local = readPackageJson(cwd);
5191
+ if (!local?.name || !local.version) {
5192
+ throw new Error(`${PKG_NAME}: \u043D\u0443\u0436\u043D\u044B \u043F\u043E\u043B\u044F name \u0438 version`);
5193
+ }
5194
+ if (!existsSync20(publicPath)) {
5195
+ await publishPlain(cwd, local, parsed, dry, noCheck);
5196
+ return;
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 publishPlain(cwd, local, parsed, dry, noCheck) {
5252
+ const access = parsed.options.access || (typeof local.publishConfig?.access === "string" ? local.publishConfig.access : "public");
5253
+ console.log(`[publish] ${local.name}@${local.version}`);
5254
+ console.log(`[publish] \u0431\u0435\u0437 ${PUBLIC_NAME} \u2014 npm publish \u0442\u0435\u043A\u0443\u0449\u0435\u0433\u043E ${PKG_NAME}`);
5255
+ if (!noCheck && !dry) {
5256
+ await runPreChecks(cwd, local.scripts);
5257
+ } else if (noCheck) {
5258
+ console.log("[publish] \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u043F\u0440\u043E\u043F\u0443\u0449\u0435\u043D\u044B (--no-check)");
5259
+ }
5260
+ const npmArgs = buildNpmArgs(parsed, access);
5261
+ console.log(`[publish] npm ${npmArgs.join(" ")}`);
5262
+ if (dry) {
5263
+ console.log("[publish] --dry: publish \u043D\u0435 \u0432\u044B\u043F\u043E\u043B\u043D\u044F\u043B\u0441\u044F");
5264
+ return;
5265
+ }
5266
+ const r = spawnSync3("npm", npmArgs, {
5267
+ cwd,
5268
+ stdio: "inherit",
5269
+ env: process.env
5270
+ });
5271
+ if (r.error) {
5272
+ console.error(r.error.message);
5273
+ }
5274
+ if ((r.status ?? 1) !== 0) {
5275
+ process.exit(r.status ?? 1);
5276
+ }
5277
+ console.log(`[publish] \u0433\u043E\u0442\u043E\u0432\u043E: ${local.name}@${local.version}`);
5278
+ }
5279
+ async function runPreChecks(cwd, scripts) {
5280
+ const list = scripts ?? {};
5281
+ const chain = [];
5282
+ if (list.prepublishOnly)
5283
+ chain.push("prepublishOnly");
5284
+ else {
5285
+ if (list["check:css"])
5286
+ chain.push("check:css");
5287
+ if (list.test)
5288
+ chain.push("test");
5289
+ if (list.build)
5290
+ chain.push("build");
5291
+ }
5292
+ if (chain.length === 0) {
5293
+ 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");
5294
+ return;
5295
+ }
5296
+ for (const script of chain) {
5297
+ console.log(`[publish] bun run ${script}`);
5298
+ const r = spawnSync3("bun", ["run", script], {
5299
+ cwd,
5300
+ stdio: "inherit",
5301
+ env: process.env
5302
+ });
5303
+ if ((r.status ?? 1) !== 0) {
5304
+ 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}`);
5305
+ }
5306
+ }
5307
+ }
5308
+ function buildNpmArgs(parsed, access) {
5309
+ const args = ["publish", `--access=${access}`, "--ignore-scripts"];
5310
+ if (parsed.options.tag)
5311
+ args.push(`--tag=${parsed.options.tag}`);
5312
+ if (parsed.options.otp)
5313
+ args.push(`--otp=${parsed.options.otp}`);
5314
+ const reserved = new Set([
5315
+ "dir",
5316
+ "access",
5317
+ "tag",
5318
+ "otp",
5319
+ "dry",
5320
+ "no-check",
5321
+ "noCheck"
5322
+ ]);
5323
+ for (const [key, value] of Object.entries(parsed.options)) {
5324
+ if (reserved.has(key))
5325
+ continue;
5326
+ args.push(`--${key}=${value}`);
5327
+ }
5328
+ for (const flag of parsed.flags) {
5329
+ if (reserved.has(flag) || flag === "dry")
5330
+ continue;
5331
+ if (flag === "no-check" || flag === "noCheck")
5332
+ continue;
5333
+ args.push(`--${flag}`);
5334
+ }
5335
+ return args;
5336
+ }
5337
+ function detectIndent2(jsonText) {
5338
+ const m = jsonText.match(/\n([ \t]+)"/);
5339
+ if (!m?.[1])
5340
+ return 4;
5341
+ return m[1].includes("\t") ? 4 : m[1].length;
5342
+ }
5343
+
5075
5344
  // src/index.ts
5076
5345
  function cliVersion() {
5077
5346
  const here = dirname4(fileURLToPath2(import.meta.url));
5078
5347
  for (const p of [
5079
- join25(here, "..", "package.json"),
5080
- join25(here, "package.json")
5348
+ join26(here, "..", "package.json"),
5349
+ join26(here, "package.json")
5081
5350
  ]) {
5082
5351
  try {
5083
- const v = JSON.parse(readFileSync28(p, "utf8")).version;
5352
+ const v = JSON.parse(readFileSync29(p, "utf8")).version;
5084
5353
  if (typeof v === "string" && v)
5085
5354
  return v;
5086
5355
  } catch {}
@@ -5144,6 +5413,8 @@ function showHelp() {
5144
5413
  init [--template app|lib|panel|orbit] [--force] [--no-install]
5145
5414
  build [--client] [--url=\u2026] [--name=\u2026] [--dir=\u2026] [--folder=\u2026]
5146
5415
  postbuild [--dir=./dist]
5416
+ publish [--dry] [--no-check] [--dir=\u2026] [--access=\u2026] [--tag=\u2026] [--otp=\u2026]
5417
+ npm publish; \u0435\u0441\u043B\u0438 \u0435\u0441\u0442\u044C package.public.json \u2014 \u043F\u043E\u0434\u043C\u0435\u043D\u0430 \u043C\u0430\u043D\u0438\u0444\u0435\u0441\u0442\u0430 \u0438 \u043E\u0442\u043A\u0430\u0442
5147
5418
  uuid
5148
5419
 
5149
5420
  \u0424\u043B\u0430\u0433\u0438: -v/--version, -h/--help
@@ -5151,6 +5422,8 @@ function showHelp() {
5151
5422
  \u041F\u0440\u0438\u043C\u0435\u0440\u044B:
5152
5423
  elcrm doctor
5153
5424
  elcrm doctor --docs --test
5425
+ elcrm publish
5426
+ elcrm publish --dry
5154
5427
  elcrm update --fix --test && elcrm css && elcrm docs && elcrm cursor
5155
5428
  elcrm css # ENOENT @elcrm/\u2026/light.css \u2192 \u0441\u043D\u0438\u043C\u0435\u0442 \u0431\u0438\u0442\u044B\u0439 import
5156
5429
  elcrm audit
@@ -5210,6 +5483,9 @@ async function main() {
5210
5483
  case "kit":
5211
5484
  await cmdKit(parsed);
5212
5485
  break;
5486
+ case "publish":
5487
+ await cmdPublish(parsed);
5488
+ break;
5213
5489
  case "uuid":
5214
5490
  await cmdUuid();
5215
5491
  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 assignLetters(names, overrides) {
11
- const used = new Set;
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-z0-9A-Z]$/.test(letter)) {
15
- throw new Error(`[discover-lib] override "${name}" → "${letter}" (нужна a-z0-9A-Z)`);
53
+ if (!/^[a-z]$/.test(letter)) {
54
+ throw new Error(`[discover-lib] override "${name}" → "${letter}" (нужна одна буква a-z)`);
16
55
  }
17
- if (used.has(letter)) {
18
- throw new Error(`[discover-lib] дубль буквы "${letter}" в overrides`);
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
- used.add(letter);
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
- let letter = [...base].find((c) => /[a-z]/.test(c) && !used.has(c)) ?? [..."abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"].find((c) => !used.has(c));
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] нет свободной буквы/цифры для "${name}"`);
72
+ throw new Error(`[discover-lib] нет свободной буквы a-z для "${name}" в семье "${prefixOf(name)}" — заведи новую семью в prefixGroups`);
31
73
  }
32
- used.add(letter);
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 letters = assignLetters(names, options.letterOverrides ?? {});
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
- toIndex,
273
- replaceCssVarToken,
274
- namespaceSize,
275
- minifyCssVars,
276
- isVendorJsChunk,
277
- encodeName,
278
- collectCssVarNames,
272
+ allocateUnique,
279
273
  applyVarDictionary,
280
- allocateUnique
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 символа `{prefix}{буква}{класс}`.
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
- /** Первая буква пакета (c = components, b = button, …) */
19
+ /** Дефолтная семья (c = chrome/components, b = button, …) */
8
20
  prefix: string;
9
- /** Свой prefix у компонента, если слотов пакета не хватает */
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 css.matchAll(/(?:^|[,{\s>+~])\.([a-z0-9])\b/gi)) {
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
- assertPrefix(prefix, "prefix");
31
- const prefixOverrides = options.prefixOverrides ?? {};
32
- for (const [name, pfx] of Object.entries(prefixOverrides)) {
33
- assertPrefix(pfx, `prefixOverrides["${name}"]`);
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 prefixOverrides[component] ?? prefix;
82
+ return prefixMap[component] ?? prefix;
49
83
  }
50
84
  function generateScopedName(name, filename) {
51
- if (!/^[a-z0-9]$/.test(name)) {
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-z0-9A-Z]$/.test(letter)) {
60
- throw new Error(`[css-scoped] ключ компонента "${component}" должен быть a-z0-9A-Z: "${letter}"`);
91
+ if (!/^[a-z]$/.test(letter)) {
92
+ throw new Error(`[css-scoped] ключ компонента "${component}" должен быть a-z: "${letter}"`);
61
93
  }
62
- const scoped = `${prefixFor(component)}${letter}${name}`;
63
- if (scoped.length !== 3) {
64
- throw new Error(`[css-scoped] "${scoped}" ≠ 3 символа (${component}.${name})`);
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 unknownOverride = Object.keys(prefixOverrides).filter((n) => !mapped.has(n));
89
- if (unknownOverride.length) {
90
- throw new Error(`[css-scoped] prefixOverrides без компонента: ${unknownOverride.join(", ")}`);
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 long = [
110
- ...css.matchAll(/(?:^|[,{\s>+~])\.([a-z][a-z0-9]+)\b/gi)
111
- ].map((m) => m[1]);
112
- if (long.length) {
113
- const component = componentFromFilename(file);
114
- throw new Error(`[css-scoped] ${component}: класс длиннее 1 знака (${[...new Set(long)].join(", ")}). Сократи или вынеси в prefixOverrides + 1 знак.`);
115
- }
116
- for (const local of localClasses(css)) {
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 extra = Object.entries(prefixOverrides).map(([n, p]) => `${n}=${p}`).join(", ");
122
- console.log(`[css-scoped] OK: ${files.length} module.css, ${snap.size} классов (prefix=${prefix}${extra ? `; ${extra}` : ""})`);
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
- createCssScoped
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.28",
4
- "description": "CLI @elcrm/*: doctor --fix (порядок проекта), update, css, docs, cursor, kit, migrate",
3
+ "version": "1.1.35",
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>",
@@ -48,18 +48,6 @@
48
48
  "engines": {
49
49
  "node": ">=18"
50
50
  },
51
- "scripts": {
52
- "build": "bun build ./src/index.ts --outdir ./dist --target bun --format esm && bun build ./src/vite/plugin-css-scoped.ts --outfile ./dist/vite/plugin-css-scoped.js --target node --format esm --packages external && bun build ./src/vite/discover-lib.ts --outfile ./dist/vite/discover-lib.js --target node --format esm --packages external && bun build ./src/vite/concat-css.ts --outfile ./dist/vite/concat-css.js --target node --format esm --packages external && bun build ./src/vite/check-tokens.ts --outfile ./dist/vite/check-tokens.js --target node --format esm --packages external && bun build ./src/vite/postbuild.ts --outfile ./dist/vite/postbuild.js --target node --format esm --packages external && bun build ./src/vite/minify-css-vars.ts --outfile ./dist/vite/minify-css-vars.js --target node --format esm --packages external && node ./scripts/chmod-bin.mjs && node ./scripts/copy-vite-dts.mjs",
53
- "dev": "bun run ./src/index.ts",
54
- "elcrm": "bun run ./src/index.ts",
55
- "test": "bun test src/lib/migrate src/lib/tidy.test.ts src/lib/kit src/vite",
56
- "demo": "bun run scripts/dev-template.ts",
57
- "demo:panel": "bun run scripts/dev-template.ts panel",
58
- "demo:orbit": "bun run scripts/dev-template.ts orbit",
59
- "link:local": "bun run build && bun link",
60
- "prepublishOnly": "bun run test && bun run build",
61
- "publish:npm": "npm publish"
62
- },
63
51
  "keywords": [
64
52
  "elcrm",
65
53
  "cli",
@@ -74,6 +62,9 @@
74
62
  "bugs": {
75
63
  "url": "https://elgit.ru/lib/elCRM.cli/issues"
76
64
  },
65
+ "publishConfig": {
66
+ "access": "public"
67
+ },
77
68
  "peerDependencies": {
78
69
  "vite": ">=5.0.0"
79
70
  },
@@ -81,10 +72,5 @@
81
72
  "vite": {
82
73
  "optional": true
83
74
  }
84
- },
85
- "devDependencies": {
86
- "@types/bun": "^1.2.18",
87
- "typescript": "^5.8.2",
88
- "vite": "^5.4.2"
89
75
  }
90
76
  }
@@ -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 → Stack, --page-* / --item-* → --shell-*
73
+ elcrm migrate components # Block/Row→Stack; onSelectonValueChange; 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` + `prefixOverrides`, scoped = 3 знака), `discover-lib`, `concat-css`, `check-tokens`, `postbuild`.
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] # npm publish; lib: package.public.json → откат
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 {
@@ -19,52 +21,83 @@ import {
19
21
  Loading, EmptyState, RadioGroup, PricingGroup, PricingTable,
20
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** — левое меню. `variant="cards"` (карточки) или `"plain"` (плоский backoffice). `groups` + `value`. Подменю: у пункта `items` + `defaultOpen` (открыто/скрыто при старте). Контроль: `open` / `onOpenChange` (массив `value` родителей). Клик по родителю только раскрывает; `onSelect` — у листьев. Если выбран потомок — ветка открывается. Слоты: `brand`, `extra`, `footer`. Меню аккаунта: `footerItems` + `onFooterSelect`. Отступ вложенных: `--sidebar-indent`.
36
- - **NavSections** — сайдбар страницы + lazy-панель. Подменю как у Sidebar: `items` + `defaultOpen`, `open` / `onOpenChange`. Панель грузится по `value` листа (`resolveSection`). Клик по родителю только раскрывает.
37
- - **Stack** — ряд/колонка/сетка (`direction="row"|"column"|"grid"`). Не `Block` / `Row` / `Column`. Сетка: `auto-fit` + `--stack-grid-min`, либо `columns={4}` / `columns={["1fr", "100px", "auto"]}`. С `columns` ряды `stretch` (карточки не наезжают); в одну линию — `style={{ alignItems: "center" }}`. Не `height: 100%` у детей сетки.
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** — теплокарта дней (`data.days`). Данные грузит приложение; слоты `brandName` / `brandSub` / `unit` / `hint`. Токены `--healthmap-cell`, `--healthmap-gap`, `--healthmap-l0`…`l4`.
41
- - **PanelInfo** — выезд справа (шапка / body / footer). На узком экранеоверлей. Слоты: `children`, `footer`, `headerExtra`, `close`. Токены `--panel-info-width`, `--panel-info-width-min`, `--panel-info-overlay-max`, `--panel-info-backdrop`.
42
- - **ActionGroup** — ряд иконок в карточке / строке: `label` + `items` и/или `children`. Кнопки — `@elcrm/button`.
43
- - **Toolbar** — фильтры над списком. `size` (`s`/`m`/`l`, по умолчанию `s`) наследуют вложенные `*Field`, `Search`, `Button`, `Dropdown` (свой `size` важнее). `align`: `start` / `center` / `end` / `between`. `width`: `l` (100%, по умолчанию) или `s` (по контенту, не растягивать). Токены `--toolbar-gap`. Не `ActionGroup`.
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"` в приложении.
46
73
  - **Loading** — индикатор ожидания. Мин. высота: `--loading-min-height` (`min(50vh, 24rem)`).
47
74
  - **EmptyState** — заглушка / ошибка страницы. `--empty-max-width`, `--empty-padding`.
48
- - **Item** — строка списка. `as` button/a/li, `active`. `variant="card"` — рамка и muted-фон (идеи / тикеты / inbox). Не дублировать `.desk-row` в приложении.
75
+ - **Item** — строка списка. По умолчанию `<li>`; кликабельный — `as="button"` / `as="a"`. `active`. `variant="card"` — рамка и muted-фон. Не дублировать `.desk-row` в приложении.
76
+ - **Brand** — логотип в шапке. По умолчанию `<strong>`; кликабельный — `as="button"` / `as="a"`.
49
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`).
50
- - **Table** — `<table>` в обёртке со скроллом. `layout="fixed"` для фиксированной сетки. Токены `--table-*`.
78
+ - **Table** — `<table>` в обёртке со скроллом. `stickyHeader` — липкий thead. `layout="fixed"`. Подпись `<caption>`. Токены `--table-*`.
79
+ - **ChatFrame** — внутренний каркас ChatList/Messages; **не** в корневом экспорте → `@elcrm/components/ChatFrame` (`elcrm migrate components`).
51
80
  - **CodeBlock** — bar + `<pre>`, `onCopy`, `children` для подсветки. Токены `--code-block-*`.
52
- - **SplitPane** — две панели + drag handle. `direction`, `ratio`, `onRatioChange`, `minFirst` / `minSecond`. Не `ChatSplit` (только чат). Токены `--split-handle-*`.
81
+ - **SplitPane** — две панели + drag handle. `direction`, `ratio` / `defaultRatio`, `onRatioChange`, `minFirst` / `minSecond`. Не `ChatSplit` (только чат). Токены `--split-handle-*`.
53
82
  - **Banner** — статусная полоса (`tone`: info / warning / danger / success). Не toast — не `Notice`.
54
83
  - **IconButton** — `name` (`Icons.Line`) + обязательный `label` + `Tooltip` по умолчанию. `tooltip={false}` без подсказки. Низкий уровень — `@elcrm/button/IconButton` (`icon` + `label`).
55
84
 
56
85
  ## Меню
57
86
 
58
- - **Menu** — inline-навигация в шапке. `items`: `value` + `label` + `badge` (`0` не показываем, иначе `Badge` size s). `variant="line"` — вкладки с чертой снизу (не ломать pill селекторами страницы).
59
- - **Dropdown** — выпадающее меню по `items`/`groups` (portal). `placement="auto"|"top"|"bottom"` — куда открывать панель (футер Sidebar `top`). `size`высота триггера; в `Toolbar` берётся сам.
60
- - **Popover** — свой попап в portal (якорь + `children`). z-index: `acquireOverlayZIndex` (тот же стек, что у form). Не `@elcrm/overlay` `Menu`. `placement` как у Dropdown: позиция по **реальной** высоте панели, не по `maxHeight` 420.
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`.
61
92
 
62
93
  ## Avatar
63
94
 
64
95
  - **Avatar** — фото / инициалы (`name` только для инициалов). Скругление: `radius={10}` или `--avatar-radius` (дефолт `999px` — круг).
65
96
  - **Badge** — метка: `tone`, `size` (`s`/`m`/`l`), `dot` (точка слева). Не Button.
66
97
  - **AvatarName** — `title` + `description` (+ `name` для инициалов). Тот же `radius`. Цвета: `--avatar-name-title-color`, `--avatar-name-description-color`.
67
- - **AvatarGroup** — `items` с `value` / `title` / `name` / `src`.
98
+ - **AvatarGroup** — `items` с обязательным `value` + `title` / `name` / `src`; клик — `onValueChange(value)`.
99
+ - **Breadcrumb** — `items` + `onValueChange(value)`; `separator`, `collapseAfter`.
100
+ - **createLazyResolver** — кэш lazy-чанков; при ошибке загрузки слот сбрасывается; `.clear(segment?)` для retry.
68
101
 
69
102
  ## Pricing
70
103
 
@@ -82,12 +115,18 @@ import type { PricingPlan, HealthmapData } from "@elcrm/components";
82
115
  - `--popup-shadow` общий с `@elcrm/form`.
83
116
  - `elcrm css` — недостающие `--shell-*`.
84
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 в приложении.
85
119
 
86
120
  ## Нельзя
87
121
 
88
122
  - `import "@elcrm/…/themes.css"`.
89
- - `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`.
90
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`).
91
130
  - Нативные `<button>` / инпуты вместо `@elcrm/button` / `@elcrm/form` в приложении.
92
131
  - `size="sm"|"md"` → `"s"|"m"`.
93
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`: вложенные поля/Search/Button берут этот `size`.
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
  /* —— база приложения (без захардкоженных цветов) —— */