elcrm 1.1.2 → 1.1.3
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 +548 -33
- package/dist/vite/concat-css.js +13 -1
- package/dist/vite/postbuild.js +13 -1
- package/package.json +4 -3
- package/templates/elcrm-cursor/AGENTS.md +3 -1
- package/templates/elcrm-cursor/rules/elcrm-jsdoc.mdc +89 -0
- package/templates/elcrm-cursor/rules/elcrm-lib.mdc +1 -0
- package/templates/elcrm-cursor/rules/elcrm-packages.mdc +2 -2
- package/templates/elcrm-cursor/rules/elcrm-theme.mdc +1 -0
- package/templates/elcrm-cursor/rules/elcrm-tokens.mdc +40 -0
- package/templates/elcrm-cursor/rules/elcrm-ui.mdc +1 -0
- package/templates/elcrm-cursor/rules/elcrm.mdc +1 -1
- package/templates/elcrm-docs/CLI.elCRM.md +4 -3
- package/templates/elcrm-docs/COMPONENTS.elCRM.md +51 -46
- package/templates/panel/web/src/App.tsx +15 -1
- package/templates/panel/web/src/component/Header.tsx +19 -6
- package/templates/panel/web/src/lazyRoute.ts +6 -0
- package/templates/panel/web/src/lib/routeKey.ts +8 -1
- package/templates/panel/web/src/lib/theme.ts +20 -3
- package/templates/panel/web/src/modules/Account/index.tsx +12 -8
- package/templates/panel/web/src/modules/Account/router.sections.ts +5 -3
- package/templates/panel/web/src/modules/Account/sections/Profile.tsx +9 -4
- package/templates/panel/web/src/modules/Account/sections/Sessions.tsx +1 -1
- package/templates/panel/web/src/modules/Home/index.tsx +11 -4
- package/templates/panel/web/src/modules/Home/modal/Demo.tsx +10 -0
- package/templates/panel/web/src/modules/List/index.tsx +12 -11
- package/templates/panel/web/src/modules/List/router.sections.ts +6 -4
- package/templates/panel/web/src/modules/List/sections/Basic.tsx +3 -3
- package/templates/panel/web/src/modules/List/sections/Horizontal.tsx +1 -1
- package/templates/panel/web/src/modules/List/sections/Rows.tsx +6 -3
- package/templates/panel/web/src/modules/Settings/index.tsx +14 -8
- package/templates/panel/web/src/modules/Settings/router.sections.ts +7 -4
- package/templates/panel/web/src/modules/Settings/sections/Appearance.tsx +8 -4
- package/templates/panel/web/src/modules/Settings/sections/General.tsx +5 -1
- package/templates/panel/web/src/modules/Settings/sections/Profile.tsx +5 -1
- package/templates/panel/web/src/pages/Auth.tsx +10 -0
- package/templates/panel/web/src/pages/Main.tsx +7 -1
- package/templates/panel/web/src/state.ts +9 -0
- package/templates/panel/web/src/style/theme-dark.css +18 -87
- package/templates/panel/web/src/style/theme-light.css +18 -87
- package/templates/panel/web/src/style/theme.css +29 -78
package/dist/vite/concat-css.js
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
// src/vite/concat-css.ts
|
|
2
2
|
import { readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
var SKIP_CSS = new Set([
|
|
5
|
+
"index.css",
|
|
6
|
+
"tokens.css",
|
|
7
|
+
"light.css",
|
|
8
|
+
"dark.css",
|
|
9
|
+
"themes.css"
|
|
10
|
+
]);
|
|
4
11
|
function concatDistCss(options) {
|
|
5
12
|
const outFile = options.outFile ?? "index.css";
|
|
6
|
-
const
|
|
13
|
+
const skip = new Set([
|
|
14
|
+
...SKIP_CSS,
|
|
15
|
+
outFile,
|
|
16
|
+
...options.skip ?? []
|
|
17
|
+
]);
|
|
18
|
+
const files = readdirSync(options.distDir).filter((f) => f.endsWith(".css") && !skip.has(f)).sort();
|
|
7
19
|
const css = files.map((f) => readFileSync(join(options.distDir, f), "utf8")).join(`
|
|
8
20
|
`);
|
|
9
21
|
const outPath = join(options.distDir, outFile);
|
package/dist/vite/postbuild.js
CHANGED
|
@@ -12,9 +12,21 @@ import { join as join2 } from "node:path";
|
|
|
12
12
|
// src/vite/concat-css.ts
|
|
13
13
|
import { readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
14
14
|
import { join } from "node:path";
|
|
15
|
+
var SKIP_CSS = new Set([
|
|
16
|
+
"index.css",
|
|
17
|
+
"tokens.css",
|
|
18
|
+
"light.css",
|
|
19
|
+
"dark.css",
|
|
20
|
+
"themes.css"
|
|
21
|
+
]);
|
|
15
22
|
function concatDistCss(options) {
|
|
16
23
|
const outFile = options.outFile ?? "index.css";
|
|
17
|
-
const
|
|
24
|
+
const skip = new Set([
|
|
25
|
+
...SKIP_CSS,
|
|
26
|
+
outFile,
|
|
27
|
+
...options.skip ?? []
|
|
28
|
+
]);
|
|
29
|
+
const files = readdirSync(options.distDir).filter((f) => f.endsWith(".css") && !skip.has(f)).sort();
|
|
18
30
|
const css = files.map((f) => readFileSync(join(options.distDir, f), "utf8")).join(`
|
|
19
31
|
`);
|
|
20
32
|
const outPath = join(options.distDir, outFile);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "elcrm",
|
|
3
|
-
"version": "1.1.
|
|
4
|
-
"description": "CLI @elcrm/*: update, create, migrate front, audit, doctor, test, build, css",
|
|
3
|
+
"version": "1.1.3",
|
|
4
|
+
"description": "CLI @elcrm/*: update, create, migrate front/components, audit, doctor, test, build, css",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "MaSkal <dev@elcrm.online>",
|
|
@@ -47,8 +47,9 @@
|
|
|
47
47
|
"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 && node ./scripts/chmod-bin.mjs && node ./scripts/copy-vite-dts.mjs",
|
|
48
48
|
"dev": "bun run ./src/index.ts",
|
|
49
49
|
"elcrm": "bun run ./src/index.ts",
|
|
50
|
+
"test": "bun test src/lib/migrate",
|
|
50
51
|
"link:local": "bun run build && bun link",
|
|
51
|
-
"prepublishOnly": "bun run build",
|
|
52
|
+
"prepublishOnly": "bun run test && bun run build",
|
|
52
53
|
"publish:npm": "npm publish"
|
|
53
54
|
},
|
|
54
55
|
"keywords": [
|
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
Корень UI: `Alert.Init` / `Modal.Init` / `Notice.Init` / `Form.Init` + `LoadRoute`. Кнопки `@elcrm/button`, поля `@elcrm/form`, модалки `Modal.Create`.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Свой компонент: JSDoc на русском (зачем, props, слоты, `@example`). Без него не сдавать.
|
|
10
|
+
|
|
11
|
+
Нельзя: native input/button, react-router, axios, MUI/antd/tailwind, `themes.css`, `size="sm"|"md"`. Свой CSS — `var(--button-*)` / `--field-*` / `--item-*` из theme, не `#hex`.
|
|
10
12
|
|
|
11
13
|
Перед сдачей: `elcrm test` (в panel — из корня). После смены пакетов: `elcrm update --fix --test`.
|
|
12
14
|
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: JSDoc у каждого компонента — props, слоты, пример
|
|
3
|
+
globs: "**/*.{tsx,ts,jsx,js}"
|
|
4
|
+
alwaysApply: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# JSDoc компонентов (обязательно)
|
|
8
|
+
|
|
9
|
+
У **каждого** React-компонента и публичной функции — блок JSDoc на русском **до** объявления. Без него компонент не считают законченным. Цель: по ховеру в IDE видно props, слоты, что рендерится.
|
|
10
|
+
|
|
11
|
+
Не дублировать шпаргалку пакета (`docs/BUTTON.elCRM.md`) целиком — кратко + ссылка, если это обёртка над `@elcrm/*`.
|
|
12
|
+
|
|
13
|
+
## Шаблон компонента
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
/**
|
|
17
|
+
* Шапка приложения: слоты brand / nav / search / actions.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* <Header />
|
|
21
|
+
*
|
|
22
|
+
* Слоты (что можно передать):
|
|
23
|
+
* - brand — логотип (`Brand`)
|
|
24
|
+
* - nav — пункты (`Menu`)
|
|
25
|
+
* - search — `@elcrm/search`
|
|
26
|
+
* - actions — кнопки, `Avatar`, `Tooltip`
|
|
27
|
+
*
|
|
28
|
+
* @see Header из `@elcrm/components` — те же слоты
|
|
29
|
+
*/
|
|
30
|
+
export default function Header() { … }
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Шаблон с props
|
|
34
|
+
|
|
35
|
+
Тип props — отдельный `type` / `interface`, каждое поле с `@param` **или** JSDoc на поле типа:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
type Props = {
|
|
39
|
+
/** Логин для текста в теле модалки */
|
|
40
|
+
login?: string;
|
|
41
|
+
/** Закрытие; `Modal.Create` прокидывает сам */
|
|
42
|
+
onClose?: () => void;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Демо-модалка модуля Home.
|
|
47
|
+
*
|
|
48
|
+
* @param props.login - отображаемое имя
|
|
49
|
+
* @returns очередь `Modal.Create` → `{ Open, Icon }`
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* Open({ login: "admin" })
|
|
53
|
+
*/
|
|
54
|
+
function Screen({ login }: Props) { … }
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Что обязательно описать
|
|
58
|
+
|
|
59
|
+
| Тема | Как писать |
|
|
60
|
+
| --- | --- |
|
|
61
|
+
| Зачем компонент | первая строка |
|
|
62
|
+
| Props | имя, тип в коде, смысл, default если не очевиден |
|
|
63
|
+
| Слоты / children | что класть: `Button`, `StringField`, не «react node» |
|
|
64
|
+
| Размер | `"s" \| "m" \| "l"` |
|
|
65
|
+
| Роутер | какой ключ (`module`, `section`, `listTab`) |
|
|
66
|
+
| Связь с пакетом | `@elcrm/form` поле, `@elcrm/button` `onSend` |
|
|
67
|
+
| Токены | если кастомный CSS: какие `--*` |
|
|
68
|
+
|
|
69
|
+
## Функции / хуки
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
/**
|
|
73
|
+
* Берёт ключ секции из роутера, только если он есть в списке модуля.
|
|
74
|
+
*
|
|
75
|
+
* @param value - `Router.useRouter("section")`
|
|
76
|
+
* @param keys - допустимые value секций
|
|
77
|
+
* @param fallback - секция по умолчанию
|
|
78
|
+
*/
|
|
79
|
+
export function pickRouteKey(value: unknown, keys: readonly string[], fallback: string): string
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Нельзя
|
|
83
|
+
|
|
84
|
+
- Пустой `/** Header */` без props/слотов
|
|
85
|
+
- JSDoc на английском в app (код и доки — русские)
|
|
86
|
+
- Описывать внутренности пакета вместо его шпаргалки — для `@elcrm/button` достаточно «см. docs/BUTTON.elCRM.md», для **своей** обёртки — полный разбор слотов
|
|
87
|
+
- Новый компонент в `src/` без JSDoc (в т.ч. `sections/*.tsx`, `modal/*.tsx`)
|
|
88
|
+
|
|
89
|
+
При правке существующего компонента без JSDoc — **добавить блок сразу**, не ждать отдельной задачи.
|
|
@@ -59,6 +59,7 @@ alwaysApply: true
|
|
|
59
59
|
|
|
60
60
|
### 1. Публичный API (компонент, хук, функция, тип, props)
|
|
61
61
|
|
|
62
|
+
- [ ] JSDoc на русском у каждого экспорта: props, слоты, `@example`
|
|
62
63
|
- [ ] Экспорт в `index` и `package.json` `exports`
|
|
63
64
|
- [ ] `README.md` этого пакета
|
|
64
65
|
- [ ] **Шпаргалка в elCRM.cli** (таблица выше) — те же можно/нельзя, что в README
|
|
@@ -13,7 +13,7 @@ alwaysApply: true
|
|
|
13
13
|
|
|
14
14
|
| Файл | Пакет | Делать так |
|
|
15
15
|
| ------------------------ | ---------------------- | ------------------------------------------------------- |
|
|
16
|
-
| `CLI.elCRM.md` | `elcrm` | `update --fix --test`,
|
|
16
|
+
| `CLI.elCRM.md` | `elcrm` | `upd` (сам CLI), `update --fix --test`, docs, cursor |
|
|
17
17
|
| `API.elCRM.md` | `@elcrm/api` | `Api.create` + `Api.query("router/method", body)` |
|
|
18
18
|
| `ALERT.elCRM.md` | `@elcrm/alert` | `<Alert.Init />`, `Alert.Send` / `Confirm` |
|
|
19
19
|
| `BUTTON.elCRM.md` | `@elcrm/button` | `<Button onSend>`, size s/m/l, не `<button>` |
|
|
@@ -26,7 +26,7 @@ alwaysApply: true
|
|
|
26
26
|
| `TOOLTIP.elCRM.md` | `@elcrm/tooltip` | `<Tooltip content>`, не `title` |
|
|
27
27
|
| `ICONS.elCRM.md` | `@elcrm/icons` | `Icons.Line`, vite `icons()`, `icons update` |
|
|
28
28
|
| `FORM.elCRM.md` | `@elcrm/form` | поля + useForm, не native input |
|
|
29
|
-
| `COMPONENTS.elCRM.md` | `@elcrm/components` | Layout/Header/Section/NavSections
|
|
29
|
+
| `COMPONENTS.elCRM.md` | `@elcrm/components` | Layout/Header/Section/Stack/NavSections; migrate components |
|
|
30
30
|
| `SERVER.elCRM.md` | `@elcrm/server` | createServer + createRouter, не REST, не node:fs |
|
|
31
31
|
|
|
32
32
|
## Корень приложения (panel)
|
|
@@ -11,3 +11,4 @@ alwaysApply: false
|
|
|
11
11
|
- Не класть `--*` в `modules/**/style.css`, если это цвет/размер оболочки.
|
|
12
12
|
- Не `import "@elcrm/*/themes.css"`. Не shorthand `1px solid` в `--field-border-hover`.
|
|
13
13
|
- Недостающие дефолты: `elcrm css` (уже заданные не затирает).
|
|
14
|
+
- Кастомный UI — `var(--layout-*)` / `--button-*` / `--field-*` / `--item-*`, не новые цвета в модуле.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Кастомный UI — сначала токены @elcrm/*, не свои цвета и px
|
|
3
|
+
globs: "**/*.{tsx,jsx,css}"
|
|
4
|
+
alwaysApply: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Кастомные элементы → токены библиотек
|
|
8
|
+
|
|
9
|
+
Свой блок/обёртка/CSS модуля — **не** новый `#hex` и не магические `14px`, если уже есть переменная пакета. Смотри `src/style/theme.css` + light/dark (их заполняет `elcrm css`).
|
|
10
|
+
|
|
11
|
+
## Сначала существующий токен
|
|
12
|
+
|
|
13
|
+
| Нужно | Брать из |
|
|
14
|
+
| --- | --- |
|
|
15
|
+
| Фон/текст/бордер страницы | `--layout-*`, `--section-*`, `--card-*` |
|
|
16
|
+
| Шапка / подвал | `--header-*`, `--footer-*` |
|
|
17
|
+
| Список / строка | `--list-*`, `--item-*` |
|
|
18
|
+
| Кнопка | `--button-*` (variant + hover/active/disabled) |
|
|
19
|
+
| Поле ввода | `--field-*`, `--popup-shadow` |
|
|
20
|
+
| Меню / вкладки | `--menu-*`, `--nav-sections-*`, `--tab-sections-*` |
|
|
21
|
+
| Тост / алерт / тултип | `--notice-*`, `--alert-*`, `--tooltip-*` |
|
|
22
|
+
| Отступ стека | `--block-gap`, `--row-gap`, `--column-gap` |
|
|
23
|
+
| Размер контрола | `s` / `m` / `l` у компонента, не свои px |
|
|
24
|
+
|
|
25
|
+
В CSS: `color: var(--item-color)`, `padding: var(--card-padding)`, `border-radius: var(--card-radius)`.
|
|
26
|
+
|
|
27
|
+
В inline-style допустимо только `var(--…)`, не литерал цвета.
|
|
28
|
+
|
|
29
|
+
## Если токена нет
|
|
30
|
+
|
|
31
|
+
1. Похожий из той же группы (`--item-background-hover` вместо нового `--my-hover`).
|
|
32
|
+
2. Редко — объявить `--*` в theme (цвет в **light и dark**, геометрия в `theme.css`) и использовать `var()`.
|
|
33
|
+
3. Не копировать значения из `node_modules/@elcrm/*/dist` в модуль.
|
|
34
|
+
|
|
35
|
+
## Нельзя
|
|
36
|
+
|
|
37
|
+
- Свой `style={{ color: "#333" }}` / `background: white`
|
|
38
|
+
- Дублировать `--button-primary-bg` под другим именем
|
|
39
|
+
- `themes.css` из пакета
|
|
40
|
+
- shorthand `1px solid …` в `--field-border-hover|focus|error|disabled`
|
|
@@ -37,5 +37,6 @@ src/style/theme*.css + elcrm.css
|
|
|
37
37
|
|
|
38
38
|
## CSS / запреты
|
|
39
39
|
|
|
40
|
+
- Кастомный блок: `var(--токен)` из theme / `@elcrm/*` — правило `elcrm-tokens`. Не свой `#hex` и не случайные px.
|
|
40
41
|
- Токены в theme, не `themes.css`, не `size="sm"|"md"`, не native input/select/textarea.
|
|
41
42
|
- Border hover/focus полей — только цвет.
|
|
@@ -7,7 +7,7 @@ alwaysApply: true
|
|
|
7
7
|
|
|
8
8
|
**Всегда** сверяйся со шпаргалками: `docs/*.elCRM.md` и `.cursor/docs/*.elCRM.md` (каталог пакетов — правило `elcrm-packages`). Не выдумывай методы. Обновить: `elcrm docs && elcrm cursor`.
|
|
9
9
|
|
|
10
|
-
Комментарии и документация — **на русском**. Минимум зависимостей. Компоненты разделяй по обязанностям.
|
|
10
|
+
Комментарии и документация — **на русском**. У каждого компонента — JSDoc (props, слоты, пример): правило `elcrm-jsdoc`. Минимум зависимостей. Компоненты разделяй по обязанностям.
|
|
11
11
|
|
|
12
12
|
## Стек
|
|
13
13
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# `elcrm` CLI
|
|
4
4
|
|
|
5
|
-
Глобально: `bun i -g elcrm
|
|
5
|
+
Глобально: `bun i -g elcrm` или `elcrm upd` (latest). В приложении — после смены `@elcrm/*`:
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
elcrm update --fix --test
|
|
@@ -13,9 +13,10 @@ elcrm cursor
|
|
|
13
13
|
|
|
14
14
|
| Команда | Зачем |
|
|
15
15
|
| --- | --- |
|
|
16
|
-
| `
|
|
16
|
+
| `upd` | сам CLI → latest (`bun i -g elcrm@latest`) |
|
|
17
|
+
| `update [--dry] [--core] [--fix] [--test]` | latest `@elcrm/*` в проекте; `--fix` = migrate front + css; `--test` = elcrm test |
|
|
17
18
|
| `create` / `init` | шаблоны `app` \| `lib` \| `panel` |
|
|
18
|
-
| `migrate <name> [--dry]` | `front` (пакет), `form-aliases`, `form-tokens`, `size-sml`, `field-border`, `modal-create`, `legacy-hacks`, `socket-server` |
|
|
19
|
+
| `migrate <name> [--dry]` | `front` (пакет), `form-aliases`, `form-tokens`, `components`, `size-sml`, `field-border`, `modal-create`, `legacy-hacks`, `socket-server` |
|
|
19
20
|
| `audit [--fix]` | старые API; `--fix` = migrate front |
|
|
20
21
|
| `doctor [--fix]` | стандарты; сервер --fix → `bunx elcrm-server update` |
|
|
21
22
|
| `test [--front\|--server] [--no-scripts]` | статический анализ + scripts |
|
|
@@ -2,67 +2,72 @@
|
|
|
2
2
|
|
|
3
3
|
# `@elcrm/components`
|
|
4
4
|
|
|
5
|
-
Shell UI:
|
|
5
|
+
Shell UI: оболочка, шапка, секции, списки, Dropdown / RadioGroup / Pricing* / Avatar* / Breadcrumb / Stack.
|
|
6
|
+
**Цвета и размеры задаёт приложение** (`theme.css` + `theme-light` / `theme-dark`). Fallback в пакете нет.
|
|
7
|
+
|
|
8
|
+
Токены как у form: сначала общие **`--shell-*`** (+ `--control-*`, `--popup-shadow`), потом узкие (`--header-height`, `--section-padding`, `--avatar-size-*`, …).
|
|
9
|
+
Дефолты подтягивает `elcrm css` из `@elcrm/components/tokens.css` + `light.css` / `dark.css`.
|
|
10
|
+
|
|
11
|
+
Группы — через `items` / `groups`, идентификатор пункта — **`value`**.
|
|
6
12
|
|
|
7
13
|
```ts
|
|
8
14
|
import {
|
|
9
|
-
Layout, Header, Footer, Brand, Avatar, Badge,
|
|
10
|
-
Menu, Section, NavSections, TabSections,
|
|
11
|
-
Card, List, Item,
|
|
12
|
-
Loading, EmptyState,
|
|
15
|
+
Layout, Header, Footer, Brand, Avatar, AvatarName, AvatarGroup, Badge,
|
|
16
|
+
Menu, Dropdown, Section, NavSections, TabSections, Breadcrumb, Stack,
|
|
17
|
+
Card, List, Item, PageHead, PageShell,
|
|
18
|
+
Loading, EmptyState, RadioGroup, PricingGroup, PricingTable,
|
|
19
|
+
createLazyResolver,
|
|
13
20
|
} from "@elcrm/components";
|
|
21
|
+
import type { PricingPlan } from "@elcrm/components";
|
|
14
22
|
```
|
|
15
23
|
|
|
16
|
-
|
|
24
|
+
## Роли layout
|
|
17
25
|
|
|
18
|
-
|
|
26
|
+
- **PageShell** — каркас модуля (скролл + footer).
|
|
27
|
+
- **PageHead** — title + actions.
|
|
28
|
+
- **Section** — блок контента / auth.
|
|
29
|
+
- **Layout / Header** — оболочка приложения.
|
|
30
|
+
- **Stack** — ряд/колонка (`direction="row"|"column"`). Не `Block` / `Row` / `Column`.
|
|
19
31
|
|
|
20
|
-
|
|
21
|
-
| --- | --- | --- |
|
|
22
|
-
| Layout | страница | `header`, `footer`, `children` |
|
|
23
|
-
| Header | слоты шапки | `brand`, `nav`, `search`, `actions` |
|
|
24
|
-
| Footer | подвал | `children` |
|
|
25
|
-
| Brand | логотип | `as`, `onClick` |
|
|
26
|
-
| Avatar | фото / инициалы | `src`, `name`, `size` (`s`/`m`/`l`) |
|
|
27
|
-
| Badge | статус | `tone` |
|
|
28
|
-
| Menu | нав (не overlay) | `items`, `activeKey`, `onSelect` |
|
|
29
|
-
| Section | блок / auth | `title`, `description`, `icon`, `actions`, `variant` |
|
|
30
|
-
| NavSections | сайдбар + lazy | `groups`, `resolveSection` |
|
|
31
|
-
| TabSections | вкладки + lazy | `items`, `resolveSection` |
|
|
32
|
-
| Card / List / Item | карточка, список | Item: `as`, `active` |
|
|
33
|
-
| Block / Row / Column | стеки | `gap` |
|
|
34
|
-
| Loading / EmptyState | плейсхолдеры | |
|
|
35
|
-
| createLazyResolver | кэш `import()` | `(key) => import(…)` |
|
|
32
|
+
## Меню
|
|
36
33
|
|
|
37
|
-
|
|
34
|
+
- **Menu** — inline-навигация в шапке.
|
|
35
|
+
- **Dropdown** — выпадающее меню по `items`/`groups`.
|
|
36
|
+
- **`@elcrm/overlay`** — portal / позиционирование / z-stack для своих попапов.
|
|
38
37
|
|
|
39
|
-
##
|
|
38
|
+
## Avatar
|
|
40
39
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
index.tsx
|
|
47
|
-
router.sections.ts
|
|
48
|
-
sections/<Key>.tsx # export default
|
|
49
|
-
modal/ # опционально
|
|
50
|
-
component/ # локальные обёртки (Header приложения)
|
|
51
|
-
style/theme.css theme-light.css theme-dark.css elcrm.css
|
|
52
|
-
```
|
|
40
|
+
- **Avatar** — фото / инициалы (`name` только для инициалов).
|
|
41
|
+
- **AvatarName** — `title` + `description` (+ `name` для инициалов).
|
|
42
|
+
- **AvatarGroup** — `items` с `value` / `title` / `name` / `src`.
|
|
43
|
+
|
|
44
|
+
## Pricing
|
|
53
45
|
|
|
54
|
-
|
|
55
|
-
- Ключи роутера не смешивать (`section` vs `listTab` vs `accountTab`) — иначе lazy откроет не ту секцию.
|
|
56
|
-
- Auth-экран: `<Section variant="auth">`, поля из `@elcrm/form`, submit через `@elcrm/button` `onSend`.
|
|
46
|
+
Один тип `PricingPlan` и один массив `plans` → и `PricingGroup`, и `PricingTable` (+ `sections` у Table).
|
|
57
47
|
|
|
58
48
|
## Можно
|
|
59
49
|
|
|
60
|
-
|
|
50
|
+
- Задать `--shell-color` / `--shell-padding` / `--shell-radius` один раз — большинство компонентов подхватят.
|
|
51
|
+
- `--popup-shadow` общий с `@elcrm/form`.
|
|
52
|
+
- `elcrm css` — недостающие `--shell-*`.
|
|
53
|
+
- Импорт `@elcrm/components/style.css` — опционально.
|
|
61
54
|
|
|
62
55
|
## Нельзя
|
|
63
56
|
|
|
64
|
-
- `import "@elcrm/…/themes.css"
|
|
65
|
-
-
|
|
66
|
-
- `
|
|
67
|
-
-
|
|
68
|
-
-
|
|
57
|
+
- `import "@elcrm/…/themes.css"`.
|
|
58
|
+
- `Block` / `Row` / `Column` — `Stack` (`elcrm migrate components`).
|
|
59
|
+
- `key` / `activeKey` / `defaultActiveKey` в группах — `value` / `defaultValue` (`elcrm migrate components`).
|
|
60
|
+
- Нативные `<button>` / инпуты вместо `@elcrm/button` / `@elcrm/form` в приложении.
|
|
61
|
+
- `size="sm"|"md"` → `"s"|"m"`.
|
|
62
|
+
- Хардкод цветов в модулях приложения, если есть токен.
|
|
63
|
+
|
|
64
|
+
## Обновление проекта
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
elcrm update --fix --test
|
|
68
|
+
elcrm css
|
|
69
|
+
elcrm docs
|
|
70
|
+
elcrm cursor
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Пилюля: `elcrm migrate components` (также внутри `migrate front` / `update --fix`).
|
|
@@ -6,11 +6,25 @@ import { Form } from "@elcrm/form";
|
|
|
6
6
|
import { LoadRoute } from "@elcrm/router";
|
|
7
7
|
import { createLazyResolver } from "./lazyRoute";
|
|
8
8
|
|
|
9
|
-
/** Страницы: src/pages/Auth.tsx, Main.tsx */
|
|
10
9
|
const resolvePage = createLazyResolver((segment) =>
|
|
11
10
|
import(`./pages/${segment}.tsx`),
|
|
12
11
|
);
|
|
13
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Корень UI: глобальные Init пакетов + страница (`LoadRoute` variant=`page`).
|
|
15
|
+
*
|
|
16
|
+
* Init (по одному):
|
|
17
|
+
* - `Alert.Init` — диалоги
|
|
18
|
+
* - `Modal.Init` — очередь модалок (`Modal.Create` / Open)
|
|
19
|
+
* - `Notice.Init` — тосты (`position`, `durationMs`, `maxVisible`)
|
|
20
|
+
* - `Notifications.Init` — inbox (без autoSync в демо)
|
|
21
|
+
* - `Form.Init` — `onNotice` → `Notice.Remark`
|
|
22
|
+
*
|
|
23
|
+
* Страницы: `src/pages/{Auth|Main}.tsx`, default `Main`.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* <App />
|
|
27
|
+
*/
|
|
14
28
|
export default function App() {
|
|
15
29
|
return (
|
|
16
30
|
<>
|
|
@@ -13,7 +13,20 @@ import { Search } from "@elcrm/search";
|
|
|
13
13
|
import useStateData from "../state";
|
|
14
14
|
import { getTheme, toggleTheme, type ThemeMode } from "../lib/theme";
|
|
15
15
|
|
|
16
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* Шапка panel: слоты `@elcrm/components` `Header`.
|
|
18
|
+
*
|
|
19
|
+
* Слоты:
|
|
20
|
+
* - `brand` — `Brand`, клик → модуль `Home`
|
|
21
|
+
* - `nav` — `Menu` (`value` = `Router.module`: Home | List | Account | Settings)
|
|
22
|
+
* - `search` — `@elcrm/search`
|
|
23
|
+
* - `actions` — тема (`Button` ghost), `Avatar` → Account, выход (`Alert.Send`)
|
|
24
|
+
*
|
|
25
|
+
* Роутер: ключ `module`. Тема: `lib/theme` + localStorage.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* <Layout header={<Header />} />
|
|
29
|
+
*/
|
|
17
30
|
export default function Header() {
|
|
18
31
|
const { user } = useStateData(["user"]);
|
|
19
32
|
const module = Router.useRouter("module");
|
|
@@ -48,13 +61,13 @@ export default function Header() {
|
|
|
48
61
|
}
|
|
49
62
|
nav={
|
|
50
63
|
<Menu
|
|
51
|
-
|
|
64
|
+
value={String(module || "Home")}
|
|
52
65
|
onSelect={onTab}
|
|
53
66
|
items={[
|
|
54
|
-
{
|
|
55
|
-
{
|
|
56
|
-
{
|
|
57
|
-
{
|
|
67
|
+
{ value: "Home", label: "Главная" },
|
|
68
|
+
{ value: "List", label: "Список" },
|
|
69
|
+
{ value: "Account", label: "Аккаунт" },
|
|
70
|
+
{ value: "Settings", label: "Настройки" },
|
|
58
71
|
]}
|
|
59
72
|
/>
|
|
60
73
|
}
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Ключ вкладки/секции из роутера, только если он есть в списке модуля.
|
|
3
|
+
* Иначе после смены модуля откроется чужой lazy-чанк.
|
|
4
|
+
*
|
|
5
|
+
* @param value - `Router.useRouter("section" | "listTab" | "accountTab")`
|
|
6
|
+
* @param keys - допустимые `value` из router.sections
|
|
7
|
+
* @param fallback - секция по умолчанию
|
|
8
|
+
*/
|
|
2
9
|
export function pickRouteKey(
|
|
3
10
|
value: unknown,
|
|
4
11
|
keys: readonly string[],
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Тема panel: `data-theme` на html/body + localStorage `elcrm-panel-theme`.
|
|
3
|
+
* Цвета — `theme-light.css` / `theme-dark.css`.
|
|
4
|
+
*/
|
|
3
5
|
export type ThemeMode = "dark" | "light";
|
|
4
6
|
|
|
5
7
|
const STORAGE_KEY = "elcrm-panel-theme";
|
|
6
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Текущая тема (`dark`, если ключа нет).
|
|
11
|
+
*/
|
|
7
12
|
export function getTheme(): ThemeMode {
|
|
8
13
|
try {
|
|
9
14
|
const v = localStorage.getItem(STORAGE_KEY);
|
|
@@ -14,7 +19,11 @@ export function getTheme(): ThemeMode {
|
|
|
14
19
|
return "dark";
|
|
15
20
|
}
|
|
16
21
|
|
|
17
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* Ставит `data-theme` и `color-scheme` на document.
|
|
24
|
+
*
|
|
25
|
+
* @param theme - `light` | `dark`, по умолчанию `getTheme()`
|
|
26
|
+
*/
|
|
18
27
|
export function applyTheme(theme: ThemeMode = getTheme()): void {
|
|
19
28
|
const root = document.documentElement;
|
|
20
29
|
const body = document.body;
|
|
@@ -23,6 +32,9 @@ export function applyTheme(theme: ThemeMode = getTheme()): void {
|
|
|
23
32
|
root.style.colorScheme = theme;
|
|
24
33
|
}
|
|
25
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Сохранить тему и применить к DOM.
|
|
37
|
+
*/
|
|
26
38
|
export function setTheme(theme: ThemeMode): void {
|
|
27
39
|
try {
|
|
28
40
|
localStorage.setItem(STORAGE_KEY, theme);
|
|
@@ -32,6 +44,11 @@ export function setTheme(theme: ThemeMode): void {
|
|
|
32
44
|
applyTheme(theme);
|
|
33
45
|
}
|
|
34
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Переключить light ↔ dark.
|
|
49
|
+
*
|
|
50
|
+
* @returns новая тема
|
|
51
|
+
*/
|
|
35
52
|
export function toggleTheme(): ThemeMode {
|
|
36
53
|
const next: ThemeMode = getTheme() === "dark" ? "light" : "dark";
|
|
37
54
|
setTheme(next);
|
|
@@ -8,17 +8,21 @@ import { Router } from "@elcrm/router";
|
|
|
8
8
|
import { pickRouteKey } from "../../lib/routeKey";
|
|
9
9
|
import { ACCOUNT_TAB_DEFAULT, ACCOUNT_TABS } from "./router.sections";
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
import(`./sections/${key}.tsx`),
|
|
11
|
+
const resolveSection = createLazyResolver((value) =>
|
|
12
|
+
import(`./sections/${value}.tsx`),
|
|
14
13
|
);
|
|
15
14
|
|
|
16
|
-
const ACCOUNT_KEYS = ACCOUNT_TABS.map((t) => t.
|
|
15
|
+
const ACCOUNT_KEYS = ACCOUNT_TABS.map((t) => t.value);
|
|
17
16
|
|
|
18
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* Аккаунт: `Section` + `TabSections`.
|
|
19
|
+
*
|
|
20
|
+
* Роутер: ключ `accountTab` (не смешивать с `section` / `listTab`).
|
|
21
|
+
* Секции: `sections/{Value}.tsx`.
|
|
22
|
+
*/
|
|
19
23
|
export default function Account() {
|
|
20
24
|
const tab = Router.useRouter("accountTab");
|
|
21
|
-
const
|
|
25
|
+
const value = pickRouteKey(tab, ACCOUNT_KEYS, ACCOUNT_TAB_DEFAULT);
|
|
22
26
|
|
|
23
27
|
return (
|
|
24
28
|
<Section
|
|
@@ -28,8 +32,8 @@ export default function Account() {
|
|
|
28
32
|
>
|
|
29
33
|
<TabSections
|
|
30
34
|
items={ACCOUNT_TABS}
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
value={value}
|
|
36
|
+
onValueChange={(next) => Router.setRouter("accountTab", next)}
|
|
33
37
|
resolveSection={resolveSection}
|
|
34
38
|
loading={<Loading />}
|
|
35
39
|
navLabel="Разделы аккаунта"
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { TabSectionsItem } from "@elcrm/components";
|
|
2
2
|
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Вкладки Account. `value` = `sections/{Value}.tsx`.
|
|
5
|
+
*/
|
|
4
6
|
export const ACCOUNT_TABS: TabSectionsItem[] = [
|
|
5
|
-
{
|
|
6
|
-
{
|
|
7
|
+
{ value: "Profile", label: "Профиль" },
|
|
8
|
+
{ value: "Sessions", label: "Сессии", badge: 1 },
|
|
7
9
|
];
|
|
8
10
|
|
|
9
11
|
export const ACCOUNT_TAB_DEFAULT = "Profile";
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { Card,
|
|
2
|
+
import { Card, Stack } from "@elcrm/components";
|
|
3
3
|
import { Api } from "@elcrm/api";
|
|
4
4
|
import { Button } from "@elcrm/button";
|
|
5
5
|
import { Notice } from "@elcrm/notice";
|
|
6
6
|
import { DisplayField, EmailField, StringField, useForm } from "@elcrm/form";
|
|
7
7
|
import useStateData from "../../../state";
|
|
8
8
|
|
|
9
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* Вкладка аккаунта: профиль.
|
|
11
|
+
*
|
|
12
|
+
* Поля: `DisplayField` login, `StringField` display_name, `EmailField` email.
|
|
13
|
+
* Сохранение: `Button` → `Api.query("auth/profile")` + `loadUser()`.
|
|
14
|
+
*/
|
|
10
15
|
export default function Profile() {
|
|
11
16
|
const { user, loadUser } = useStateData(["user", "loadUser"]);
|
|
12
17
|
const form = useForm({
|
|
@@ -45,7 +50,7 @@ export default function Profile() {
|
|
|
45
50
|
|
|
46
51
|
return (
|
|
47
52
|
<Card style={{ maxWidth: "28rem" }}>
|
|
48
|
-
<
|
|
53
|
+
<Stack gap="0.75rem">
|
|
49
54
|
<DisplayField
|
|
50
55
|
name="login"
|
|
51
56
|
form={form}
|
|
@@ -69,7 +74,7 @@ export default function Profile() {
|
|
|
69
74
|
disabled={busy}
|
|
70
75
|
onClick={() => void onSave()}
|
|
71
76
|
/>
|
|
72
|
-
</
|
|
77
|
+
</Stack>
|
|
73
78
|
</Card>
|
|
74
79
|
);
|
|
75
80
|
}
|