elcrm 1.1.13 → 1.1.15

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.
Files changed (51) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +1982 -1443
  3. package/package.json +3 -3
  4. package/templates/elcrm-cursor/rules/elcrm-jsdoc.mdc +1 -1
  5. package/templates/elcrm-cursor/rules/elcrm-packages.mdc +1 -1
  6. package/templates/elcrm-cursor/rules/elcrm-ui.mdc +3 -1
  7. package/templates/elcrm-docs/BUTTON.elCRM.md +3 -3
  8. package/templates/elcrm-docs/CLI.elCRM.md +26 -12
  9. package/templates/elcrm-docs/COMPONENTS.elCRM.md +18 -10
  10. package/templates/elcrm-docs/FORM.elCRM.md +11 -3
  11. package/templates/elcrm-docs/ICONS.elCRM.md +2 -0
  12. package/templates/elcrm-docs/NOTIFICATIONS.elCRM.md +1 -1
  13. package/templates/elcrm-docs/SERVER.elCRM.md +3 -1
  14. package/templates/orbit/README.md +12 -0
  15. package/templates/orbit/index.html +28 -0
  16. package/templates/orbit/package.json +39 -0
  17. package/templates/orbit/src/App.tsx +34 -0
  18. package/templates/orbit/src/component/Nav.tsx +139 -0
  19. package/templates/orbit/src/lazyRoute.ts +4 -0
  20. package/templates/orbit/src/lib/icon.tsx +48 -0
  21. package/templates/orbit/src/lib/theme.ts +39 -0
  22. package/templates/orbit/src/main.tsx +54 -0
  23. package/templates/orbit/src/modules/Analytics/index.tsx +5 -0
  24. package/templates/orbit/src/modules/Catalog/index.tsx +7 -0
  25. package/templates/orbit/src/modules/Customers/index.tsx +5 -0
  26. package/templates/orbit/src/modules/Finances/index.tsx +5 -0
  27. package/templates/orbit/src/modules/Home/index.tsx +317 -0
  28. package/templates/orbit/src/modules/Integrations/index.tsx +7 -0
  29. package/templates/orbit/src/modules/Link/index.tsx +7 -0
  30. package/templates/orbit/src/modules/Marketing/index.tsx +5 -0
  31. package/templates/orbit/src/modules/Placeholder.tsx +13 -0
  32. package/templates/orbit/src/modules/Settings/index.tsx +22 -0
  33. package/templates/orbit/src/modules/Store/index.tsx +7 -0
  34. package/templates/orbit/src/pages/Auth.tsx +45 -0
  35. package/templates/orbit/src/pages/Main.tsx +29 -0
  36. package/templates/orbit/src/router-data.d.ts +10 -0
  37. package/templates/orbit/src/state.ts +28 -0
  38. package/templates/orbit/src/style/elcrm.css +27 -0
  39. package/templates/orbit/src/style/theme-dark.css +148 -0
  40. package/templates/orbit/src/style/theme-light.css +148 -0
  41. package/templates/orbit/src/style/theme.css +177 -0
  42. package/templates/orbit/src/vite-env.d.ts +1 -0
  43. package/templates/orbit/tsconfig.json +19 -0
  44. package/templates/orbit/vite.config.ts +14 -0
  45. package/templates/panel/README.md +1 -1
  46. package/templates/panel/web/package.json +9 -9
  47. package/templates/panel/web/src/pages/Auth.tsx +1 -1
  48. package/templates/panel/web/src/style/theme-dark.css +9 -32
  49. package/templates/panel/web/src/style/theme-light.css +9 -32
  50. package/templates/panel/web/src/style/theme.css +20 -39
  51. package/templates/panel/web/vite.config.ts +3 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "elcrm",
3
- "version": "1.1.13",
4
- "description": "CLI @elcrm/*: update --fix --test, css (prune imports), docs, cursor, migrate, audit",
3
+ "version": "1.1.15",
4
+ "description": "CLI @elcrm/*: doctor --fix (порядок проекта), update, css, docs, cursor, migrate",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "MaSkal <dev@elcrm.online>",
@@ -52,7 +52,7 @@
52
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
53
  "dev": "bun run ./src/index.ts",
54
54
  "elcrm": "bun run ./src/index.ts",
55
- "test": "bun test src/lib/migrate src/vite",
55
+ "test": "bun test src/lib/migrate src/lib/tidy.test.ts src/vite",
56
56
  "link:local": "bun run build && bun link",
57
57
  "prepublishOnly": "bun run test && bun run build",
58
58
  "publish:npm": "npm publish"
