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.
- package/README.md +1 -1
- package/dist/index.js +1982 -1443
- package/package.json +3 -3
- package/templates/elcrm-cursor/rules/elcrm-jsdoc.mdc +1 -1
- package/templates/elcrm-cursor/rules/elcrm-packages.mdc +1 -1
- package/templates/elcrm-cursor/rules/elcrm-ui.mdc +3 -1
- package/templates/elcrm-docs/BUTTON.elCRM.md +3 -3
- package/templates/elcrm-docs/CLI.elCRM.md +26 -12
- package/templates/elcrm-docs/COMPONENTS.elCRM.md +18 -10
- package/templates/elcrm-docs/FORM.elCRM.md +11 -3
- package/templates/elcrm-docs/ICONS.elCRM.md +2 -0
- package/templates/elcrm-docs/NOTIFICATIONS.elCRM.md +1 -1
- package/templates/elcrm-docs/SERVER.elCRM.md +3 -1
- package/templates/orbit/README.md +12 -0
- package/templates/orbit/index.html +28 -0
- package/templates/orbit/package.json +39 -0
- package/templates/orbit/src/App.tsx +34 -0
- package/templates/orbit/src/component/Nav.tsx +139 -0
- package/templates/orbit/src/lazyRoute.ts +4 -0
- package/templates/orbit/src/lib/icon.tsx +48 -0
- package/templates/orbit/src/lib/theme.ts +39 -0
- package/templates/orbit/src/main.tsx +54 -0
- package/templates/orbit/src/modules/Analytics/index.tsx +5 -0
- package/templates/orbit/src/modules/Catalog/index.tsx +7 -0
- package/templates/orbit/src/modules/Customers/index.tsx +5 -0
- package/templates/orbit/src/modules/Finances/index.tsx +5 -0
- package/templates/orbit/src/modules/Home/index.tsx +317 -0
- package/templates/orbit/src/modules/Integrations/index.tsx +7 -0
- package/templates/orbit/src/modules/Link/index.tsx +7 -0
- package/templates/orbit/src/modules/Marketing/index.tsx +5 -0
- package/templates/orbit/src/modules/Placeholder.tsx +13 -0
- package/templates/orbit/src/modules/Settings/index.tsx +22 -0
- package/templates/orbit/src/modules/Store/index.tsx +7 -0
- package/templates/orbit/src/pages/Auth.tsx +45 -0
- package/templates/orbit/src/pages/Main.tsx +29 -0
- package/templates/orbit/src/router-data.d.ts +10 -0
- package/templates/orbit/src/state.ts +28 -0
- package/templates/orbit/src/style/elcrm.css +27 -0
- package/templates/orbit/src/style/theme-dark.css +148 -0
- package/templates/orbit/src/style/theme-light.css +148 -0
- package/templates/orbit/src/style/theme.css +177 -0
- package/templates/orbit/src/vite-env.d.ts +1 -0
- package/templates/orbit/tsconfig.json +19 -0
- package/templates/orbit/vite.config.ts +14 -0
- package/templates/panel/README.md +1 -1
- package/templates/panel/web/package.json +9 -9
- package/templates/panel/web/src/pages/Auth.tsx +1 -1
- package/templates/panel/web/src/style/theme-dark.css +9 -32
- package/templates/panel/web/src/style/theme-light.css +9 -32
- package/templates/panel/web/src/style/theme.css +20 -39
- 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.
|
|
4
|
-
"description": "CLI @elcrm/*:
|
|
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/
|
|
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/
|
|
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={(
|
|
15
|
-
|
|
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)`:
|
|
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 # пакет (то же, что
|
|
66
|
-
# form-aliases → form-
|
|
67
|
-
#
|
|
68
|
-
# modal-create →
|
|
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,
|
|
86
|
-
elcrm audit --fix # = migrate front (
|
|
87
|
-
elcrm doctor #
|
|
88
|
-
elcrm doctor --
|
|
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
|
|
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
|
|
114
|
-
elcrm
|
|
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** — каркас модуля (
|
|
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
|
-
- **
|
|
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
|
-
-
|
|
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
|
-
- **
|
|
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
|
-
-
|
|
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
|
-
|
|
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,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
|
+
}
|