elcrm 1.0.0 → 1.1.2
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 +10 -0
- package/dist/index.js +2828 -425
- package/dist/vite/discover-lib.js +4 -4
- package/dist/vite/plugin-css-scoped.js +2 -2
- package/dist/vite/postbuild.d.ts +5 -0
- package/dist/vite/postbuild.js +59 -0
- package/package.json +10 -4
- package/templates/elcrm-cursor/AGENTS.md +13 -0
- package/templates/elcrm-cursor/rules/elcrm-lib.mdc +101 -0
- package/templates/elcrm-cursor/rules/elcrm-module.mdc +25 -0
- package/templates/elcrm-cursor/rules/elcrm-packages.mdc +43 -0
- package/templates/elcrm-cursor/rules/elcrm-rpc.mdc +23 -0
- package/templates/elcrm-cursor/rules/elcrm-server.mdc +30 -0
- package/templates/elcrm-cursor/rules/elcrm-theme.mdc +13 -0
- package/templates/elcrm-cursor/rules/elcrm-ui.mdc +41 -0
- package/templates/elcrm-cursor/rules/elcrm.mdc +43 -0
- package/templates/elcrm-docs/ALERT.elCRM.md +26 -0
- package/templates/elcrm-docs/API.elCRM.md +38 -0
- package/templates/elcrm-docs/BUTTON.elCRM.md +36 -0
- package/templates/elcrm-docs/CLI.elCRM.md +27 -0
- package/templates/elcrm-docs/COMPONENTS.elCRM.md +68 -0
- package/templates/elcrm-docs/FORM.elCRM.md +39 -0
- package/templates/elcrm-docs/ICONS.elCRM.md +40 -0
- package/templates/elcrm-docs/MODAL.elCRM.md +41 -0
- package/templates/elcrm-docs/NOTICE.elCRM.md +31 -0
- package/templates/elcrm-docs/NOTIFICATIONS.elCRM.md +39 -0
- package/templates/elcrm-docs/PASSKEY.elCRM.md +26 -0
- package/templates/elcrm-docs/ROUTER.elCRM.md +34 -0
- package/templates/elcrm-docs/SERVER.elCRM.md +53 -0
- package/templates/elcrm-docs/SOCKET.elCRM.md +34 -0
- package/templates/elcrm-docs/TOOLTIP.elCRM.md +25 -0
- package/templates/panel/README.md +2 -26
- package/templates/panel/api/package.json +2 -5
- package/templates/panel/api/src/index.ts +11 -20
- package/templates/panel/api/src/lib/cron.ts +10 -7
- package/templates/panel/api/src/lib/db.ts +3 -5
- package/templates/panel/api/src/lib/jwt.ts +1 -1
- package/templates/panel/api/src/routes/auth.ts +20 -8
- package/templates/panel/web/package.json +4 -4
- package/templates/panel/web/src/modules/Account/sections/Profile.tsx +0 -1
- package/templates/panel/web/src/modules/Settings/sections/General.tsx +0 -2
- package/templates/panel/web/src/modules/Settings/sections/Profile.tsx +0 -1
- package/templates/panel/web/src/pages/Auth.tsx +0 -1
- package/templates/panel/web/src/style/elcrm.css +4 -1
- package/templates/panel/web/src/style/theme-dark.css +65 -3
- package/templates/panel/web/src/style/theme-light.css +65 -3
- package/templates/panel/web/src/style/theme.css +73 -3
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<!-- Сгенерировано `elcrm docs`. Источник: @elcrm/components + шаблон panel. Обновление: снова elcrm docs. -->
|
|
2
|
+
|
|
3
|
+
# `@elcrm/components`
|
|
4
|
+
|
|
5
|
+
Shell UI: оболочка страницы, шапка, секции, списки. **Цвета и размеры задаёт приложение** (`theme.css` + `theme-light` / `theme-dark`). Fallback в пакете нет. `elcrm css` дописывает недостающие токены.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import {
|
|
9
|
+
Layout, Header, Footer, Brand, Avatar, Badge,
|
|
10
|
+
Menu, Section, NavSections, TabSections,
|
|
11
|
+
Card, List, Item, Block, Row, Column,
|
|
12
|
+
Loading, EmptyState, createLazyResolver,
|
|
13
|
+
} from "@elcrm/components";
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Точечно: `import { Header } from "@elcrm/components/Header"`.
|
|
17
|
+
|
|
18
|
+
## Компоненты
|
|
19
|
+
|
|
20
|
+
| | Назначение | Ключевые props |
|
|
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(…)` |
|
|
36
|
+
|
|
37
|
+
Слоты Header заполняет приложение: поиск `@elcrm/search`, кнопки `@elcrm/button`, иконки `@elcrm/icons`.
|
|
38
|
+
|
|
39
|
+
## Структура приложения (panel)
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
src/
|
|
43
|
+
App.tsx # Init: Alert, Modal, Notice, Form, LoadRoute
|
|
44
|
+
pages/Auth.tsx Main.tsx
|
|
45
|
+
modules/<Name>/
|
|
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
|
+
```
|
|
53
|
+
|
|
54
|
+
- Секции: `createLazyResolver((key) => import(\`./sections/${key}.tsx\`))`.
|
|
55
|
+
- Ключи роутера не смешивать (`section` vs `listTab` vs `accountTab`) — иначе lazy откроет не ту секцию.
|
|
56
|
+
- Auth-экран: `<Section variant="auth">`, поля из `@elcrm/form`, submit через `@elcrm/button` `onSend`.
|
|
57
|
+
|
|
58
|
+
## Можно
|
|
59
|
+
|
|
60
|
+
Токены только в theme-файлах приложения. Импорт `@elcrm/components/style.css` — опционально (единый файл).
|
|
61
|
+
|
|
62
|
+
## Нельзя
|
|
63
|
+
|
|
64
|
+
- `import "@elcrm/…/themes.css"` — пакеты больше не отдают темы.
|
|
65
|
+
- Свои `<button>` / нативные инпуты вместо `@elcrm/button` / `@elcrm/form` в UI приложения.
|
|
66
|
+
- `size="sm"|"md"` → `"s"|"m"` (`elcrm migrate size-sml`).
|
|
67
|
+
- Дублировать toolbar-токены в модульных CSS — общий класс + theme.
|
|
68
|
+
- Хардкодить цвета в компонентах модулей, если есть токен.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<!-- Сгенерировано `elcrm docs`. Источник: @elcrm/form. Обновление: снова elcrm docs. -->
|
|
2
|
+
|
|
3
|
+
# `@elcrm/form`
|
|
4
|
+
|
|
5
|
+
Поля ввода + `useForm`. Стили вшиты в JS — отдельный css-loader не нужен. Темы: `@elcrm/form/light.css` / `dark.css` (или токены приложения + `elcrm css`).
|
|
6
|
+
|
|
7
|
+
## Можно
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { useForm, StringField, PasswordField, Form } from "@elcrm/form";
|
|
11
|
+
|
|
12
|
+
const form = useForm({ login: "", password: "" });
|
|
13
|
+
|
|
14
|
+
<Form.Init onNotice={(msg) => { /* тост */ }} />
|
|
15
|
+
<StringField name="login" form={form} label="Логин" autoComplete="username" />
|
|
16
|
+
<PasswordField name="password" form={form} label="Пароль" />
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`Form.Init` — если нужны notice / модалки для `ModalField` / `OptionsField`.
|
|
20
|
+
|
|
21
|
+
Связь: всегда `name` + `form`. Значения: `getValue` / `getValues` / `setValue` / `setValues` / `reset`.
|
|
22
|
+
Для Date/массивов/объектов — **новая ссылка**, иначе `Object.is` не уведомит. Принудительный ререндер: `setValue`.
|
|
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`.
|
|
25
|
+
|
|
26
|
+
`before` / `after` — слоты (иконки). Размеры: `"s"` | `"m"` | `"l"`, не `sm`/`md`.
|
|
27
|
+
|
|
28
|
+
Токены: `--field-padding-block` / `--field-padding-inline`, `--popup-shadow`. Hover/focus/disabled border — **только цвет**, не shorthand `1px solid …`.
|
|
29
|
+
|
|
30
|
+
## Нельзя
|
|
31
|
+
|
|
32
|
+
- Нативные `<input>`, `<textarea>`, `<select>` в приложении — `elcrm test` это **error**. Исключение: пакет `@elcrm/form`.
|
|
33
|
+
- Алиасы: `ProgressField` → `RangeField`, `ReadonlyField` → `DisplayField`, `OtpField` → `CodeField`, `SegmentField` → `TabsField`, `DragonDropField` → `DragDropField`. Чинит `elcrm migrate form-aliases`.
|
|
34
|
+
- Проп `native` на полях — `elcrm test` предупреждает. У `StringField` `native` исторически для autofill; у `PasswordField` `native` значит «не рендерить» — не путать.
|
|
35
|
+
- Legacy-токены: `--field-note-padding*`, `--control-inner-padding`, `--control-side-padding`, `--field-side-padding`, `--date-popup-shadow` → `elcrm migrate form-tokens`.
|
|
36
|
+
- Пересоздавать `options` селекта на каждый рендер — константа модуля.
|
|
37
|
+
- Серверные пакеты `@elcrm/db|jwt|cron|tb` на фронте.
|
|
38
|
+
|
|
39
|
+
Подпути (tree-shake): `@elcrm/form/fields/Date`, `@elcrm/form/hooks/use`.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<!-- Сгенерировано `elcrm docs`. Источник: @elcrm/icons. Обновление: снова elcrm docs. -->
|
|
2
|
+
|
|
3
|
+
# `@elcrm/icons`
|
|
4
|
+
|
|
5
|
+
CLI синхронизации SVG с [icons.elpm.dev](https://icons.elpm.dev). **Набор иконок в npm не входит** — только CLI и компонент `Icons`.
|
|
6
|
+
|
|
7
|
+
## Можно
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
icons remote https://icons.elpm.dev/{login}/{token}.icon # пишет .icons
|
|
11
|
+
icons update # → src/icons.generated.tsx
|
|
12
|
+
icons app fnqgs # иконка приложения в public/
|
|
13
|
+
icons migrate # старый ./Icons → @elcrm/icons
|
|
14
|
+
icons prune / icons prune --save
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Vite (`register`, формат по умолчанию):
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { icons } from "@elcrm/icons/vite";
|
|
21
|
+
export default defineConfig({ plugins: [icons(), react()] });
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
import { Icons } from "@elcrm/icons";
|
|
26
|
+
<Icons.Line name="search" size={20} />
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Ручной `import "./icons.generated"` **не нужен** — плагин подставляет файл.
|
|
30
|
+
|
|
31
|
+
Проверка: `hasIcons()` / `needsRegister()`.
|
|
32
|
+
|
|
33
|
+
## Нельзя
|
|
34
|
+
|
|
35
|
+
- Класть SVG-набор в git как «библиотеку пакета» — качайте через `icons update`.
|
|
36
|
+
- Вызывать `icons update` без `.icons` (`icons remote` сначала).
|
|
37
|
+
- Передавать `#` в hash для `icons app` в shell (`#` = комментарий).
|
|
38
|
+
- Импортировать локальный `./Icons`, если уже `@elcrm/icons` (см. `icons migrate`).
|
|
39
|
+
|
|
40
|
+
Формат `file`: `import { Icons } from "./icons.generated"` — standalone, без vite-плагина.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<!-- Сгенерировано `elcrm docs`. Пакет: @elcrm/modal. -->
|
|
2
|
+
|
|
3
|
+
# `@elcrm/modal`
|
|
4
|
+
|
|
5
|
+
Очередь окон. Обязательно `<Modal.Init />` в корне.
|
|
6
|
+
|
|
7
|
+
## Канон: `Modal.Create`
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
function EditModal({ id, onClose }: { id: number; onClose?: () => void }) {
|
|
11
|
+
return (
|
|
12
|
+
<Modal.Main title="Редактирование" onClose={onClose} size="m">
|
|
13
|
+
<Modal.Scroll>…</Modal.Scroll>
|
|
14
|
+
</Modal.Main>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
const { Open, Icon } = Modal.Create(EditModal, "edit-modal");
|
|
18
|
+
Open({ id: 123 });
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Файлы модулей: `modules/<Type>/modal/<Name>.tsx` + `Modal.Open("Type", "Name", params)` или `import_modules` у Init.
|
|
22
|
+
|
|
23
|
+
## Состав
|
|
24
|
+
|
|
25
|
+
| API | Зачем |
|
|
26
|
+
| --- | --- |
|
|
27
|
+
| `Modal.Main` | окно + оверлей: `title`, `size`, `button`, `tabs`, `menu`, `shake`, `escape` |
|
|
28
|
+
| `Modal.Panel` | без оверлея |
|
|
29
|
+
| `Modal.Scroll` / `Center` / `Form` / `Column` / `List` | раскладка |
|
|
30
|
+
| `Modal.Close(name?)` | одно или все |
|
|
31
|
+
| `Modal.Shake` | тряска |
|
|
32
|
+
|
|
33
|
+
`size`: `a` auto · `s` 400 · `m` 500 · `l` 800 · `x` 1000 · `f` full.
|
|
34
|
+
`escape` закрывает верхнее; клик по оверлею при `shake` (default true) трясёт, не закрывает; крестик закрывает всегда.
|
|
35
|
+
|
|
36
|
+
## Нельзя
|
|
37
|
+
|
|
38
|
+
- `Modal.Add` в новом коде — `elcrm migrate modal-create` / `elcrm test` warn. Add ещё работает, но Create задаёт стабильный `name`.
|
|
39
|
+
- `key={Date.now()}` на модалке.
|
|
40
|
+
- Без `Modal.Init`.
|
|
41
|
+
- Нативные input внутри модалки — `@elcrm/form`.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<!-- Сгенерировано `elcrm docs`. Пакет: @elcrm/notice. -->
|
|
2
|
+
|
|
3
|
+
# `@elcrm/notice`
|
|
4
|
+
|
|
5
|
+
Тосты. Один `<Notice.Init position="top-right" durationMs={2200} maxVisible={4} />` в корне.
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { Notice } from "@elcrm/notice";
|
|
9
|
+
|
|
10
|
+
Notice.Success("Готово");
|
|
11
|
+
Notice.Error("Ошибка");
|
|
12
|
+
Notice.Remark("Подсказка");
|
|
13
|
+
Notice.Copy("Скопировано");
|
|
14
|
+
Notice.Undo("Удалено", { onUndo: () => restore() });
|
|
15
|
+
Notice.Close(id);
|
|
16
|
+
Notice.CloseAll();
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Можно
|
|
20
|
+
|
|
21
|
+
- `Init`: `durationMs` (≥100), `maxVisible`, `pauseOnHover`, `undoLabel`, `icon` (false = палочка), `position`, `titles`.
|
|
22
|
+
- Второй аргумент: `boolean` hide или `{ hide, actions, … }` (до двух кнопок).
|
|
23
|
+
- `Notice.Send({ type, text, … })` / `Notice.Custom`.
|
|
24
|
+
- Методы возвращают `NoticeId`.
|
|
25
|
+
- `Form.Init onNotice={(msg) => Notice.Remark(String(msg))}` — связка с формой.
|
|
26
|
+
|
|
27
|
+
## Нельзя
|
|
28
|
+
|
|
29
|
+
- Несколько Init.
|
|
30
|
+
- `alert()` вместо тоста для немодальных сообщений (модальный вопрос — `@elcrm/alert`).
|
|
31
|
+
- `import "@elcrm/notice/themes.css"`.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<!-- Сгенерировано `elcrm docs`. Пакет: @elcrm/notifications. -->
|
|
2
|
+
|
|
3
|
+
# `@elcrm/notifications`
|
|
4
|
+
|
|
5
|
+
In-app inbox + Web Push. Сервер: `@elcrm/server` (subpath notifications), не `web-push`.
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { Notifications, bindRouterNavigate } from "@elcrm/notifications";
|
|
9
|
+
import workerUrl from "@elcrm/notifications/worker?url";
|
|
10
|
+
|
|
11
|
+
bindRouterNavigate((url) => { /* Router / pathname */ });
|
|
12
|
+
|
|
13
|
+
<Notifications.Init
|
|
14
|
+
workerUrl={workerUrl}
|
|
15
|
+
tenantId="app"
|
|
16
|
+
groupBy="module"
|
|
17
|
+
appBadge
|
|
18
|
+
autoSync
|
|
19
|
+
getVapidPublicKey={async () => (await Api.query("notifications/vapid")).publicKey}
|
|
20
|
+
onSubscribe={(sub) => Api.query("notifications/subscribe", sub)}
|
|
21
|
+
fetchPrefs={async () => (await Api.query("notifications/prefsGet")).prefs}
|
|
22
|
+
onPrefsChange={(p) => Api.query("notifications/prefsSet", p)}
|
|
23
|
+
/>
|
|
24
|
+
<Notifications.Bell />
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Можно
|
|
28
|
+
|
|
29
|
+
- Push enable/disable/sync, SW, `subscriptionchange`.
|
|
30
|
+
- Inbox: группировка `module` / `tag`.
|
|
31
|
+
- Quiet hours / mute в Bell.
|
|
32
|
+
- `navigator.setAppBadge` (`appBadge`).
|
|
33
|
+
- Навигация: `onNavigate` / `bindRouterNavigate` / событие `elcrm:navigate`.
|
|
34
|
+
|
|
35
|
+
## Нельзя
|
|
36
|
+
|
|
37
|
+
- Пакет `web-push` на сервере — `@elcrm/server/notifications`.
|
|
38
|
+
- Забывать Service Worker URL (`worker?url`).
|
|
39
|
+
- FCM/APNs SDK — этого пакета нет, свой VAPID + сервер elCRM.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<!-- Сгенерировано `elcrm docs`. Пакет: @elcrm/passkey. -->
|
|
2
|
+
|
|
3
|
+
# `@elcrm/passkey`
|
|
4
|
+
|
|
5
|
+
Клиент WebAuthn. Сервер: `@elcrm/server/passkey`.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { createPasskeyClient } from "@elcrm/passkey";
|
|
9
|
+
import { usePasskeyClient, usePasskeySupport } from "@elcrm/passkey/react";
|
|
10
|
+
|
|
11
|
+
const passkey = createPasskeyClient({ baseUrl: "https://api.example.com/v1" });
|
|
12
|
+
await passkey.register({ userId, username });
|
|
13
|
+
await passkey.login({ userId });
|
|
14
|
+
await passkey.loginDiscoverable();
|
|
15
|
+
await passkey.remove({ userId, credentialId });
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Пути: `/passkey/register|auth|discoverable|remove` + `options`/`verify`. Переопределение: `paths`, `headers`, `fetch`.
|
|
19
|
+
|
|
20
|
+
Ядро **без React**. Хуки — `@elcrm/passkey/react`. Ошибки: `PasskeyError.code`.
|
|
21
|
+
|
|
22
|
+
## Нельзя
|
|
23
|
+
|
|
24
|
+
- `createPasskey` / `authenticateWithPasskey` — **не экспортируются** (`elcrm test` error).
|
|
25
|
+
- Импорт серверного passkey на фронт.
|
|
26
|
+
- Свой fetch на `/passkey/*` в обход клиента, если нет причины.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<!-- Сгенерировано `elcrm docs`. Пакет: @elcrm/router. -->
|
|
2
|
+
|
|
3
|
+
# `@elcrm/router`
|
|
4
|
+
|
|
5
|
+
Состояние `Router.data` + опциональный URL + lazy chunk. Это **не** React Router.
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { Router, LoadRoute } from "@elcrm/router";
|
|
9
|
+
|
|
10
|
+
Router.createRouter({
|
|
11
|
+
defaults: { page: "Main", module: "Home" },
|
|
12
|
+
auth: "Auth",
|
|
13
|
+
glob: import.meta.glob("./{pages,modules}/**/*.{tsx,ts}", { eager: false }),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
<LoadRoute variant="page" defaultName="Main" resolveLazyPage={resolvePage}>
|
|
17
|
+
<span aria-hidden />
|
|
18
|
+
</LoadRoute>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Можно
|
|
22
|
+
|
|
23
|
+
- Состояние: `useRouter`, `setRouter`, `mergeRouter`, `getRouter`, `resetRouter`, `matchesRouter`.
|
|
24
|
+
- Типизация: `declare module "@elcrm/router" { interface RouterDataMap { page: "Auth"|"Main"; module: … } }`.
|
|
25
|
+
- Секции модуля: отдельный ключ (`section` / `listTab` / `accountTab`) — **не один ключ на все вкладки**.
|
|
26
|
+
- `createLazyResolver` + `import(\`./sections/${key}.tsx\`)` (`export default`).
|
|
27
|
+
- Persist sessionStorage (crypto), `urlTemplate` по желанию.
|
|
28
|
+
- `GuardRoute`, `RouteLoader`, `RouterOutletStack`.
|
|
29
|
+
|
|
30
|
+
## Нельзя
|
|
31
|
+
|
|
32
|
+
- `react-router` / `window.location` как основной роутер приложения.
|
|
33
|
+
- Смешивать ключи вкладок разных модулей (после смены таба откроется чужая lazy-секция).
|
|
34
|
+
- Нормализовать регистр сегментов самостоятельно, если URL-слой включён — библиотека не нижний регистр.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<!-- Сгенерировано `elcrm docs`. Источник: @elcrm/server + elcrm test (server). Обновление: снова elcrm docs. -->
|
|
2
|
+
|
|
3
|
+
# `@elcrm/server`
|
|
4
|
+
|
|
5
|
+
Bun HTTP/RPC. Одна зависимость `@elcrm/server`. Клиент: `@elcrm/api` + `@elcrm/socket`.
|
|
6
|
+
|
|
7
|
+
## Можно
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { createServer, createRouter, rpcOk, rpcLogout } from "@elcrm/server";
|
|
11
|
+
import { initDB, db } from "@elcrm/server/db";
|
|
12
|
+
import { createJWT } from "@elcrm/server/jwt";
|
|
13
|
+
import { cron } from "@elcrm/server/cron";
|
|
14
|
+
import { createSocket, sendSocket } from "@elcrm/server/ws";
|
|
15
|
+
|
|
16
|
+
createRouter("health").use("ping", async () => rpcOk({ ok: true }));
|
|
17
|
+
|
|
18
|
+
createSocket("ws")
|
|
19
|
+
.use("auth", async ({ user }) => ({ id: String(user.id) }));
|
|
20
|
+
|
|
21
|
+
cron.use(async () => { /* … */ }, 60_000, "name");
|
|
22
|
+
|
|
23
|
+
await initDB({ type: "sqlite", database: DB_PATH }); // каталог создаёт openSqlite
|
|
24
|
+
await createServer({ port, prefix: "/v1", jwt, cron: true });
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Path-RPC: `/{prefix}/{router}/{method}`. Сессия: `createServer({ jwt })` кладёт `user` — в handler не звать `jwt.init` повторно. Гейт: middleware `if (!user?.id) return rpcLogout()`.
|
|
28
|
+
|
|
29
|
+
Env: `TOKEN` / `JWT_SECRET`, `CORS_ORIGINS` (origins **без path**, через запятую), `PORT`. Секреты: `bunx elcrm-server env seal` → `.env.crypto`.
|
|
30
|
+
|
|
31
|
+
Файлы: `Bun.file` / `Bun.write`. Почта: `@elcrm/server/mail`. Push: `@elcrm/server/notifications`.
|
|
32
|
+
|
|
33
|
+
Проверка: `elcrm test --server` или `bunx elcrm-server test .`. Автозамены: `bunx elcrm-server update`.
|
|
34
|
+
|
|
35
|
+
## Нельзя
|
|
36
|
+
|
|
37
|
+
| Было | Стало |
|
|
38
|
+
| --- | --- |
|
|
39
|
+
| `@elcrm/db` `@elcrm/jwt` `@elcrm/cron` `@elcrm/tb` | `@elcrm/server/db` `/jwt` `/cron` `/tb` |
|
|
40
|
+
| `createServerFromEnv` | `createServer()` |
|
|
41
|
+
| `createJwtFromEnv` / `createCronFromEnv` / `createTbFromEnv` | `createJWT` / `createCron` / `createTb` |
|
|
42
|
+
| `createWsHub` / `serverConnect` | `createSocket("ws").use(…)` |
|
|
43
|
+
| `hub.sendListen` | `sendSocket` |
|
|
44
|
+
| `Cron.useJob` / namespace `Cron` | `cron.use` |
|
|
45
|
+
| REST `path === "/api/…"` / `handle*(ctx): Promise<Response>` | `createRouter().use()` |
|
|
46
|
+
| `nodemailer` / `web-push` | `@elcrm/server/mail` / `notifications` |
|
|
47
|
+
| `process.env.CORS_ORIGIN` | `CORS_ORIGINS` |
|
|
48
|
+
| `import { mkdirSync } from "node:fs"` для SQLite | `initDB` сам создаёт каталог |
|
|
49
|
+
| `from "node:path"` в app | строки путей / Bun |
|
|
50
|
+
|
|
51
|
+
Не светить `err.message` клиенту — throw даёт `{ status:"error", data:"internal" }` + 500; детали только в лог.
|
|
52
|
+
|
|
53
|
+
Структура API (panel): `src/index.ts` (createServer) · `src/routes/*.ts` (createRouter) · `src/lib/db.ts` · `jwt.ts` · `cron.ts` · `config.ts`.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<!-- Сгенерировано `elcrm docs`. Пакет: @elcrm/socket (клиент). -->
|
|
2
|
+
|
|
3
|
+
# `@elcrm/socket`
|
|
4
|
+
|
|
5
|
+
Клиент SharedWorker WebSocket. Сервер: `@elcrm/server/ws` (`createSocket`), **не** этот пакет.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { Socket } from "@elcrm/socket";
|
|
9
|
+
|
|
10
|
+
Socket.configure({ storageKey: "elcrm-token" }); // тот же ключ, что Api ssid
|
|
11
|
+
await Socket.createFromApiUrl(apiUrl, ukey, "/ws");
|
|
12
|
+
await Socket.connect(ukey);
|
|
13
|
+
Socket.send("inbox", { … });
|
|
14
|
+
await Socket.fetch("inbox", { … });
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
React: `useConnect` из `@elcrm/socket/react` — сначала `create`/`createFromApiUrl`.
|
|
18
|
+
|
|
19
|
+
| Метод | |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| `create` / `createFromApiUrl` | сохранить link (шифруется в localStorage) |
|
|
22
|
+
| `connect` / `detach` / `disconnect` | вкладка / все вкладки |
|
|
23
|
+
| `send(path, data)` | path-RPC |
|
|
24
|
+
| `sendRaw` / `onMessage` / `onStatus` | сырой канал |
|
|
25
|
+
| `fetch` | Promise + listen |
|
|
26
|
+
| `supported` / `isConnected` | |
|
|
27
|
+
|
|
28
|
+
`ukey` = ssid HTTP. Worker: `@elcrm/socket/worker?sharedworker&url` при отдельном подключении.
|
|
29
|
+
|
|
30
|
+
## Нельзя
|
|
31
|
+
|
|
32
|
+
- `serverConnect` / `createWsHub` / `from "@elcrm/socket/server"` / `@elcrm/socket-server` — `elcrm migrate socket-server`.
|
|
33
|
+
- На сервере импортировать `@elcrm/socket` для hub.
|
|
34
|
+
- Другой path WS, чем у `createSocket` на API (часто `/ws`).
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<!-- Сгенерировано `elcrm docs`. Пакет: @elcrm/tooltip. -->
|
|
2
|
+
|
|
3
|
+
# `@elcrm/tooltip`
|
|
4
|
+
|
|
5
|
+
Подсказка в portal, общий overlay z-index. Не нативный `title`.
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { Tooltip } from "@elcrm/tooltip";
|
|
9
|
+
|
|
10
|
+
<Tooltip content="Подсказка" placement="top">
|
|
11
|
+
<Button label="…" />
|
|
12
|
+
</Tooltip>
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Можно
|
|
16
|
+
|
|
17
|
+
- `placement`: `top` \| `bottom` \| `left` \| `right` (не `orientation` — deprecated).
|
|
18
|
+
- `delay` / `delayHide`, `disabled`, `textAlign`, `open` (отладка).
|
|
19
|
+
- Токены `--tooltip-*` в теме.
|
|
20
|
+
- z-index сам: `acquireOverlayZIndex` вручную не нужен.
|
|
21
|
+
|
|
22
|
+
## Нельзя
|
|
23
|
+
|
|
24
|
+
- Атрибут `title` на контролах UI как единственная подсказка.
|
|
25
|
+
- Свой `z-index: 99999` на оверлеях — конфликт с modal/form/alert/notice.
|
|
@@ -9,36 +9,12 @@ bun install
|
|
|
9
9
|
bun run dev
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
- Web: http://localhost:5173
|
|
12
|
+
- Web: http://localhost:5173
|
|
13
13
|
- API: http://localhost:4545 (`/v1/...`)
|
|
14
14
|
|
|
15
15
|
Демо-вход: `admin` / `admin`
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
Пакет ещё не опубликован в npm. Локально:
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
# 1) в репозитории библиотеки
|
|
23
|
-
cd /path/to/elCRM.components
|
|
24
|
-
bun run build
|
|
25
|
-
bun link
|
|
26
|
-
|
|
27
|
-
# 2) в panel (корень workspaces)
|
|
28
|
-
bun install # остальные @elcrm/* подтянутся; components можно пропустить/упасть — ок
|
|
29
|
-
|
|
30
|
-
# 3) привязать shell UI
|
|
31
|
-
cd web && bun link @elcrm/components && cd ..
|
|
32
|
-
bun install
|
|
33
|
-
bun run dev
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
Публикация (нужен OTP 2FA):
|
|
37
|
-
|
|
38
|
-
```bash
|
|
39
|
-
cd /path/to/elCRM.components
|
|
40
|
-
npm publish --access public --otp=XXXXXX
|
|
41
|
-
```
|
|
17
|
+
Зависимость shell UI: `@elcrm/components@0.0.1` (npm).
|
|
42
18
|
|
|
43
19
|
## Структура
|
|
44
20
|
|
|
@@ -9,11 +9,8 @@
|
|
|
9
9
|
"start": "bun run src/index.ts"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@elcrm/
|
|
13
|
-
"@elcrm/
|
|
14
|
-
"@elcrm/deploy": "^0.0.10",
|
|
15
|
-
"@elcrm/jwt": "^0.1.1",
|
|
16
|
-
"@elcrm/server": "^0.5.3"
|
|
12
|
+
"@elcrm/server": "^0.6.2",
|
|
13
|
+
"@elcrm/deploy": "^0.0.10"
|
|
17
14
|
},
|
|
18
15
|
"devDependencies": {
|
|
19
16
|
"@types/bun": "latest"
|
|
@@ -1,41 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* panel.api — @elcrm/server
|
|
2
|
+
* panel.api — DX как в @elcrm/server README:
|
|
3
|
+
* createRouter + createServer; JWT/CORS/cron из конфига без дублей.
|
|
3
4
|
*/
|
|
4
5
|
import { createServer, cors as serverCors } from "@elcrm/server";
|
|
5
|
-
import { DB_PATH, getCorsOrigin, PORT } from "./config";
|
|
6
|
+
import { DB_PATH, getCorsOrigin, PORT, TOKEN } from "./config";
|
|
6
7
|
import { initDatabase } from "./lib/db";
|
|
7
8
|
import { jwt } from "./lib/jwt";
|
|
8
|
-
import
|
|
9
|
+
import "./lib/cron";
|
|
9
10
|
import { registerRoutes } from "./routes/index";
|
|
10
11
|
|
|
11
|
-
async function
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if (method === "GET" && (path === "/health" || path === "/v1/health")) {
|
|
17
|
-
return Response.json({
|
|
18
|
-
ok: true,
|
|
19
|
-
service: "panel.api",
|
|
20
|
-
version: process.env.APP_VERSION ?? null,
|
|
21
|
-
});
|
|
12
|
+
async function start() {
|
|
13
|
+
// TOKEN в env — createJWT/createServer видят один секрет
|
|
14
|
+
if (!process.env.TOKEN?.trim()) {
|
|
15
|
+
process.env.TOKEN = TOKEN;
|
|
22
16
|
}
|
|
23
17
|
|
|
24
|
-
return null;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
async function start() {
|
|
28
18
|
await initDatabase();
|
|
29
|
-
runCron();
|
|
30
19
|
registerRoutes();
|
|
31
20
|
|
|
32
21
|
try {
|
|
33
22
|
createServer({
|
|
34
23
|
port: PORT,
|
|
35
24
|
prefix: "/v1",
|
|
25
|
+
// alias + password hash — явный jwt; иначе хватило бы omit + TOKEN
|
|
36
26
|
jwt,
|
|
27
|
+
// auth в exclude: pass без ssid; me/profile — middleware requireUser (user уже из createServer)
|
|
37
28
|
jwtAuth: { bearer: false },
|
|
38
29
|
securityHeaders: true,
|
|
30
|
+
cron: true,
|
|
39
31
|
cors: serverCors({
|
|
40
32
|
origin: getCorsOrigin(),
|
|
41
33
|
credentials: true,
|
|
@@ -56,7 +48,6 @@ async function start() {
|
|
|
56
48
|
err instanceof Error ? err.message : err,
|
|
57
49
|
);
|
|
58
50
|
},
|
|
59
|
-
beforeFetch,
|
|
60
51
|
});
|
|
61
52
|
} catch (e) {
|
|
62
53
|
const msg = e instanceof Error ? e.message : String(e);
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Минимальный cron
|
|
2
|
+
* Минимальный cron — heartbeat. createServer({ cron: true }) / CRON_AUTORUN=1
|
|
3
|
+
* подхватит cron.run() сам.
|
|
3
4
|
*/
|
|
4
|
-
import {
|
|
5
|
+
import { cron } from "@elcrm/server/cron";
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
cron.use(
|
|
8
|
+
() => {
|
|
9
|
+
console.log(`[cron] heartbeat ${new Date().toISOString()}`);
|
|
10
|
+
},
|
|
11
|
+
"0 */10 * * * *",
|
|
12
|
+
"heartbeat",
|
|
13
|
+
);
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SQLite через @elcrm/db + схема users и демо-админ.
|
|
2
|
+
* SQLite через @elcrm/server/db + схема users и демо-админ.
|
|
3
|
+
* Каталог для файла БД создаёт сам openSqlite (ensureDir) — node:fs не нужен.
|
|
3
4
|
*/
|
|
4
|
-
import {
|
|
5
|
-
import { dirname } from "node:path";
|
|
6
|
-
import { db, initDB } from "@elcrm/db";
|
|
5
|
+
import { db, initDB } from "@elcrm/server/db";
|
|
7
6
|
import { DB_PATH } from "../config";
|
|
8
7
|
import { jwt } from "./jwt";
|
|
9
8
|
|
|
@@ -12,7 +11,6 @@ let ready: Promise<typeof db> | null = null;
|
|
|
12
11
|
export async function initDatabase() {
|
|
13
12
|
if (!ready) {
|
|
14
13
|
ready = (async () => {
|
|
15
|
-
mkdirSync(dirname(DB_PATH), { recursive: true });
|
|
16
14
|
await initDB({ type: "sqlite", database: DB_PATH });
|
|
17
15
|
const ok = await db.ping();
|
|
18
16
|
if (!ok) throw new Error(`SQLite недоступна: ${DB_PATH}`);
|