@@ -22,7 +22,7 @@ alwaysApply: false
22
22
  * Слоты (что можно передать):
23
23
  * - brand — логотип (`Brand`)
24
24
  * - nav — пункты (`Menu`)
25
- * - search — `@elcrm/search`
25
+ * - search — `SearchField` из `@elcrm/form`
26
26
  * - actions — кнопки, `Avatar`, `Tooltip`
27
27
  *
28
28
  * @see Header из `@elcrm/components` — те же слоты
@@ -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/Stack/NavSections; migrate components |
29
+ | `COMPONENTS.elCRM.md` | `@elcrm/components` | Layout/Header/Sidebar/Popover/Dropdown; migrate components |
30
30
  | `SERVER.elCRM.md` | `@elcrm/server` | createServer + createRouter, не REST, не node:fs |
31
31
 
32
32
  ## Корень приложения (panel)
@@ -12,7 +12,7 @@ alwaysApply: false
12
12
 
13
13
  `Alert.Init`, `Modal.Init`, `Notice.Init`, `Form.Init`, при необходимости `Notifications.Init`, затем `LoadRoute`.
14
14
 
15
- ## Структура (panel / app)
15
+ ## Структура (panel / app / orbit)
16
16
 
17
17
  ```
18
18
  src/App.tsx — *.Init + LoadRoute
@@ -25,6 +25,8 @@ src/modules/Name/
25
25
  src/style/theme*.css + elcrm.css
26
26
  ```
27
27
 
28
+ Orbit: `Layout sidebar={<Sidebar />}` вместо верхней `Header`. Дашборд — `PageShell` + `Stat` / `Card` / `Item`.
29
+
28
30
  - Кнопки: `@elcrm/button` (`onSend`). Иконки: `Icons.Line`. Подсказки: `@elcrm/tooltip`.
29
31
  - HTTP: `Api.query("auth/pass", body)`. Роутер: `Router.setRouter` / `mergeRouter`, разные ключи вкладок.
30
32
  - `NavSections` / `TabSections`: `createLazyResolver((key) => import(\`./sections/${key}.tsx\`))`.
@@ -11,8 +11,8 @@ import { Button } from "@elcrm/button";
11
11
 
12
12
  <Button
13
13
  label="Сохранить"
