elcrm 1.1.17 → 1.1.19

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.
@@ -1,10 +1,13 @@
1
1
  /**
2
2
  * Vite helper: короткие unique className для CSS Modules elCRM.
3
+ * Формат: 3 символа `{prefix}{буква}{класс}`.
3
4
  * @see createCssScoped
4
5
  */
5
6
  export type CssScopedOptions = {
6
7
  /** Первая буква пакета (c = components, b = button, …) */
7
8
  prefix: string;
9
+ /** Свой prefix у компонента, если слотов пакета не хватает */
10
+ prefixOverrides?: Record<string, string>;
8
11
  /** Папка компонента → одна буква */
9
12
  components: Record<string, string>;
10
13
  /** Каталог с папками компонентов (абсолютный путь) */
@@ -15,15 +15,22 @@ function listModuleCss(dir) {
15
15
  }
16
16
  function localClasses(css) {
17
17
  const set = new Set;
18
- for (const m of css.matchAll(/(?:^|[,{\s>+~])\.([a-z][a-z0-9]*)\b/gi)) {
19
- set.add(m[1]);
18
+ for (const m of css.matchAll(/(?:^|[,{\s>+~])\.([a-z0-9])\b/gi)) {
19
+ set.add(m[1].toLowerCase());
20
20
  }
21
21
  return [...set];
22
22
  }
23
+ function assertPrefix(value, label) {
24
+ if (!/^[a-z]$/.test(value)) {
25
+ throw new Error(`[css-scoped] ${label} должен быть одной буквой a-z, получено: "${value}"`);
26
+ }
27
+ }
23
28
  function createCssScoped(options) {
24
29
  const prefix = options.prefix;
25
- if (!/^[a-z]$/.test(prefix)) {
26
- throw new Error(`[css-scoped] prefix должен быть одной буквой a-z, получено: "${prefix}"`);
30
+ assertPrefix(prefix, "prefix");
31
+ const prefixOverrides = options.prefixOverrides ?? {};
32
+ for (const [name, pfx] of Object.entries(prefixOverrides)) {
33
+ assertPrefix(pfx, `prefixOverrides["${name}"]`);
27
34
  }
28
35
  const components = options.components;
29
36
  const libDir = options.libDir;
@@ -37,9 +44,12 @@ function createCssScoped(options) {
37
44
  }
38
45
  return m[1];
39
46
  }
47
+ function prefixFor(component) {
48
+ return prefixOverrides[component] ?? prefix;
49
+ }
40
50
  function generateScopedName(name, filename) {
41
- if (!/^[a-z][a-z0-9]*$/.test(name)) {
42
- throw new Error(`[css-scoped] локальный класс должен быть [a-z][a-z0-9]*: ".${name}" (${filename})`);
51
+ if (!/^[a-z0-9]$/.test(name)) {
52
+ throw new Error(`[css-scoped] локальный класс 1 знак [a-z0-9] (scoped = 3 символа): ".${name}" (${filename})`);
43
53
  }
44
54
  const component = componentFromFilename(filename);
45
55
  const letter = components[component];
@@ -49,7 +59,10 @@ function createCssScoped(options) {
49
59
  if (!/^[a-z0-9A-Z]$/.test(letter)) {
50
60
  throw new Error(`[css-scoped] ключ компонента "${component}" должен быть a-z0-9A-Z: "${letter}"`);
51
61
  }
52
- const scoped = `${prefix}${letter}${name}`;
62
+ const scoped = `${prefixFor(component)}${letter}${name}`;
63
+ if (scoped.length !== 3) {
64
+ throw new Error(`[css-scoped] "${scoped}" ≠ 3 символа (${component}.${name})`);
65
+ }
53
66
  const owner = `${component}.${name}`;
54
67
  const prev = registry.get(scoped);
55
68
  if (prev && prev !== owner) {
@@ -72,6 +85,10 @@ function createCssScoped(options) {
72
85
  if (missingMap.length) {
73
86
  throw new Error(`[css-scoped] нет буквы в components: ${missingMap.join(", ")}`);
74
87
  }
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
+ }
75
92
  const letters = Object.values(components);
76
93
  const dupLetter = [
77
94
  ...new Set(letters.filter((l, i) => letters.indexOf(l) !== i))
@@ -79,15 +96,30 @@ function createCssScoped(options) {
79
96
  if (dupLetter.length) {
80
97
  throw new Error(`[css-scoped] дубли букв компонентов: ${dupLetter.join(", ")}`);
81
98
  }
99
+ const namespaces = Object.entries(components).map(([name, letter]) => `${prefixFor(name)}${letter}`);
100
+ const dupNs = [
101
+ ...new Set(namespaces.filter((n, i) => namespaces.indexOf(n) !== i))
102
+ ];
103
+ if (dupNs.length) {
104
+ throw new Error(`[css-scoped] дубли пространства {prefix}{буква}: ${dupNs.join(", ")}`);
105
+ }
82
106
  const files = listModuleCss(libDir);
83
107
  for (const file of files) {
84
108
  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
+ }
85
116
  for (const local of localClasses(css)) {
86
117
  generateScopedName(local, file);
87
118
  }
88
119
  }
89
120
  const snap = snapshot();
90
- console.log(`[css-scoped] OK: ${files.length} module.css, ${snap.size} классов (prefix=${prefix})`);
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}` : ""})`);
91
123
  return snap;
92
124
  }
93
125
  function plugin() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "elcrm",
3
- "version": "1.1.17",
3
+ "version": "1.1.19",
4
4
  "description": "CLI @elcrm/*: doctor --fix (порядок проекта), update, css, docs, cursor, migrate",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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`, `discover-lib`, `concat-css`, `check-tokens`, `postbuild`.
114
+ Также: `elcrm/vite/plugin-css-scoped` (`prefix` + `prefixOverrides`, scoped = 3 знака), `discover-lib`, `concat-css`, `check-tokens`, `postbuild`.
115
115
 
116
116
  ### Прочее
117
117
 
@@ -35,8 +35,8 @@ import type { PricingPlan, HealthmapData } from "@elcrm/components";
35
35
  - **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%` у детей сетки.
36
36
  - **Card** — курсор: `--card-pointer` (`auto` / `pointer`). Сетка Stack: `--stack-grid-min`.
37
37
  - **Stat** — метрика: `label` + `value`, `tone` как у Badge, `labelMuted`. Не собирать из `span` в приложении.
38
- - **Healthmap** — теплокарта дней (`data.days`). Данные грузит приложение; слоты `brandName` / `brandSub` / `unit` / `hint`. Токены `--healthmap-l0`…`l4`.
39
- - **PanelInfo** — выезд справа (шапка / body / footer). На узком экране — оверлей. Слоты: `children`, `footer`, `headerExtra`, `close`. Токены `--panel-info-*`.
38
+ - **Healthmap** — теплокарта дней (`data.days`). Данные грузит приложение; слоты `brandName` / `brandSub` / `unit` / `hint`. Токены `--healthmap-cell`, `--healthmap-gap`, `--healthmap-l0`…`l4`.
39
+ - **PanelInfo** — выезд справа (шапка / body / footer). На узком экране — оверлей. Слоты: `children`, `footer`, `headerExtra`, `close`. Токены `--panel-info-width`, `--panel-info-width-min`, `--panel-info-overlay-max`, `--panel-info-backdrop`.
40
40
  - **ActionGroup** — тулбар: `label` + `items` (узлы) и/или `children`. Кнопки — `@elcrm/button`, не нативный `<button>`.
41
41
  - **TextGroup** — две строки: `title` + `description` (без аватара; с аватаром — AvatarName).
42
42
  - **Item** — строка списка. `as` button/a/li, `active`. `variant="card"` — рамка и muted-фон (идеи / тикеты / inbox). Не дублировать `.desk-row` в приложении.
@@ -153,6 +153,17 @@
153
153
  --chat-messages-header-padding: 14px 16px;
154
154
  --chat-messages-body-padding: 16px 16px 20px;
155
155
  --chat-messages-footer-padding: 10px 12px 12px;
156
+ --panel-info-width: min(22rem, 42vw);
157
+ --panel-info-width-min: 16rem;
158
+ --panel-info-overlay-max: 24rem;
159
+ --panel-info-backdrop: color-mix(in srgb, var(--shell-color) 35%, transparent);
160
+ --healthmap-cell: 12px;
161
+ --healthmap-gap: 3px;
162
+ --healthmap-l0: var(--shell-background-muted);
163
+ --healthmap-l1: color-mix(in srgb, var(--shell-color-success) 25%, var(--shell-background-muted));
164
+ --healthmap-l2: color-mix(in srgb, var(--shell-color-success) 45%, transparent);
165
+ --healthmap-l3: color-mix(in srgb, var(--shell-color-success) 70%, transparent);
166
+ --healthmap-l4: var(--shell-color-success);
156
167
  }
157
168
 
158
169
  /* —— база приложения (без захардкоженных цветов) —— */
@@ -164,6 +164,17 @@
164
164
  --chat-messages-header-padding: 14px 16px;
165
165
  --chat-messages-body-padding: 16px 16px 20px;
166
166
  --chat-messages-footer-padding: 10px 12px 12px;
167
+ --panel-info-width: min(22rem, 42vw);
168
+ --panel-info-width-min: 16rem;
169
+ --panel-info-overlay-max: 24rem;
170
+ --panel-info-backdrop: color-mix(in srgb, var(--shell-color) 35%, transparent);
171
+ --healthmap-cell: 12px;
172
+ --healthmap-gap: 3px;
173
+ --healthmap-l0: var(--shell-background-muted);
174
+ --healthmap-l1: color-mix(in srgb, var(--shell-color-success) 25%, var(--shell-background-muted));
175
+ --healthmap-l2: color-mix(in srgb, var(--shell-color-success) 45%, transparent);
176
+ --healthmap-l3: color-mix(in srgb, var(--shell-color-success) 70%, transparent);
177
+ --healthmap-l4: var(--shell-color-success);
167
178
  }
168
179
 
169
180
  /* —— база приложения (без захардкоженных цветов) —— */