14
- onSend={(done) => {
15
- void save().finally(() => done(false));
14
+ onSend={async () => {
15
+ await save();
16
16
  }}
17
17
  pendingContent="Сохранение…"
18
18
  />
@@ -23,7 +23,7 @@ import { Button } from "@elcrm/button";
23
23
  - `variant`: `primary` · `secondary` · `positive` · `negative` · `ghost`
24
24
  - `size`: `s` · `m` · `l` | `width`: `s` (контент) · `m` (min-width) · `l` (на всю ширину)
25
25
  - `label` / `before` / `after`; `children` перекрывает слоты
26
- - `onSend(setDisabled, event)`: вернуть `false` — не входить в pending; иначе снять через `setDisabled(false)`
26
+ - `onSend(setDisabled, event)`: синхронный `false` — без pending; промис/`async` — pending до settle; иначе снять через `setDisabled(false)`
27
27
  - Если задан `onClick` — `onSend` **не** вызывается
28
28
  - `buttonType`: `button` \| `submit` \| `reset`
29
29
  - Точечно: `@elcrm/button/Button`
@@ -62,12 +62,15 @@ elcrm cursor --lib # + чеклист автора @elcrm/* (прав
62
62
 
63
63
  ```bash
64
64
  elcrm migrate --list
65
- elcrm migrate front # пакет (то же, что update/audit/doctor --fix):
66
- # form-aliases → form-tokens → components →
67
- # css-sanitize → size-sml → field-border →
68
- # modal-create → socket-server
65
+ elcrm migrate front # пакет (то же, что doctor --fix без css):
66
+ # form-aliases → form-native → native-html →
67
+ # form-tokens → components → css-sanitize →
68
+ # size-sml → field-border → modal-create →
69
+ # socket-server
70
+ elcrm migrate form-native # снять prop native у @elcrm/form
71
+ elcrm migrate native-html # <input|textarea> → StringField/CheckField/…
69
72
  elcrm migrate css-sanitize # осиротевшие in srgb / transparent);
70
- elcrm migrate components # Block/Row → Stack, --page-* → --shell-*
73
+ elcrm migrate components # Block/Row → Stack, --page-* / --item-* → --shell-*
71
74
  elcrm migrate form-tokens
72
75
  elcrm migrate form-aliases
73
76
  elcrm migrate size-sml # size sm|md → s|m|l
@@ -82,12 +85,15 @@ elcrm migrate socket-server
82
85
  ### `audit` / `doctor` / `test` — проверки
83
86
 
84
87
  ```bash
85
- elcrm audit # старые API, битые @import, orphan color-mix, …
86
- elcrm audit --fix # = migrate front (+ css не вызывает — после: elcrm css)
87
- elcrm doctor # стандарты проекта
88
- elcrm doctor --fix # type/engines + migrate front
88
+ elcrm audit # старые API, native input, prop native, orphan color-mix, …
89
+ elcrm audit --fix # = migrate front (после: elcrm css или doctor --fix)
90
+ elcrm doctor # навести порядок (migrate + css)
91
+ elcrm doctor --docs # + docs/*.elCRM.md и .cursor
92
+ elcrm doctor --test # затем elcrm test
93
+ elcrm doctor --dry # только превью
89
94
  elcrm test # статический анализ + scripts проекта
90
- elcrm test --front|--server|--no-scripts
95
+ elcrm test --fix # migrate front, затем анализ
96
+ elcrm test --front|--server|--no-scripts|--dry
91
97
  ```
92
98
 
93
99
  ### Vite-плагины из пакета `elcrm`
@@ -110,8 +116,15 @@ minifyCssVars({ var: 2 })
110
116
  ### Прочее
111
117
 
112
118
  ```bash
113
- elcrm create <name> [--template app|lib|panel] [--no-install]
114
- elcrm init [--template …] [--force] [--no-install]
119
+ elcrm create my-app --template app
120
+ elcrm create my-lib --template lib
121
+ elcrm create my-panel --template panel
122
+ elcrm create my-shop --template orbit
123
+ elcrm create my-app --template app --no-install
124
+ elcrm init --template app
125
+ elcrm init --template lib --force
126
+ elcrm init --template panel
127
+ elcrm init --template orbit --force --no-install
115
128
  elcrm build --client [--url=…] [--name=…] [--dir=…] [--folder=…]
116
129
  elcrm postbuild [--dir=./dist] # lib: склейка CSS после vite build
117
130
  elcrm uuid
@@ -124,6 +137,7 @@ elcrm -v / --help
124
137
 
125
138
  | Ситуация | Команды |
126
139
  | --- | --- |
140
+ | Проект после смены API/токенов, пакеты уже стоят | `elcrm doctor` (+ `--docs --test`) |
127
141
  | Обновили пакеты / «после elcrm update всё красное» | полный цикл из 4 команд сверху |
128
142
  | Vite: `ENOENT … @elcrm/…/light.css` | `elcrm css` → при необходимости `elcrm audit` |
129
143
  | Битый theme (`in srgb` без свойства) | `elcrm migrate css-sanitize` или `update --fix` |
@@ -14,7 +14,7 @@ Shell UI: оболочка, шапка, секции, списки, Dropdown / R
14
14
  import {
15
15
  Layout, Header, Footer, Brand, Avatar, AvatarName, AvatarGroup, Badge,
16
16
  Menu, Dropdown, Section, NavSections, TabSections, Breadcrumb, Stack,
17
- Card, List, Item, PageHead, PageShell,
17
+ Card, Stat, ActionGroup, TextGroup, ItemColumn, List, Item, PageHead, PageShell, Sidebar, Popover,
18
18
  Loading, EmptyState, RadioGroup, PricingGroup, PricingTable,
19
19
  createLazyResolver,
20
20
  } from "@elcrm/components";
@@ -23,22 +23,30 @@ import type { PricingPlan } from "@elcrm/components";
23
23
 
24
24
  ## Роли layout
25
25
 
26
- - **PageShell** — каркас модуля (скролл + footer).
27
- - **PageHead** — title + actions.
26
+ - **PageShell** — каркас модуля (`scroll` в body + footer). `scroll={false}` — без внутреннего скролла.
27
+ - **PageHead** — title + actions. `sup` — текст или Badge сразу после title (надстрочно; `0` скрыт). `sticky` — шапка прилипает к верху скролла (фон `--shell-bg`). `children` — фильтры/поиск под заголовком, отступ `--shell-gap`.
28
28
  - **Section** — блок контента / auth.
29
- - **Layout / Header** — оболочка приложения.
30
- - **Stack** — ряд/колонка (`direction="row"|"column"`). Не `Block` / `Row` / `Column`.
29
+ - **Layout / Header** — оболочка приложения. `Layout sidebar={<Sidebar />}` — ряд: меню слева + контент.
30
+ - **Sidebar** — левое меню. `variant="cards"` (карточки) или `"plain"` (плоский backoffice). `groups` + `value`. Слоты: `brand`, `extra`, `footer`. Пункты: `--sidebar-item-font-size`, `--sidebar-item-padding`, `--sidebar-icon-size`, `--sidebar-group-font-size`. Не наследуют `body` 18px (`Item` `font: inherit` перебит).
31
+ - **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%` у детей сетки.
32
+ - **Card** — курсор: `--card-pointer` (`auto` / `pointer`). Сетка Stack: `--stack-grid-min`.
33
+ - **Stat** — метрика: `label` + `value`, `tone` как у Badge, `labelMuted`. Не собирать из `span` в приложении.
34
+ - **ActionGroup** — тулбар: `label` + `items` (узлы) и/или `children`. Кнопки — `@elcrm/button`, не нативный `<button>`.
35
+ - **TextGroup** — две строки: `title` + `description` (без аватара; с аватаром — AvatarName).
36
+ - **Item** — строка списка. `as` button/a/li, `active`. `variant="card"` — рамка и muted-фон (идеи / тикеты / inbox). Не дублировать `.desk-row` в приложении.
37
+ - **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`).
31
38
 
32
39
  ## Меню
33
40
 
34
- - **Menu** — inline-навигация в шапке.
35
- - **Dropdown** — выпадающее меню по `items`/`groups`.
36
- - **`@elcrm/overlay`** — portal / позиционирование / z-stack для своих попапов.
41
+ - **Menu** — inline-навигация в шапке. `items`: `value` + `label` + `badge` (`0` не показываем, иначе `Badge` size s). `variant="line"` — вкладки с чертой снизу (не ломать pill селекторами страницы).
42
+ - **Dropdown** — выпадающее меню по `items`/`groups` (portal).
43
+ - **Popover** — свой попап в portal (якорь + `children`). z-index: `acquireOverlayZIndex` (тот же стек, что у form). Не `@elcrm/overlay` `Menu`.
37
44
 
38
45
  ## Avatar
39
46
 
40
- - **Avatar** — фото / инициалы (`name` только для инициалов).
41
- - **AvatarName** — `title` + `description` (+ `name` для инициалов).
47
+ - **Avatar** — фото / инициалы (`name` только для инициалов). Скругление: `radius={10}` или `--avatar-radius` (дефолт `999px` — круг).
48
+ - **Badge** — метка: `tone`, `size` (`s`/`m`/`l`), `dot` (точка слева). Не Button.
49
+ - **AvatarName** — `title` + `description` (+ `name` для инициалов). Тот же `radius`. Цвета: `--avatar-name-title-color`, `--avatar-name-description-color`.
42
50
  - **AvatarGroup** — `items` с `value` / `title` / `name` / `src`.
43
51
 
44
52
  ## Pricing
@@ -21,17 +21,25 @@ const form = useForm({ login: "", password: "" });
21
21
  Связь: всегда `name` + `form`. Значения: `getValue` / `getValues` / `setValue` / `setValues` / `reset`.
22
22
  Для Date/массивов/объектов — **новая ссылка**, иначе `Object.is` не уведомит. Принудительный ререндер: `setValue`.
23
23
 
24
- Канонические поля: `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`.
24
+ Канонические поля: `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`.
25
25
 
26
26
  `before` / `after` — слоты (иконки). Размеры: `"s"` | `"m"` | `"l"`, не `sm`/`md`.
27
27
 
28
+ `PasswordField`: `generate` — кнопка случайного пароля. `true` → `Az09#` (A–Z, a–z, 0–9, символы). `generate="A"` только заглавные, `"59"` цифры 5–9, `"#"` только символы. Длина — `maxLength` или 12.
29
+
30
+ `SearchField` — поиск в форме (`form`+`name`, `timeout`, `suggestions` / `loadSuggestions`, `minimal`, `hotkey`, `variant="ghost"|"filled"`).
31
+
32
+ В хедере / тулбаре — `<Search value onValue>` из `@elcrm/search` (без формы). Стили те же `--field-*` / `--select-*`; узкие: `--field-search-min-width`, `--field-search-expand-min`, `--field-search-list-max-height`.
33
+
28
34
  Токены: `--field-padding-block` / `--field-padding-inline`, `--popup-shadow`. Hover/focus/disabled border — **только цвет**, не shorthand `1px solid …`.
29
35
 
30
36
  ## Нельзя
31
37
 
32
- - Нативные `<input>`, `<textarea>`, `<select>` в приложении — `elcrm test` это **error**. Исключение: пакет `@elcrm/form`.
38
+ - Нативные `<input>`, `<textarea>`, `<select>` в приложении — `elcrm test` это **error**. Исключение: пакет `@elcrm/form`. Чинит (best-effort): `elcrm migrate native-html` / `elcrm test --fix`. `<select>` и input с `ref`/`onKeyDown` — вручную.
33
39
  - Алиасы: `ProgressField` → `RangeField`, `ReadonlyField` → `DisplayField`, `OtpField` → `CodeField`, `SegmentField` → `TabsField`, `DragonDropField` → `DragDropField`. Чинит `elcrm migrate form-aliases`.
34
- - Проп `native` на полях — `elcrm test` предупреждает. У `StringField` `native` исторически для autofill; у `PasswordField` `native` значит «не рендерить» — не путать.
40
+ - В форме: `Search` из `@elcrm/search` → `SearchField` (`form`+`name`). Хедер — `@elcrm/search`.
41
+ - Токены `--search-*` → `--field-*` (`elcrm migrate form-tokens`).
42
+ - Проп `native` на полях — `elcrm test` предупреждает; снять: `elcrm migrate form-native`. У `StringField` `native` исторически для autofill; у `PasswordField` `native` значит «не рендерить» — не путать.
35
43
  - Legacy-токены: `--field-note-padding*`, `--control-inner-padding`, `--control-side-padding`, `--field-side-padding`, `--date-popup-shadow` → `elcrm migrate form-tokens`.
36
44
  - Пересоздавать `options` селекта на каждый рендер — константа модуля.
37
45
  - Серверные пакеты `@elcrm/db|jwt|cron|tb` на фронте.
@@ -21,6 +21,8 @@ import { icons } from "@elcrm/icons/vite";
21
21
  export default defineConfig({ plugins: [icons(), react()] });
22
22
  ```
23
23
 
24
+ Плагин ставит alias `@elcrm/icons/generated` → `src/icons.generated.tsx` и исключает пакет из `optimizeDeps` (иначе prebundle отдаёт stub и `Icons.Line` пустой).
25
+
24
26
  ```tsx
25
27
  import { Icons } from "@elcrm/icons";
26
28
  <Icons.Line name="search" size={20} />
@@ -6,7 +6,7 @@ In-app inbox + Web Push. Сервер: `@elcrm/server` (subpath notifications),
6
6
 
7
7
  ```tsx
8
8
  import { Notifications, bindRouterNavigate } from "@elcrm/notifications";
9
- import workerUrl from "@elcrm/notifications/worker?url";
9
+ const workerUrl = "/push-sw.js"; // не import ?url — Vite делает data:, SW нельзя
10
10
 
11
11
  bindRouterNavigate((url) => { /* Router / pathname */ });
12
12
 
@@ -39,8 +39,10 @@ Env: `TOKEN` / `JWT_SECRET`, `CORS_ORIGINS` (origins **без path**, через
39
39
  | `@elcrm/db` `@elcrm/jwt` `@elcrm/cron` `@elcrm/tb` | `@elcrm/server/db` `/jwt` `/cron` `/tb` |
40
40
  | `createServerFromEnv` | `createServer()` |
41
41
  | `createJwtFromEnv` / `createCronFromEnv` / `createTbFromEnv` | `createJWT` / `createCron` / `createTb` |
42
- | `createWsHub` / `serverConnect` | `createSocket("ws").use(…)` |
42
+ | `createWsHub` / `serverConnect` / `createServer({ websocket: { handlers } })` | `createSocket("ws").use(…)` до `createServer` |
43
43
  | `hub.sendListen` | `sendSocket` |
44
+ | свой `fetch` на `api.telegram.org` | `tb.sendMessage` / `tb.use` |
45
+ | `CORS_ORIGINS` для origins из БД | `cors({ resolveOrigin })` |
44
46
  | `Cron.useJob` / namespace `Cron` | `cron.use` |
45
47
  | REST `path === "/api/…"` / `handle*(ctx): Promise<Response>` | `createRouter().use()` |
46
48
  | `nodemailer` / `web-push` | `@elcrm/server/mail` / `notifications` |
@@ -0,0 +1,12 @@
1
+ # Orbit
2
+
3
+ Шаблон витрины на `@elcrm/components`: `Layout` + `Sidebar` + дашборд Home.
4
+
5
+ ```bash
6
+ elcrm create my-shop --template orbit
7
+ bun run dev
8
+ ```
9
+
10
+ Web: http://localhost:5173
11
+
12
+ Демо-вход: любой логин/пароль (локальный store, API не обязателен).
@@ -0,0 +1,28 @@
1
+ <!doctype html>
2
+ <html lang="en" data-theme="light">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Orbit</title>
7
+ <script>
8
+ try {
9
+ var t = localStorage.getItem("elcrm-orbit-theme");
10
+ if (t !== "light" && t !== "dark") t = "light";
11
+ document.documentElement.setAttribute("data-theme", t);
12
+ document.documentElement.style.colorScheme = t;
13
+ } catch (e) {}
14
+ </script>
15
+ </head>
16
+ <body data-theme="light">
17
+ <script>
18
+ try {
19
+ document.body.setAttribute(
20
+ "data-theme",
21
+ document.documentElement.getAttribute("data-theme") || "light",
22
+ );
23
+ } catch (e) {}
24
+ </script>
25
+ <div id="root"></div>
26
+ <script type="module" src="/src/main.tsx"></script>
27
+ </body>
28
+ </html>
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "orbit",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite --port 5173",
8
+ "build": "vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "@elcrm/alert": "0.1.1",
13
+ "@elcrm/api": "0.1.55",
14
+ "@elcrm/button": "1.1.0",
15
+ "@elcrm/components": "0.1.2",
16
+ "@elcrm/core": "0.1.3",
17
+ "@elcrm/crypto": "0.0.2",
18
+ "@elcrm/error-boundary": "0.0.2",
19
+ "@elcrm/form": "0.1.8",
20
+ "@elcrm/modal": "0.0.34",
21
+ "@elcrm/notice": "0.1.3",
22
+ "@elcrm/notifications": "0.3.1",
23
+ "@elcrm/passkey": "0.2.3",
24
+ "@elcrm/router": "0.0.58",
25
+ "@elcrm/search": "0.1.5",
26
+ "@elcrm/setting": "0.0.6",
27
+ "@elcrm/state": "0.0.7",
28
+ "@elcrm/tooltip": "0.1.0",
29
+ "react": "^18.2.0",
30
+ "react-dom": "^18.2.0"
31
+ },
32
+ "devDependencies": {
33
+ "@types/react": "^18.2.79",
34
+ "@types/react-dom": "^18.2.25",
35
+ "@vitejs/plugin-react": "^4.3.1",
36
+ "typescript": "^5.8.2",
37
+ "vite": "^5.4.2"
38
+ }
39
+ }
@@ -0,0 +1,34 @@
1
+ import { Alert } from "@elcrm/alert";
2
+ import { Modal } from "@elcrm/modal";
3
+ import { Notice } from "@elcrm/notice";
4
+ import { Notifications } from "@elcrm/notifications";
5
+ import { Form } from "@elcrm/form";
6
+ import { LoadRoute } from "@elcrm/router";
7
+ import { createLazyResolver } from "./lazyRoute";
8
+
9
+ const resolvePage = createLazyResolver((segment) =>
10
+ import(`./pages/${segment}.tsx`),
11
+ );
12
+
13
+ /**
14
+ * Корень Orbit: Init пакетов + страница.
15
+ */
16
+ export default function App() {
17
+ return (
18
+ <>
19
+ <Alert.Init />
20
+ <Modal.Init />
21
+ <Notice.Init position="top-right" durationMs={2200} maxVisible={4} />
22
+ <Notifications.Init appBadge={false} autoSync={false} />
23
+ <Form.Init onNotice={(msg) => Notice.Remark(String(msg))} />
24
+ <LoadRoute
25
+ variant="page"
26
+ defaultName="Main"
27
+ resolveLazyPage={resolvePage}
28
+ loading={<div className="app-loading">Загрузка…</div>}
29
+ >
30
+ <span aria-hidden />
31
+ </LoadRoute>
32
+ </>
33
+ );
34
+ }
@@ -0,0 +1,139 @@
1
+ import React from "react";
2
+ import {
3
+ AvatarName,
4
+ Brand,
5
+ Dropdown,
6
+ Sidebar,
7
+ Stack,
8
+ } from "@elcrm/components";
9
+ import { Button } from "@elcrm/button";
10
+ import { Search } from "@elcrm/search";
11
+ import { Router } from "@elcrm/router";
12
+ import { Alert } from "@elcrm/alert";
13
+ import { Tooltip } from "@elcrm/tooltip";
14
+ import useStateData from "../state";
15
+ import { Ico, Icon } from "../lib/icon";
16
+ import { getTheme, toggleTheme } from "../lib/theme";
17
+
18
+ const GROUPS = [
19
+ {
20
+ value: "menu",
21
+ title: "Menu",
22
+ items: [
23
+ { value: "Home", label: "Home", icon: <Icon d={Ico.home} /> },
24
+ { value: "Catalog", label: "Catalog", icon: <Icon d={Ico.catalog} /> },
25
+ { value: "Finances", label: "Finances", icon: <Icon d={Ico.finances} /> },
26
+ { value: "Customers", label: "Customers", icon: <Icon d={Ico.customers} /> },
27
+ { value: "Marketing", label: "Marketing", icon: <Icon d={Ico.marketing} /> },
28
+ { value: "Analytics", label: "Analytics", icon: <Icon d={Ico.analytics} /> },
29
+ ],
30
+ },
31
+ {
32
+ value: "integrations",
33
+ title: "Integrations",
34
+ items: [
35
+ {
36
+ value: "Integrations",
37
+ label: "Integrations",
38
+ icon: <Icon d={Ico.grid} />,
39
+ },
40
+ ],
41
+ },
42
+ {
43
+ value: "channels",
44
+ title: "Sales Channels",
45
+ items: [
46
+ { value: "Store", label: "Online Store", icon: <Icon d={Ico.store} /> },
47
+ { value: "Link", label: "Sell Via Link", icon: <Icon d={Ico.link} /> },
48
+ ],
49
+ },
50
+ ];
51
+
52
+ /**
53
+ * Левое меню Orbit: `Sidebar` + магазин + поиск + профиль.
54
+ */
55
+ export default function Nav() {
56
+ const { user, shop, setShop } = useStateData(["user", "shop"]);
57
+ const module = Router.useRouter("module");
58
+ const [q, setQ] = React.useState("");
59
+ const [theme, setThemeState] = React.useState(() => getTheme());
60
+
61
+ const onLogout = () => {
62
+ Alert.Send(
63
+ { title: "Log out", text: "Leave the dashboard?" },
64
+ () => {
65
+ Router.mergeRouter({ page: "Auth", module: "Home" });
66
+ },
67
+ );
68
+ };
69
+
70
+ return (
71
+ <Sidebar
72
+ variant="plain"
73
+ value={String(module || "Home")}
74
+ onSelect={(value) => Router.setRouter("module", value)}
75
+ brand={
76
+ <Stack direction="column" gap="0.75rem">
77
+ <Brand>Orbit</Brand>
78
+ <Dropdown
79
+ label={shop}
80
+ align="start"
81
+ items={[
82
+ { value: "Mob Shop", label: "Mob Shop" },
83
+ { value: "Jankenpun", label: "Jankenpun" },
84
+ ]}
85
+ onSelect={(value) => setShop(value)}
86
+ />
87
+ <Search
88
+ value={q}
89
+ onValue={(v: string) => setQ(v)}
90
+ placeholder="Search"
91
+ aria-label="Search"
92
+ timeout={0}
93
+ />
94
+ </Stack>
95
+ }
96
+ groups={GROUPS}
97
+ footer={
98
+ <Stack direction="column" gap="0.5rem">
99
+ <AvatarName
100
+ name={user.displayName}
101
+ title={user.displayName}
102
+ description={user.email}
103
+ />
104
+ <Stack direction="row" gap="0.35rem">
105
+ <Tooltip content={theme === "dark" ? "Light" : "Dark"}>
106
+ <Button
107
+ variant="ghost"
108
+ width="s"
109
+ size="s"
110
+ label={theme === "dark" ? "☀" : "☾"}
111
+ onClick={() => setThemeState(toggleTheme())}
112
+ />
113
+ </Tooltip>
114
+ <Tooltip content="Settings">
115
+ <Button
116
+ variant="ghost"
117
+ width="s"
118
+ size="s"
119
+ before={<Icon d={Ico.settings} size={16} />}
120
+ label="Settings"
121
+ onClick={() => Router.setRouter("module", "Settings")}
122
+ />
123
+ </Tooltip>
124
+ <Tooltip content="Log out">
125
+ <Button
126
+ variant="ghost"
127
+ width="s"
128
+ size="s"
129
+ before={<Icon d={Ico.logout} size={16} />}
130
+ label="Log out"
131
+ onClick={onLogout}
132
+ />
133
+ </Tooltip>
134
+ </Stack>
135
+ </Stack>
136
+ }
137
+ />
138
+ );
139
+ }
@@ -0,0 +1,4 @@
1
+ export {
2
+ createLazyResolver,
3
+ type LazyRouteChunk,
4
+ } from "@elcrm/components";
@@ -0,0 +1,48 @@
1
+ import React from "react";
2
+
3
+ /** Линейная иконка 20×20, цвет — currentColor. */
4
+ export function Icon({
5
+ d,
6
+ size = 18,
7
+ }: {
8
+ d: string;
9
+ size?: number;
10
+ }) {
11
+ return (
12
+ <svg
13
+ width={size}
14
+ height={size}
15
+ viewBox="0 0 24 24"
16
+ fill="none"
17
+ stroke="currentColor"
18
+ strokeWidth="1.6"
19
+ strokeLinecap="round"
20
+ strokeLinejoin="round"
21
+ aria-hidden
22
+ >
23
+ <path d={d} />
24
+ </svg>
25
+ );
26
+ }
27
+
28
+ export const Ico = {
29
+ home: "M3 10.5 12 3l9 7.5V21a1 1 0 0 1-1 1h-5v-7H9v7H4a1 1 0 0 1-1-1z",
30
+ catalog: "M4 6h16M4 12h16M4 18h10",
31
+ finances: "M12 3v18M17 8H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H7",
32
+ customers: "M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8M22 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75",
33
+ marketing: "M4 12v8a1 1 0 0 0 1 1h3v-9zM8 9l11-6v18L8 15",
34
+ analytics: "M4 19V5M4 19h16M8 17v-6M12 17V8M16 17v-4",
35
+ grid: "M4 4h7v7H4zM13 4h7v7h-7zM4 13h7v7H4zM13 13h7v7h-7z",
36
+ store: "M3 9 5 3h14l2 6M5 9v11h14V9M9 22V12h6v10",
37
+ link: "M10 13a5 5 0 0 0 7.07 0l1.41-1.41a5 5 0 0 0-7.07-7.07L10 5.93M14 11a5 5 0 0 0-7.07 0L5.52 12.4a5 5 0 1 0 7.07 7.07L14 18.07",
38
+ search: "M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16zM21 21l-4.3-4.3",
39
+ globe: "M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20zM2 12h20M12 2a15 15 0 0 1 0 20M12 2a15 15 0 0 0 0 20",
40
+ settings: "M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9c.3.6.9 1 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z",
41
+ logout: "M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9",
42
+ plus: "M12 5v14M5 12h14",
43
+ box: "M21 8 12 3 3 8l9 5 9-5zM3 8v8l9 5M21 8v8l-9 5M12 13v8",
44
+ truck: "M3 7h11v10H3zM14 11h4l3 3v3h-7M5 21a2 2 0 1 0 0-4 2 2 0 0 0 0 4zM17 21a2 2 0 1 0 0-4 2 2 0 0 0 0 4z",
45
+ bell: "M6 8a6 6 0 1 1 12 0c0 7 3 9 3 9H3s3-2 3-9M10 21a2 2 0 0 0 4 0",
46
+ chat: "M21 15a4 4 0 0 1-4 4H8l-5 3V7a4 4 0 0 1 4-4h10a4 4 0 0 1 4 4z",
47
+ crown: "M3 8 7.5 13 12 5l4.5 8L21 8v10H3z",
48
+ };
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Тема Orbit: `data-theme` на html/body + localStorage `elcrm-orbit-theme`.
3
+ */
4
+ export type ThemeMode = "dark" | "light";
5
+
6
+ const STORAGE_KEY = "elcrm-orbit-theme";
7
+
8
+ export function getTheme(): ThemeMode {
9
+ try {
10
+ const v = localStorage.getItem(STORAGE_KEY);
11
+ if (v === "light" || v === "dark") return v;
12
+ } catch {
13
+ /* ignore */
14
+ }
15
+ return "light";
16
+ }
17
+
18
+ export function applyTheme(theme: ThemeMode = getTheme()): void {
19
+ const root = document.documentElement;
20
+ const body = document.body;
21
+ root.setAttribute("data-theme", theme);
22
+ if (body) body.setAttribute("data-theme", theme);
23
+ root.style.colorScheme = theme;
24
+ }
25
+
26
+ export function setTheme(theme: ThemeMode): void {
27
+ try {
28
+ localStorage.setItem(STORAGE_KEY, theme);
29
+ } catch {
30
+ /* ignore */
31
+ }
32
+ applyTheme(theme);
33
+ }
34
+
35
+ export function toggleTheme(): ThemeMode {
36
+ const next: ThemeMode = getTheme() === "dark" ? "light" : "dark";
37
+ setTheme(next);
38
+ return next;
39
+ }