@helfy/helfy 0.0.6 → 0.0.8

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 CHANGED
@@ -1,52 +1,72 @@
1
1
  # Helfy
2
2
 
3
- Helfy -- TypeScript UI-фреймворк с декоратор-ориентированным API, кастомным JSX и fine-grained reactivity.
3
+ Helfy a TypeScript UI framework with a decorator-oriented API, custom JSX, and fine-grained reactivity.
4
4
 
5
- Компоненты описываются классами с декоратором `@View`, состояние управляется через `@state` и `@observable`, а шаблоны используют расширенный JSX с директивами `@if`, `@for`, `@else`. Реактивность построена на signals (аналог SolidJS): `render()` вызывается один раз, а последующие обновления DOM происходят точечно только для тех узлов, которые читают изменившийся signal.
5
+ Components are defined as classes with the `@View` decorator, state is managed via `@state` and `@observable`, and templates use extended JSX with `@if`, `@for`, and `@else` directives. Reactivity is built on signals (similar to SolidJS): `render()` runs once, and subsequent DOM updates are granularonly nodes that read the changed signal are updated.
6
6
 
7
- ## Содержание
7
+ ## Table of contents
8
8
 
9
- - [Установка](#установка)
10
- - [Компоненты](#компоненты)
11
- - [Базовый компонент](#базовый-компонент)
9
+ - [Installation](#installation)
10
+ - [Components](#components)
11
+ - [Basic component](#basic-component)
12
12
  - [Props](#props)
13
- - [Локальное состояние (@state)](#локальное-состояние-state)
14
- - [Вложенные компоненты](#вложенные-компоненты)
15
- - [Монтирование в DOM](#монтирование-в-dom)
16
- - [Привязка к DOM и компонентам (@ref)](#привязка-к-dom-и-компонентам-ref)
17
- - [Двустороннее связывание (@bind)](#двустороннее-связывание-bind)
18
- - [Формы (@Form, @field, @field в JSX)](#формы-form-field-field-в-jsx)
19
- - [Шаблонные директивы](#шаблонные-директивы)
13
+ - [Local state (@state)](#local-state-state)
14
+ - [Nested components](#nested-components)
15
+ - [DOM mounting](#dom-mounting)
16
+ - [DOM and component refs (@ref)](#dom-and-component-refs-ref)
17
+ - [Two-way binding (@bind)](#two-way-binding-bind)
18
+ - [Forms (@Form, @field, @field in JSX)](#forms-form-field-field-in-jsx)
19
+ - [Template directives](#template-directives)
20
20
  - [@if / @elseif / @else](#if--elseif--else)
21
21
  - [@for](#for)
22
22
  - [@empty](#empty)
23
23
  - [State Management](#state-management)
24
- - [Создание Store](#создание-store)
25
- - [Подписка через @observe](#подписка-через-observe)
26
- - [Контекст хранилищ](#контекст-хранилищ)
27
- - [Логирование (@logger)](#логирование-logger)
24
+ - [Creating a Store](#creating-a-store)
25
+ - [Subscribing with @observe](#subscribing-with-observe)
26
+ - [Store context](#store-context)
27
+ - [Logging (@logger)](#logging-logger)
28
28
  - [Context & DI](#context--di)
29
- - [Провайдер (@Context, @provide)](#провайдер-context-provide)
30
- - [Потребитель (@ctx)](#потребитель-ctx)
31
- - [Реактивные поля](#реактивные-поля)
32
- - [Опциональная инжекция](#опциональная-инжекция)
33
- - [DI Container](#di-container-конструкторный-инжект)
29
+ - [Provider (@Context, @provide)](#provider-context-provide)
30
+ - [Consumer (@ctx)](#consumer-ctx)
31
+ - [Reactive fields](#reactive-fields)
32
+ - [Optional injection](#optional-injection)
33
+ - [DI Container](#di-container-constructor-injection)
34
34
  - [JSX](#jsx)
35
- - [Атрибуты](#атрибуты)
36
- - [События](#события)
37
- - [Стили](#стили)
38
- - [CSS-классы](#css-классы)
35
+ - [Attributes](#attributes)
36
+ - [Events](#events)
37
+ - [Styles](#styles)
38
+ - [CSS classes](#css-classes)
39
39
  - [API](#api)
40
40
 
41
41
  ---
42
42
 
43
- ## Установка
43
+ ## Installation
44
+
45
+ ### Creating a new project (helfy-create)
46
+
47
+ Quick start — create a project with a single command:
48
+
49
+ ```bash
50
+ npx helfy-create my-app
51
+ cd my-app
52
+ npm run dev
53
+ ```
54
+
55
+ Without a project name (creates `helfy-app`):
56
+
57
+ ```bash
58
+ npx helfy-create
59
+ ```
60
+
61
+ The `--skip-install` option — create without running `npm install` (e.g. for offline use or custom registry).
62
+
63
+ ### Adding to an existing project
44
64
 
45
65
  ```bash
46
66
  npm install @helfy/helfy
47
67
  ```
48
68
 
49
- или в `package.json`:
69
+ Or in `package.json`:
50
70
 
51
71
  ```json
52
72
  {
@@ -58,11 +78,11 @@ npm install @helfy/helfy
58
78
 
59
79
  ### Build setup
60
80
 
61
- Babel-плагин автоматически запускает DI-сканер перед трансформацией (pre-скрипты в `package.json` не нужны). Сканер генерирует `.helfy/di-tokens.ts`, `.helfy/di-registry.ts`. Добавьте `.helfy` в `.gitignore`.
81
+ The Babel plugin runs the DI scanner before transformation (no pre-scripts in `package.json` needed). The scanner generates `.helfy/di-tokens.ts` and `.helfy/di-registry.ts`. Add `.helfy` to `.gitignore`.
62
82
 
63
83
  ### Babel
64
84
 
65
- В `.babelrc` достаточно одного пресета:
85
+ A single preset in `.babelrc` is enough:
66
86
 
67
87
  ```json
68
88
  {
@@ -70,11 +90,11 @@ Babel-плагин автоматически запускает DI-сканер
70
90
  }
71
91
  ```
72
92
 
73
- Пресет включает: JSX runtime, TypeScript, legacy-декораторы, class properties, babel-plugin-transform-typescript-metadata, **helfy-di** (compile-time DI для `@Injectable<IX>()`, автоинъекция регистрации при `createApp`, трансформация `@logger()` → `@logger("<ClassName>")`).
93
+ The preset includes: JSX runtime, TypeScript, legacy decorators, class properties, babel-plugin-transform-typescript-metadata, **helfy-di** (compile-time DI for `@Injectable<IX>()`, auto-registration at `createApp`, `@logger()` → `@logger("<ClassName>")` transformation).
74
94
 
75
95
  ### Webpack
76
96
 
77
- Для обработки директив (`@if`, `@for`, `@ref`, `@bind`, `@field`) в `.tsx` файлах подключить loader:
97
+ To process directives (`@if`, `@for`, `@ref`, `@bind`, `@field`) in `.tsx` files, add the loader:
78
98
 
79
99
  ```javascript
80
100
  {
@@ -88,7 +108,7 @@ Babel-плагин автоматически запускает DI-сканер
88
108
 
89
109
  ### TypeScript
90
110
 
91
- В `tsconfig.json` указать:
111
+ In `tsconfig.json` set:
92
112
 
93
113
  ```json
94
114
  {
@@ -96,20 +116,20 @@ Babel-плагин автоматически запускает DI-сканер
96
116
  "jsx": "preserve",
97
117
  "experimentalDecorators": true,
98
118
  "emitDecoratorMetadata": true,
99
- "plugins": [{ "name": "@helfy/helfy/ts-plugin" }]
119
+ "plugins": [{ "name": "@helfy/helfy-ts-plugin" }]
100
120
  }
101
121
  }
102
122
  ```
103
123
 
104
- Плагин `@helfy/helfy/ts-plugin` даёт поддержку директив `@if`, `@for`, `@ref`, `@bind`, `@field` в IDE (автодополнение, типизация, переход к определению). Идёт в комплекте с пакетом helfy.
124
+ The `@helfy/helfy-ts-plugin` plugin provides IDE support for `@if`, `@for`, `@ref`, `@bind`, `@field` directives (autocomplete, typing, go-to-definition). Install separately: `npm install @helfy/helfy-ts-plugin`.
105
125
 
106
126
  ---
107
127
 
108
- ## Компоненты
128
+ ## Components
109
129
 
110
- ### Базовый компонент
130
+ ### Basic component
111
131
 
112
- Компонент -- это класс с декоратором `@View` и методом `render()`, возвращающим JSX:
132
+ A component is a class with the `@View` decorator and a `render()` method that returns JSX:
113
133
 
114
134
  ```tsx
115
135
  import { View } from "@helfy/helfy";
@@ -126,19 +146,19 @@ class Hello {
126
146
  }
127
147
  ```
128
148
 
129
- Декоратор `@View` автоматически:
130
- - вызывает `render()` **один раз** и монтирует результат в DOM-фрагмент
131
- - добавляет свойство `view` (ссылка на корневой DOM-элемент)
132
- - оборачивает `this.props` в реактивный Proxy (каждый prop signal)
133
- - настраивает fine-grained эффекты: при изменении signal обновляется только конкретный DOM-узел, а не весь компонент
149
+ The `@View` decorator automatically:
150
+ - calls `render()` **once** and mounts the result into a DOM fragment
151
+ - adds a `view` property (reference to the root DOM element)
152
+ - wraps `this.props` in a reactive Proxy (each prop is a signal)
153
+ - configures fine-grained effects: when a signal changes, only the specific DOM node that reads it is updated, not the entire component
134
154
 
135
- > **Важно:** При наследовании `@View` нужно указывать и на дочернем классе. Без этого не будут работать `@ctx`, `scheduleUpdate` и обновление DOM.
155
+ > **Important:** When inheriting from `@View`, apply the decorator on the child class as well. Otherwise `@ctx`, `scheduleUpdate`, and DOM updates will not work.
136
156
 
137
157
  ### Props
138
158
 
139
- Props передаются через конструктор. Декоратор `@View` оборачивает `this.props` в реактивный Proxy — каждый prop становится signal. Это означает, что при изменении props родителем обновляются только те DOM-узлы, которые их читают, без полного перерендера компонента.
159
+ Props are passed via the constructor. The `@View` decorator wraps `this.props` in a reactive Proxy — each prop becomes a signal. This means that when the parent updates props, only the DOM nodes that read them are updated, without a full re-render.
140
160
 
141
- > **Важно:** Не деструктуризируйте `this.props` в `render()`. Обращайтесь к полям напрямую через `this.props.field` — это обеспечивает корректную подписку на signals.
161
+ > **Important:** Do not destructure `this.props` in `render()`. Access fields directly via `this.props.field` — this ensures correct signal subscription.
142
162
 
143
163
  ```tsx
144
164
  import { View } from "@helfy/helfy";
@@ -166,18 +186,17 @@ class Button {
166
186
  }
167
187
  ```
168
188
 
169
- Использование:
189
+ Usage:
170
190
 
171
191
  ```tsx
172
- <Button label="Нажми" onClick={() => console.log('clicked')} />
192
+ <Button label="Click" onClick={() => console.log('clicked')} />
173
193
  ```
174
194
 
175
- При изменении props родителем `updateProps()` обновляет соответствующие signals через `batch()`. Fine-grained эффекты, созданные Babel-плагином (`createReactiveChild`), отслеживают эти signals и точечно обновляют DOM.
195
+ When the parent updates props, only the DOM nodes that read the changed props are updated.
176
196
 
177
- ### Локальное состояние (@state)
197
+ ### Local state (@state)
178
198
 
179
- Декоратор `@state` делает поле реактивным под капотом каждое поле это signal из `createSignal()`.
180
- При изменении значения обновляются только те DOM-узлы и эффекты, которые читают это поле (fine-grained). Полный перерендер компонента не происходит:
199
+ The `@state` decorator makes a field reactive. When its value changes, only the DOM nodes and effects that read it are updated (fine-grained). The component is not fully re-rendered:
181
200
 
182
201
  ```tsx
183
202
  import { View, state } from "@helfy/helfy";
@@ -187,7 +206,7 @@ class Counter {
187
206
  @state private count = 0;
188
207
 
189
208
  increment() {
190
- this.count++; // обновит только узлы, читающие count
209
+ this.count++; // updates only nodes that read count
191
210
  }
192
211
 
193
212
  decrement() {
@@ -206,13 +225,13 @@ class Counter {
206
225
  }
207
226
  ```
208
227
 
209
- Можно объявить несколько `@state`-полей. Каждое из них независимо триггерит обновление только тех узлов DOM, которые его читают.
228
+ You can declare multiple `@state` fields. Each independently triggers updates only in the DOM nodes that read it.
210
229
 
211
- Signals‑примитивы (`createSignal`, `createEffect`, `createComputed`, `batch`, `onCleanup`) также экспортируются из `helfy` и могут использоваться напрямую, но в большинстве случаев достаточно `@state`, `@computed` и `@effect`.
230
+ Signal primitives (`createSignal`, `createEffect`, `createComputed`, `batch`, `onCleanup`) are also exported from `helfy` and can be used directly, but in most cases `@state`, `@computed`, and `@effect` are sufficient.
212
231
 
213
- ### Вложенные компоненты
232
+ ### Nested components
214
233
 
215
- Компоненты используются в JSX как теги. Props передаются как атрибуты:
234
+ Components are used in JSX as tags. Props are passed as attributes:
216
235
 
217
236
  ```tsx
218
237
  import { View } from "@helfy/helfy";
@@ -230,20 +249,31 @@ class App {
230
249
  }
231
250
  ```
232
251
 
233
- ### Монтирование в DOM
252
+ ### DOM mounting
253
+
254
+ Typical bootstrap is via `createApp()`: an instance is created, `attach(root)` is called internally, and the `onAttached()` hook runs after insertion into the document.
255
+
256
+ ```typescript
257
+ import { createApp } from "@helfy/helfy";
258
+
259
+ createApp({ root: document.getElementById("root")! })
260
+ .router({ routes })
261
+ .mount(App);
262
+ ```
234
263
 
235
- Корневой компонент монтируется через `attach()`, чтобы вызвать хук `onAttached()` (после вставки в document):
264
+ For scenarios without routing:
236
265
 
237
266
  ```typescript
238
- const app = new App();
239
- app.attach(document.getElementById('root'));
267
+ createApp({ root: document.getElementById("root")! }).mount(App);
240
268
  ```
241
269
 
242
- ### Привязка к DOM и компонентам (@ref)
270
+ Manual mounting (without `createApp`): `const app = new App(); app.attach(root)`.
243
271
 
244
- Декоратор `@ref` помечает поле для получения ссылки на DOM-элемент или экземпляр компонента. В JSX используется директива `@ref(this.имяПоля)`. Доступ к ссылке возможен после `onMount()`. Для операций вроде `focus()`, требующих элемент в document, используйте `onAttached()`.
272
+ ### DOM and component refs (@ref)
245
273
 
246
- **Для компонентов** родитель получает прокси с доступом только к методам, помеченным `@expose`. Это позволяет явно определить публичный API компонента.
274
+ The `@ref` decorator marks a field to receive a reference to a DOM element or component instance. In JSX use the `@ref(this.fieldName)` directive. The reference is available after `onMount()`. For operations like `focus()` that require the element in the document, use `onAttached()`.
275
+
276
+ **For components** — the parent receives a proxy with access only to methods marked with `@expose`. This lets you explicitly define the component's public API.
247
277
 
248
278
  ```tsx
249
279
  import { View, ref, expose } from "@helfy/helfy";
@@ -266,7 +296,7 @@ class Form {
266
296
  @ref private input!: Input;
267
297
 
268
298
  onAttached() {
269
- this.input.focus(); // доступен только помеченный метод
299
+ this.input.focus(); // only the exposed method is available
270
300
  }
271
301
 
272
302
  render() {
@@ -275,15 +305,15 @@ class Form {
275
305
  }
276
306
  ```
277
307
 
278
- - DOM-элементы по `@ref` передаются как есть
279
- - Компоненты с `@expose` — родитель видит только помеченные методы
280
- - Компоненты без `@expose` — передаётся экземпляр целиком (обратная совместимость)
308
+ - DOM elements via `@ref` are passed as-is
309
+ - Components with `@expose` — parent sees only the exposed methods
310
+ - Components without `@expose` — the full instance is passed (backward compatibility)
281
311
 
282
- При условном рендере (`@if`) ссылка обнуляется при удалении элемента. Запрещено использовать `@ref` и `@state` на одном поле.
312
+ With conditional rendering (`@if`), the ref is cleared when the element is removed. Do not use `@ref` and `@state` on the same field.
283
313
 
284
- ### Двустороннее связывание (@bind)
314
+ ### Two-way binding (@bind)
285
315
 
286
- Директива `@bind` синтаксический сахар для двусторонней привязки значения поля `@state` к элементу формы. Синтаксис: `@bind(expr)` — круглые скобки, как у `@ref`. Компилятор автоматически генерирует пару `value`/`checked` и обработчик события.
316
+ The `@bind` directive is syntactic sugar for two-way binding of a `@state` field to a form element. Syntax: `@bind(expr)` — parentheses, like `@ref`. The compiler generates the `value`/`checked` pair and event handler.
287
317
 
288
318
  ```tsx
289
319
  import { View, state } from "@helfy/helfy";
@@ -301,8 +331,8 @@ class LoginForm {
301
331
  <input @bind(this.email) type="email" />
302
332
  <input @bind(this.isActive) type="checkbox" />
303
333
  <select @bind(this.priority)>
304
- <option value="low">Низкий</option>
305
- <option value="high">Высокий</option>
334
+ <option value="low">Low</option>
335
+ <option value="high">High</option>
306
336
  </select>
307
337
  </form>
308
338
  );
@@ -310,24 +340,24 @@ class LoginForm {
310
340
  }
311
341
  ```
312
342
 
313
- Трансформация по типу элемента:
343
+ Transformation by element type:
314
344
 
315
- | Элемент | Атрибут | Событие |
316
- |---------|---------|---------|
345
+ | Element | Attribute | Event |
346
+ |---------|-----------|-------|
317
347
  | `input[text|email|password|...]` | `value` | `oninput` |
318
348
  | `input[checkbox|radio]` | `checked` | `onchange` |
319
349
  | `select` | `value` | `onchange` |
320
350
  | `textarea` | `value` | `oninput` |
321
351
 
322
- Поле должно быть помечено `@state` для реактивного обновления UI. Для union-типов (например, `TodoPriority`) используется приведение `as typeof expr` — типобезопасность сохраняется.
352
+ The field must be marked with `@state` for reactive UI updates. For union types (e.g. `TodoPriority`), use `as typeof expr` — type safety is preserved.
323
353
 
324
- **Кастомные компоненты:** для привязки к компоненту с пропом `value` используется именованный биндинг `@bind:value(expr)`:
354
+ **Custom components:** for binding to a component with a `value` prop, use named binding `@bind:value(expr)`:
325
355
 
326
356
  ```tsx
327
- // Родитель
328
- <Input @bind:value(this.title) placeholder="Заголовок" />
357
+ // Parent
358
+ <Input @bind:value(this.title) placeholder="Title" />
329
359
 
330
- // Компонент Input с @binded("value")
360
+ // Input component with @binded("value")
331
361
  @View
332
362
  class Input {
333
363
  @binded("value") private bindedVal!: string;
@@ -338,11 +368,11 @@ class Input {
338
368
  }
339
369
  ```
340
370
 
341
- ### Формы (@Form, @field, @field в JSX)
371
+ ### Forms (@Form, @field, @field in JSX)
342
372
 
343
- Централизованное управление формами с валидацией через контекст: `@Form` — класс контекста формы, `@field` — декоратор полей (создаёт `FieldState`), `@useForm` — инжект формы в компонент. JSX-директива `@field(expr)` подключает инпут к `FieldState` одной строкой.
373
+ Centralized form handling with validation via context: `@Form` — form context class, `@field` — field decorator (creates `FieldState`), `@useForm` — injects the form into a component. The JSX directive `@field(expr)` connects an input to `FieldState` in one line.
344
374
 
345
- **FormContext** — класс с `@Form` и полями `@field`:
375
+ **FormContext** — a class with `@Form` and `@field` fields:
346
376
 
347
377
  ```tsx
348
378
  import { Form, field, logger, type FieldState, type ILogger } from "@helfy/helfy";
@@ -361,7 +391,7 @@ export class LoginFormContext {
361
391
  rememberMe!: FieldState<boolean>;
362
392
 
363
393
  validateAll(): boolean {
364
- // проверка полей, установка field.error
394
+ // validate fields, set field.error
365
395
  return true;
366
396
  }
367
397
 
@@ -375,9 +405,25 @@ export class LoginFormContext {
375
405
  }
376
406
  ```
377
407
 
378
- **FieldState** содержит `value`, `isDirty`, `isTouched`, `error`, `isValid`. При изменении `value`/`error`/`isTouched` компоненты с `@useForm` перерендериваются.
408
+ **FieldState** has `value`, `isDirty`, `isTouched`, `error`, `isValid`. When `value`/`error`/`isTouched` changes, components with `@useForm` re-render.
409
+
410
+ **Form provider** — the parent component wraps the form in context:
411
+
412
+ ```tsx
413
+ // LoginPage.tsx
414
+ @View
415
+ export class LoginPage {
416
+ render() {
417
+ return (
418
+ <LoginFormContext>
419
+ <LoginForm />
420
+ </LoginFormContext>
421
+ );
422
+ }
423
+ }
424
+ ```
379
425
 
380
- **Компонент формы**инжект через `@useForm`, рендер через провайдер:
426
+ **Form component**inject via `@useForm`, access fields via `this.form`:
381
427
 
382
428
  ```tsx
383
429
  import { View, useForm } from "@helfy/helfy";
@@ -389,79 +435,79 @@ export class LoginForm {
389
435
 
390
436
  render() {
391
437
  return (
392
- <LoginFormContext>
393
- <form onsubmit={(e: Event) => { e.preventDefault(); this.form.submit(); }}>
394
- <input @field(this.form.email) type="email" class="input" />
395
- {this.form.email.isTouched && this.form.email.error && (
396
- <span class="error">{this.form.email.error}</span>
397
- )}
398
- <input @field(this.form.password) type="password" class="input" />
399
- <input @field(this.form.rememberMe) type="checkbox" id="remember" />
400
- <label for="remember">Запомнить меня</label>
401
- <button type="submit">Войти</button>
402
- </form>
403
- </LoginFormContext>
438
+ <form onsubmit={(e: Event) => { e.preventDefault(); this.form.submit(); }}>
439
+ <input @field(this.form.email) type="email" class="input" />
440
+ {this.form.email.isTouched && this.form.email.error && (
441
+ <span class="error">{this.form.email.error}</span>
442
+ )}
443
+ <input @field(this.form.password) type="password" class="input" />
444
+ <input @field(this.form.rememberMe) type="checkbox" id="remember" />
445
+ <label for="remember">Remember me</label>
446
+ <button type="submit">Sign in</button>
447
+ </form>
404
448
  );
405
449
  }
406
450
  }
407
451
  ```
408
452
 
409
- **JSX-директива `@field(expr)`** одна директива заменяет `@bind` + `onblur` + `class` для ошибок + `aria-invalid`. Компилятор генерирует:
453
+ Wrapper components (TextField, CheckboxField, etc.) accept `field` as a prop: `<TextField field={this.form.email} label="Email" />` and use `<input @field(this.props.field) />` internally.
454
+
455
+ **JSX directive `@field(expr)`** — one directive replaces `@bind` + `onblur` + error `class` + `aria-invalid`. The compiler generates:
410
456
 
411
- - `value`/`checked` и `oninput`/`onchange`
457
+ - `value`/`checked` and `oninput`/`onchange`
412
458
  - `onblur` → `expr.isTouched = true`
413
- - `class` — объединение с существующим; при `expr.isTouched && expr.error` добавляется `input-error`
459
+ - `class` — merged with existing; when `expr.isTouched && expr.error` adds `input-error`
414
460
  - `aria-invalid={expr.isTouched && expr.error ? "true" : "false"}`
415
461
 
416
- Поддерживаются `input` (text, email, password, checkbox, radio), `select`, `textarea`. Класс `.input-error` можно задать в глобальных стилях (например, `@apply border-red-500` в Tailwind).
462
+ Supports `input` (text, email, password, checkbox, radio), `select`, `textarea`. The `.input-error` class can be defined in global styles (e.g. `@apply border-red-500` in Tailwind).
417
463
 
418
464
  ---
419
465
 
420
- ## Шаблонные директивы
466
+ ## Template directives
421
467
 
422
- Helfy расширяет JSX директивами `@if`, `@elseif`, `@else`, `@for`, `@empty`. Компилятор трансформирует их в вызовы `$._if()`, `$._ifelse()`, `$._forin()` на этапе сборки.
468
+ Helfy extends JSX with `@if`, `@elseif`, `@else`, `@for`, and `@empty` directives.
423
469
 
424
470
  ### @if / @elseif / @else
425
471
 
426
- Условный рендеринг:
472
+ Conditional rendering:
427
473
 
428
474
  ```tsx
429
475
  render() {
430
476
  return (
431
477
  <div>
432
478
  @if (this.count > 0) {
433
- <span>Положительное: {this.count}</span>
479
+ <span>Positive: {this.count}</span>
434
480
  }
435
481
  </div>
436
482
  );
437
483
  }
438
484
  ```
439
485
 
440
- Цепочка условий:
486
+ Condition chains:
441
487
 
442
488
  ```tsx
443
489
  render() {
444
490
  return (
445
491
  <div>
446
492
  @if (this.status === 'loading') {
447
- <span>Загрузка...</span>
493
+ <span>Loading...</span>
448
494
  } @elseif (this.status === 'error') {
449
- <span>Ошибка!</span>
495
+ <span>Error!</span>
450
496
  } @else {
451
- <span>Данные загружены</span>
497
+ <span>Data loaded</span>
452
498
  }
453
499
  </div>
454
500
  );
455
501
  }
456
502
  ```
457
503
 
458
- Вложенные условия:
504
+ Nested conditions:
459
505
 
460
506
  ```tsx
461
507
  @if (this.isVisible) {
462
508
  <div>
463
509
  @if (this.count > 10) {
464
- <span>Больше десяти</span>
510
+ <span>More than ten</span>
465
511
  }
466
512
  </div>
467
513
  }
@@ -469,7 +515,7 @@ render() {
469
515
 
470
516
  ### @for
471
517
 
472
- Итерация по массиву. Синтаксис: `@for (item of array)` или `@for (item, index of array)`.
518
+ Array iteration. Syntax: `@for (item of array)` or `@for (item, index of array)`.
473
519
 
474
520
  ```tsx
475
521
  @state private items = ['apple', 'banana', 'cherry'];
@@ -485,7 +531,7 @@ render() {
485
531
  }
486
532
  ```
487
533
 
488
- `track` задает ключ для оптимизации DOM-диффинга (аналог `key` в React):
534
+ `track` sets the key for DOM diffing optimization (like `key` in React):
489
535
 
490
536
  ```tsx
491
537
  @for (user of this.users; track user.id) {
@@ -495,13 +541,13 @@ render() {
495
541
 
496
542
  ### @empty
497
543
 
498
- Блок `@empty` после `@for` рендерится, когда массив пуст:
544
+ The `@empty` block after `@for` renders when the array is empty:
499
545
 
500
546
  ```tsx
501
547
  @for (item of this.items; track item) {
502
548
  <div>{item}</div>
503
549
  } @empty {
504
- <span>Список пуст</span>
550
+ <span>List is empty</span>
505
551
  }
506
552
  ```
507
553
 
@@ -509,9 +555,9 @@ render() {
509
555
 
510
556
  ## State Management
511
557
 
512
- ### Создание Store
558
+ ### Creating a Store
513
559
 
514
- Store -- глобальное реактивное хранилище. Создается классом с декоратором `@Store`, реактивные поля помечаются `@observable`:
560
+ A Store is a global reactive store. Create a class with the `@Store` decorator; reactive fields use `@observable`:
515
561
 
516
562
  ```typescript
517
563
  import { Store, observable } from "@helfy/helfy";
@@ -523,7 +569,7 @@ class UserStore {
523
569
 
524
570
  login(name: string) {
525
571
  this.name = name;
526
- this.isLoggedIn = true; // подписчики name и isLoggedIn уведомлены
572
+ this.isLoggedIn = true; // subscribers of name and isLoggedIn are notified
527
573
  }
528
574
 
529
575
  logout() {
@@ -535,15 +581,15 @@ class UserStore {
535
581
  export default UserStore;
536
582
  ```
537
583
 
538
- Декоратор `@Store` добавляет классу:
539
- - `subscribe(field, callback)` -- подписка на изменение поля, возвращает функцию отписки
540
- - `unsubscribe(field, callback)` -- отписка
584
+ The `@Store` decorator adds:
585
+ - `subscribe(field, callback)` subscribe to field changes, returns unsubscribe function
586
+ - `unsubscribe(field, callback)` unsubscribe
541
587
 
542
- Декоратор `@observable` превращает поле в реактивное свойство с getter/setter. При записи нового значения все подписчики уведомляются.
588
+ The `@observable` decorator turns a field into a reactive property with getter/setter. On write, all subscribers are notified.
543
589
 
544
- ### Подписка через @observe
590
+ ### Subscribing with @observe
545
591
 
546
- Декоратор `@observe` связывает поле компонента с полем store. При изменении значения в store компонент автоматически перерендерится:
592
+ The `@observe` decorator binds a component field to a store field. When the store value changes, the component re-renders:
547
593
 
548
594
  ```tsx
549
595
  import { View, observe } from "@helfy/helfy";
@@ -562,9 +608,9 @@ class Header {
562
608
  return (
563
609
  <header>
564
610
  @if (this.isLoggedIn) {
565
- <span>Привет, {this.userName}!</span>
611
+ <span>Hello, {this.userName}!</span>
566
612
  } @else {
567
- <span>Войдите в систему</span>
613
+ <span>Sign in</span>
568
614
  }
569
615
  </header>
570
616
  );
@@ -572,11 +618,11 @@ class Header {
572
618
  }
573
619
  ```
574
620
 
575
- Тип поля берется из store через lookup-тип (`UserStore['name']`), что дает полную типобезопасность.
621
+ The field type is inferred from the store via lookup type (`UserStore['name']`), giving full type safety.
576
622
 
577
- ### Контекст хранилищ
623
+ ### Store context
578
624
 
579
- Рекомендуется создать единый контекст для всех store:
625
+ Create a single context for all stores:
580
626
 
581
627
  ```typescript
582
628
  import UserStore from "./UserStore";
@@ -590,26 +636,26 @@ class Stores {
590
636
  export default Stores;
591
637
  ```
592
638
 
593
- Использование в компонентах:
639
+ Usage in components:
594
640
 
595
641
  ```tsx
596
642
  import Stores from "./StoreContext";
597
643
 
598
- // подписка через декоратор
644
+ // subscribe via decorator
599
645
  @observe(Stores.userStore, 'name')
600
646
  private userName: string;
601
647
 
602
- // прямой вызов метода store
648
+ // direct store method call
603
649
  Stores.userStore.login("Alice");
604
650
  ```
605
651
 
606
652
  ---
607
653
 
608
- ## Логирование (@logger)
654
+ ## Logging (@logger)
609
655
 
610
- Декоратор `@logger` инжектирует логгер в классы View, Context, Store и Injectable. Имя класса подставляется на этапе компиляции (Babel-плагин), что сохраняет читаемые имена даже при минификации.
656
+ The `@logger` decorator injects a logger into View, Context, Store, and Injectable classes. The class name is set at compile time (Babel plugin), keeping readable names even after minification.
611
657
 
612
- ### Использование
658
+ ### Usage
613
659
 
614
660
  ```tsx
615
661
  import { View, logger, type ILogger } from "@helfy/helfy";
@@ -626,22 +672,22 @@ class TodoInput {
626
672
  }
627
673
  ```
628
674
 
629
- **Варианты:**
630
- - `@logger()` — имя класса подставляется на compile-time как `<ClassName>`, формат и цвет зависят от типа класса
631
- - `@logger("my-tag")` — кастомный тег, серый цвет
675
+ **Variants:**
676
+ - `@logger()` — class name is set at compile-time as `<ClassName>`, format and color depend on class type
677
+ - `@logger("my-tag")` — custom tag, gray color
632
678
 
633
- ### Формат и цвет тегов по типу класса
679
+ ### Tag format and color by class type
634
680
 
635
- | Тип | Формат | Цвет |
636
- |-----|--------|------|
637
- | View (компоненты) | `<TodoInput>` | skyblue |
638
- | Injectable (сервисы) | `TodoValidateService()` | pink |
681
+ | Type | Format | Color |
682
+ |------|--------|-------|
683
+ | View (components) | `<TodoInput>` | skyblue |
684
+ | Injectable (services) | `TodoValidateService()` | pink |
639
685
  | Context | `{TodoContext}` | khaki |
640
686
  | Form | `[LoginFormContext]` | bright blue |
641
687
  | Store | `TodoStore[]` | lime green |
642
- | Кастомный `@logger("...")` | как указано | gray |
688
+ | Custom `@logger("...")` | as specified | gray |
643
689
 
644
- ### API логгера
690
+ ### Logger API
645
691
 
646
692
  ```typescript
647
693
  interface ILogger {
@@ -649,13 +695,13 @@ interface ILogger {
649
695
  info(message: string, meta?: Record<string, unknown>): void;
650
696
  warn(message: string, meta?: Record<string, unknown>): void;
651
697
  error(messageOrError: string | Error, meta?: Record<string, unknown>): void;
652
- withContext(ctx: string): ILogger; // дочерний логгер с доп. префиксом
698
+ withContext(ctx: string): ILogger; // child logger with extra prefix
653
699
  }
654
700
  ```
655
701
 
656
- ### DI и транспорты
702
+ ### DI and transports
657
703
 
658
- `LoggerService` регистрируется под `ILoggerToken` при `registerAllServices`. Можно подключить кастомные транспорты (Sentry, файл и т.д.), реализующие `ILogTransport`:
704
+ `LoggerService` is registered under `ILoggerToken` via `registerAllServices`. Custom transports (Sentry, file, etc.) implementing `ILogTransport` can be wired up:
659
705
 
660
706
  ```typescript
661
707
  import { LoggerService, ConsoleTransport, ILoggerToken } from "@helfy/helfy";
@@ -668,17 +714,17 @@ createApp({ root })
668
714
  .mount(App);
669
715
  ```
670
716
 
671
- Без зарегистрированного логгера в контейнере используется fallback с выводом в `console`.
717
+ Without a registered logger in the container, a fallback that logs to `console` is used.
672
718
 
673
719
  ---
674
720
 
675
721
  ## Context & DI
676
722
 
677
- Helfy поддерживает иерархический Context и Dependency Injection: провайдер оборачивает поддерево в JSX, а дочерние компоненты получают значения через `@ctx`. Это удобно для темы, форм, роутинга и других общих зависимостей.
723
+ Helfy supports hierarchical Context and Dependency Injection: a provider wraps a subtree in JSX, and child components receive values via `@ctx`. Useful for theme, forms, routing, and other shared dependencies.
678
724
 
679
- ### Провайдер (@Context, @provide)
725
+ ### Provider (@Context, @provide)
680
726
 
681
- Класс с декоратором `@Context` не-рендерящий провайдер: он не имеет `render()` и рендерит только своих детей. Поля с `@provide` становятся доступны потребителям ниже по дереву.
727
+ A class with the `@Context` decorator is a non-rendering provider: it has no `render()` and only renders its children. Fields with `@provide` are available to consumers down the tree.
682
728
 
683
729
  ```tsx
684
730
  import { Context, provide } from "@helfy/helfy";
@@ -697,7 +743,7 @@ export class ThemeContext {
697
743
  }
698
744
  ```
699
745
 
700
- Использование в JSX — обёртка вокруг поддерева:
746
+ Usage in JSX — wrap a subtree:
701
747
 
702
748
  ```tsx
703
749
  <ThemeContext>
@@ -706,11 +752,11 @@ export class ThemeContext {
706
752
  </ThemeContext>
707
753
  ```
708
754
 
709
- Фреймворк рендерит только детей `ThemeContext`; сам провайдер не создаёт DOM-узлов.
755
+ The framework only renders the children of `ThemeContext`; the provider itself does not create DOM nodes.
710
756
 
711
- ### Потребитель (@ctx)
757
+ ### Consumer (@ctx)
712
758
 
713
- Компонент с `@View` может получать значение контекста через `@ctx`:
759
+ A component with `@View` can receive context values via `@ctx`:
714
760
 
715
761
  ```tsx
716
762
  import { View, ctx } from "@helfy/helfy";
@@ -724,14 +770,14 @@ class ThemeToggle {
724
770
  render() {
725
771
  return (
726
772
  <button onclick={this.theme.toggle}>
727
- {this.theme.mode === "dark" ? "Светлая тема" : "Тёмная тема"}
773
+ {this.theme.mode === "dark" ? "Light theme" : "Dark theme"}
728
774
  </button>
729
775
  );
730
776
  }
731
777
  }
732
778
  ```
733
779
 
734
- Можно инжектировать только поле контекста:
780
+ You can inject only a context field:
735
781
 
736
782
  ```tsx
737
783
  @View
@@ -740,20 +786,31 @@ class ModeDisplay {
740
786
  private mode!: TThemeMode;
741
787
 
742
788
  render() {
743
- return <span>Текущая тема: {this.mode}</span>;
789
+ return <span>Current theme: {this.mode}</span>;
744
790
  }
745
791
  }
746
792
  ```
747
793
 
748
- Поиск провайдера идёт вверх по дереву (`_parentView`); используется ближайший провайдер с нужным ключом.
794
+ Provider lookup goes up the tree (`_parentView`); the nearest provider with the matching key is used.
749
795
 
750
- ### Реактивные поля
796
+ ### Reactive fields
751
797
 
752
- `@provide({ reactive: true })` делает поле реактивным: при изменении все потребители перерендериваются (под капотом используется `subscribe`). Для методов достаточно `@provide()` без `reactive`.
798
+ `@provide({ reactive: true })` makes a field reactive: when it changes, all consumers re-render. For methods, `@provide()` without `reactive` is enough.
753
799
 
754
- ### Опциональная инжекция
800
+ **Computed fields** — getters with `@provide({ computed: true, deps: ["field1", "field2"] })` recompute when dependencies change and trigger consumer re-renders:
755
801
 
756
- Третий аргумент `@ctx` — опции `{ optional?, defaultValue? }`:
802
+ ```tsx
803
+ @provide({ computed: true, deps: ["todos", "filter"] })
804
+ get filteredTodos(): Todo[] {
805
+ return this.filter === "active"
806
+ ? this.todos.filter(t => !t.completed)
807
+ : this.todos;
808
+ }
809
+ ```
810
+
811
+ ### Optional injection
812
+
813
+ The third argument of `@ctx` — options `{ optional?, defaultValue? }`:
757
814
 
758
815
  ```tsx
759
816
  @ctx(ThemeContext, { optional: true })
@@ -763,15 +820,15 @@ private theme?: ThemeContext;
763
820
  private mode = "light";
764
821
  ```
765
822
 
766
- Без провайдера в дереве `optional: true` даёт `undefined`; `defaultValue` используется, когда провайдера нет.
823
+ With no provider in the tree, `optional: true` yields `undefined`; `defaultValue` is used when the provider is absent.
767
824
 
768
- ### DI Container (конструкторный инжект)
825
+ ### DI Container (constructor injection)
769
826
 
770
- Helfy поддерживает DI-контейнер для глобальных сервисов с **compile-time** магией: сервисы помечаются `@Injectable<IX>()`, потребители указывают только тип в конструкторебез `@inject` и явных токенов.
827
+ Helfy supports a DI container for global services with **compile-time** magic: services are marked with `@Injectable<IX>()`, consumers only specify the type in the constructor no `@inject` or explicit tokens.
771
828
 
772
- **Конвенция:** последний параметр конструктора props от родителя; предшествующие параметры резолвятся из контейнера или из @Context по дереву.
829
+ **Convention:** the last constructor parameter is props from the parent; preceding parameters are resolved from the container or from @Context up the tree.
773
830
 
774
- **Сервис**обязательно `@Injectable<IX>()` (интерфейс в generic):
831
+ **Service**must use `@Injectable<IX>()` (interface in generic):
775
832
 
776
833
  ```typescript
777
834
  export interface ITodoValidateService {
@@ -784,7 +841,7 @@ export class TodoValidateService implements ITodoValidateService {
784
841
  }
785
842
  ```
786
843
 
787
- **Потребитель**только тип в конструкторе (плагин подставляет токен):
844
+ **Consumer**only the type in the constructor (plugin injects the token):
788
845
 
789
846
  ```tsx
790
847
  @View
@@ -796,7 +853,7 @@ class TodoInput {
796
853
  }
797
854
  ```
798
855
 
799
- **Bootstrap** — регистрация DI подключается автоматически при `createApp` (плагин подставляет `.useDI(registerAllServices)` в цепочку):
856
+ **Bootstrap** — DI registration is attached automatically at `createApp` (plugin injects `.useDI(registerAllServices)` in the chain):
800
857
 
801
858
  ```typescript
802
859
  createApp({ root: document.getElementById("root")! })
@@ -804,9 +861,9 @@ createApp({ root: document.getElementById("root")! })
804
861
  .mount(App);
805
862
  ```
806
863
 
807
- Сканер вызывается Babel-плагином перед трансформацией, находит все `@Injectable<IX>()`, генерирует `.helfy/di-tokens.ts` и `.helfy/di-registry.ts`.
864
+ The scanner is run by the Babel plugin before transformation, finds all `@Injectable<IX>()`, and generates `.helfy/di-tokens.ts` and `.helfy/di-registry.ts`.
808
865
 
809
- **Fallback** — для ручной настройки доступны `.configureContainer()`, `@inject(token)`, `@Injectable(token)`:
866
+ **Fallback** — for manual setup: `.configureContainer()`, `@inject(token)`, `@Injectable(token)`:
810
867
 
811
868
  ```typescript
812
869
  createApp({ root })
@@ -818,38 +875,38 @@ createApp({ root })
818
875
  .mount(App);
819
876
  ```
820
877
 
821
- Опционально: передать свой контейнер через `.container(myContainer)` или `createApp({ root, container })`.
878
+ Optionally: pass a custom container via `.container(myContainer)` or `createApp({ root, container })`.
822
879
 
823
- `@Context` и контейнер сосуществуют: Context для tree-scoped значений (тема, роутер), Container для глобальных сервисов.
880
+ `@Context` and the container coexist: Context is for tree-scoped values (theme, router), Container for global services.
824
881
 
825
882
  ---
826
883
 
827
884
  ## JSX
828
885
 
829
- Helfy использует кастомный JSX runtime (`@helfy/helfy/jsx-runtime`). JSX транслируется в вызовы `jsx()` / `jsxs()`, которые строят виртуальное представление DOM.
886
+ Helfy uses a custom JSX runtime (`@helfy/helfy/jsx-runtime`). JSX is translated to `jsx()` / `jsxs()` calls that build a virtual DOM representation.
830
887
 
831
- ### Атрибуты
888
+ ### Attributes
832
889
 
833
- Стандартные HTML-атрибуты передаются напрямую:
890
+ Standard HTML attributes are passed through:
834
891
 
835
892
  ```tsx
836
- <input type="text" placeholder="Введите имя" />
893
+ <input type="text" placeholder="Enter name" />
837
894
  <img src="/logo.png" alt="Logo" />
838
895
  <div id="container" class="wrapper"></div>
839
896
  ```
840
897
 
841
- ### События
898
+ ### Events
842
899
 
843
- Обработчики событий передаются через атрибуты в **нижнем регистре** (`onclick`, `oninput`, а не `onClick`):
900
+ Event handlers use **lowercase** attributes (`onclick`, `oninput`, not `onClick`):
844
901
 
845
902
  ```tsx
846
903
  <button onclick={() => this.increment()}>+</button>
847
904
  <input oninput={(e) => this.handleInput(e)} />
848
905
  ```
849
906
 
850
- ### Стили
907
+ ### Styles
851
908
 
852
- Инлайн-стили передаются объектом с camelCase-ключами:
909
+ Inline styles use an object with camelCase keys:
853
910
 
854
911
  ```tsx
855
912
  <div style={{
@@ -858,26 +915,26 @@ Helfy использует кастомный JSX runtime (`@helfy/helfy/jsx-run
858
915
  padding: '8px 16px',
859
916
  borderRadius: '4px'
860
917
  }}>
861
- Стилизованный блок
918
+ Styled block
862
919
  </div>
863
920
  ```
864
921
 
865
- ### CSS-классы
922
+ ### CSS classes
866
923
 
867
- Атрибут `class` принимает строку или массив с условными классами:
924
+ The `class` attribute accepts a string or an array with conditional classes:
868
925
 
869
926
  ```tsx
870
- // строка
927
+ // string
871
928
  <div class="container">...</div>
872
929
 
873
930
  // CSS Modules
874
931
  import styles from './App.module.css';
875
932
  <div class={styles.wrapper}>...</div>
876
933
 
877
- // условные классы через массив
934
+ // conditional classes via array
878
935
  <div class={[
879
936
  styles.cell,
880
- [styles.active, this.isActive], // применяется если this.isActive === true
937
+ [styles.active, this.isActive], // applied when this.isActive === true
881
938
  [styles.disabled, this.isDisabled],
882
939
  ]}>...</div>
883
940
  ```
@@ -886,116 +943,44 @@ import styles from './App.module.css';
886
943
 
887
944
  ## API
888
945
 
889
- ### Декораторы
890
-
891
- | Декоратор | Область | Описание |
892
- |-----------|---------|----------|
893
- | `@View` | Класс | Превращает класс в компонент с `render()`, `view`, `update()` |
894
- | `@state` | Поле | Локальное состояние компонента на базе signals (запись обновляет только этот компонент) |
895
- | `@Store` | Класс | Добавляет `subscribe`/`unsubscribe` и реактивность `@observable`-полей |
896
- | `@observable` | Поле | Делает поле store реактивным -- запись уведомляет подписчиков |
897
- | `@observe(store, field)` | Поле | Связывает поле компонента с полем store |
898
- | `@Context` | Класс | Не-рендерящий провайдер контекста; рендерит только детей |
899
- | `@provide()` / `@provide({ reactive: true })` | Поле | Помечает поле как доступное для `@ctx` (reactive — потребители перерендериваются) |
900
- | `@ctx(ContextClass)` / `@ctx(ContextClass, field)` | Поле | Инжектирует контекст или поле контекста из ближайшего провайдера вверх по дереву |
901
- | `@Injectable<IX>()` / `@Injectable<IX>('singleton' \| 'transient' \| 'scoped')` / `@Injectable(token)` | Класс | Помечает класс как injectable. Scope в (): `'singleton'` (по умолчанию), `'transient'`, `'scoped'`. Fallback: `@Injectable(token)` с Symbol/строкой |
902
- | `@inject(token)` | Параметр конструктора | Задаёт токен для параметра (fallback; при `@Injectable<IX>()` плагин подставляет токен автоматически) |
903
- | `@logger()` / `@logger("tag")` | Поле | Инжектирует ILogger. Без аргумента — compile-time имя класса и цвет по типу (View/Context/Store/Injectable). С аргументомкастомный тег (серый) |
904
- | `@ref` | Поле | Помечает поле для получения ссылки на DOM или компонент (использовать с `@ref(this.имяПоля)` в JSX) |
905
- | `@binded(name)` | Поле | Связывает поле с биндингом `@bind:name` от родителя (для кастомных компонентов) |
906
- | `@bind(expr)` / `@bind:name(expr)` | JSX | Двустороннее связывание с `@state` полем (value/checked + oninput/onchange). Для компонентов: `@bind:value(expr)` |
907
- | `@Form` | Класс | Контекст формы с полями `@field` |
908
- | `@field(options)` | Поле FormContext | Создаёт FieldState для поля формы (value, isTouched, error, isDirty) |
909
- | `@useForm(FormContext)` | Поле | Инжектирует форму в компонент с подпиской на изменения полей |
910
- | `@field(expr)` | JSX | Подключает инпут к FieldState: value/checked + onblur + error class + aria-invalid |
911
-
912
- ### Экспорты пакета
946
+ ### Decorators
947
+
948
+ | Decorator | Scope | Description |
949
+ |-----------|-------|-------------|
950
+ | `@View` | Class | Turns a class into a component with `render()`, `view`, `update()` |
951
+ | `@state` | Field | Component local state on signals (write updates only this component) |
952
+ | `@Store` | Class | Adds `subscribe`/`unsubscribe` and `@observable` field reactivity |
953
+ | `@observable` | Field | Makes a store field reactive write notifies subscribers |
954
+ | `@observe(store, field)` | Field | Binds a component field to a store field |
955
+ | `@Context` | Class | Non-rendering context provider; renders only children |
956
+ | `@provide()` / `@provide({ reactive: true })` / `@provide({ computed: true, deps })` | Field | Marks a field as available to `@ctx`. `reactive`consumers re-render on change; `computed` + `deps` — for getters |
957
+ | `@ctx(ContextClass)` / `@ctx(ContextClass, field)` | Field | Injects context or a context field from the nearest provider up the tree |
958
+ | `@Injectable<IX>()` / `@Injectable<IX>('singleton' \| 'transient' \| 'scoped')` / `@Injectable(token)` | Class | Marks a class as injectable. Scope in (): `'singleton'` (default), `'transient'`, `'scoped'`. Fallback: `@Injectable(token)` with Symbol/string |
959
+ | `@inject(token)` | Constructor param | Sets the token for the parameter (fallback; with `@Injectable<IX>()` the plugin injects it automatically) |
960
+ | `@logger()` / `@logger("tag")` | Field | Injects ILogger. No arg — compile-time class name and color by type (View/Context/Store/Injectable). With argcustom tag (gray) |
961
+ | `@ref` | Field | Marks a field to receive a DOM or component reference (use with `@ref(this.fieldName)` in JSX) |
962
+ | `@expose` | Method | Makes a method available to the parent when using `@ref` on a component (without `@expose` the parent gets the full instance) |
963
+ | `@binded(name)` | Field | Binds a field to `@bind:name` from the parent (for custom components) |
964
+ | `@bind(expr)` / `@bind:name(expr)` | JSX | Two-way binding with `@state` field (value/checked + oninput/onchange). For components: `@bind:value(expr)` |
965
+ | `@Form` | Class | Form context with `@field` fields |
966
+ | `@field(options)` | FormContext field | Creates FieldState for a form field (value, isTouched, error, isDirty) |
967
+ | `@useForm(FormContext)` | Field | Injects the form into a component with subscription to field changes |
968
+ | `@field(expr)` | JSX | Connects an input to FieldState: value/checked + onblur + error class + aria-invalid |
969
+
970
+ ### Routing (SPA)
971
+
972
+ Helfy includes a lightweight SPA router. When you call `createApp().router({ routes }).mount(App)`, the framework automatically wraps the app in `RouterContext`, so you don't need to wrap your App manually:
913
973
 
914
- ```typescript
915
- import {
916
- $, // утилиты рендеринга ($._if, $._ifelse, $._forin, $._slot, $._callSlot)
917
- View, // декоратор @View
918
- state, // декоратор @state (signal-backed локальное состояние)
919
- Store, // декоратор @Store
920
- observable, // декоратор @observable
921
- observe, // декоратор @observe
922
- computed, // @computed для Store/@Context/@View (getter-based)
923
- effect, // @effect для реактивных сайд-эффектов во View
924
- Context, // декоратор @Context
925
- provide, // декоратор @provide
926
- ctx, // декоратор @ctx (поле, Context)
927
- Container, // DI-контейнер
928
- Injectable, // декоратор @Injectable
929
- inject, // декоратор @inject (параметр конструктора)
930
- createViewInstance,
931
- setRootContainer,
932
- getRootContainer,
933
- ref, // декоратор @ref
934
- binded, // декоратор @binded (для компонентов с @bind:value)
935
- Form, // декоратор @Form (контекст формы)
936
- field, // декоратор @field (поле FormContext)
937
- useForm, // декоратор @useForm (инжект формы)
938
- type FieldState,
939
- type FieldOptions,
940
- StoreBase, // базовый класс Store (для расширения)
941
- Subscriber, // тип callback-подписчика
942
- Router, // ядро роутера (Router)
943
- RouterContext, // контекст роутера для дерева компонентов
944
- RouterView, // компонент, рендерящий текущий маршрут
945
- Link, // SPA-ссылка
946
- DefaultNotFound, // fallback-компонент 404 (используется RouterView при отсутствии слота)
947
- path, // @path() — декоратор для pathname
948
- search, // @search() — декоратор для query-параметров
949
- params, // @params() — декоратор для route-параметров
950
- router, // @router() — декоратор для API роутера
951
- logger, // @logger() — декоратор для ILogger
952
- LoggerService, // реализация ILogger (DI)
953
- ConsoleTransport,
954
- ILoggerToken,
955
- // primitives для fine-grained reactivity
956
- createSignal,
957
- createEffect,
958
- createComputed,
959
- batch,
960
- onCleanup,
961
- type Signal,
962
- type SignalGetter,
963
- type SignalSetter,
964
- type EffectDisposer,
965
- type RouteConfig,
966
- type RouterLocation,
967
- type ILogger,
968
- type ILogTransport,
969
- type LogLevel,
970
- } from "@helfy/helfy";
974
+ ```tsx
975
+ // index.ts
976
+ createApp({ root: document.getElementById("root")! })
977
+ .router({ routes })
978
+ .mount(App);
971
979
  ```
972
980
 
973
- ### Subpath-экспорты
974
-
975
- | Путь | Назначение |
976
- |------|------------|
977
- | `helfy` | Основной API (декораторы, $, типы) |
978
- | `@helfy/helfy/router` | Тот же API роутера, но отдельным entry (для tree-shaking) |
979
- | `@helfy/helfy/jsx-runtime` | JSX runtime (`jsx`, `jsxs`) |
980
- | `@helfy/helfy/compiler/helfy-loader` | Webpack-loader для директив `@if`/`@for`/`@ref`/`@bind`/`@field` |
981
- | `@helfy/helfy/babel-preset` | Babel-пресет (JSX, TypeScript, декораторы) |
982
-
983
- ### Роутинг (SPA)
984
-
985
- Helfy включает лёгкий SPA‑роутер, построенный на `Context & DI`:
986
-
987
981
  ```tsx
988
- import {
989
- View,
990
- RouterContext,
991
- RouterView,
992
- Link,
993
- type RouteConfig,
994
- type RouterLocation,
995
- ctx,
996
- } from "@helfy/helfy";
997
-
998
- // Конфиг маршрутов
982
+ import { View, RouterView, Link, path, type RouteConfig } from "@helfy/helfy";
983
+
999
984
  const routes: RouteConfig[] = [
1000
985
  { path: "/", component: HomePage },
1001
986
  { path: "/analytics", component: AnalyticsPage },
@@ -1007,45 +992,34 @@ const routes: RouteConfig[] = [
1007
992
  class App {
1008
993
  render() {
1009
994
  return (
1010
- <RouterContext routes={routes}>
1011
- <Layout>
1012
- @slot.sidebar() {<Sidebar />}
1013
- @slot.content() {<RouterView />}
1014
- </Layout>
1015
- </RouterContext>
995
+ <Layout>
996
+ @slot.sidebar() {<Sidebar />}
997
+ @slot.content() {<RouterView />}
998
+ </Layout>
1016
999
  );
1017
1000
  }
1018
1001
  }
1019
1002
 
1020
1003
  @View
1021
1004
  class Sidebar {
1022
- @ctx(RouterContext, "location")
1023
- private location!: RouterLocation;
1005
+ @path()
1006
+ private pathname!: string;
1024
1007
 
1025
1008
  render() {
1026
- const pathname = this.location.pathname;
1027
- const isHome = pathname === "/";
1028
- const isAnalytics = pathname.startsWith("/analytics");
1009
+ const isHome = this.pathname === "/";
1010
+ const isAnalytics = this.pathname.startsWith("/analytics");
1029
1011
 
1030
1012
  return (
1031
1013
  <nav>
1032
- <Link
1033
- to="/"
1034
- label="Главная"
1035
- class={isHome ? "font-bold" : ""}
1036
- />
1037
- <Link
1038
- to="/analytics"
1039
- label="Аналитика"
1040
- class={isAnalytics ? "font-bold" : ""}
1041
- />
1014
+ <Link to="/" label="Home" class={isHome ? "font-bold" : ""} />
1015
+ <Link to="/analytics" label="Analytics" class={isAnalytics ? "font-bold" : ""} />
1042
1016
  </nav>
1043
1017
  );
1044
1018
  }
1045
1019
  }
1046
1020
  ```
1047
1021
 
1048
- Типичный компонент‑страница может использовать утилитарные декораторы роутера:
1022
+ A typical page component can use router decorators:
1049
1023
 
1050
1024
  ```tsx
1051
1025
  import { View, path, search, params, router, type RouterAPI } from "@helfy/helfy";
@@ -1072,7 +1046,7 @@ class DebugPage {
1072
1046
  <pre>params: {JSON.stringify(this.routeParams)}</pre>
1073
1047
  <pre>query: {JSON.stringify(this.query)}</pre>
1074
1048
  <button onclick={() => this.rtr.push("/analytics")}>
1075
- Перейти в аналитику
1049
+ Go to analytics
1076
1050
  </button>
1077
1051
  </section>
1078
1052
  );
@@ -1080,14 +1054,7 @@ class DebugPage {
1080
1054
  }
1081
1055
  ```
1082
1056
 
1083
- Под капотом:
1084
-
1085
- - `Router` использует `history.pushState/replaceState` и `popstate` для синхронизации с URL.
1086
- - `RouterContext` хранит реактивное `location` (`@provide({ reactive: true })`), так что `@ctx(RouterContext, "location")` и декораторы `@path/@search/@params/@router` автоматически триггерят перерендер компонентов при смене маршрута.
1087
- - `RouterView` сопоставляет текущий `pathname` с `RouteConfig` (в том числе с параметрами `:id` и вложенными маршрутами) и рендерит нужный `@View`‑класс. При отсутствии совпадения показывается `DefaultNotFound` или содержимое слота `@slot.notFound`.
1088
- - `Link` перехватывает `click`, не перезагружает страницу и вызывает `router.push/replace`, сохраняя поведение обычных ссылок (`href` остаётся).
1089
-
1090
- **Кастомная страница 404.** Оберните `RouterView` и переопределите слот `notFound`:
1057
+ **Custom 404 page.** Wrap `RouterView` and override the `notFound` slot:
1091
1058
 
1092
1059
  ```tsx
1093
1060
  @View
@@ -1102,35 +1069,35 @@ class AppRouter {
1102
1069
  );
1103
1070
  }
1104
1071
  }
1105
- ```
1072
+ ```
1106
1073
 
1107
- ### Жизненный цикл компонента
1074
+ ### Component lifecycle
1108
1075
 
1109
- 1. `constructor(props)` -- создание экземпляра, `this.props` оборачивается в реактивный Proxy
1110
- 2. `render()` -- возврат JSX (**вызывается один раз** при монтировании; Babel-плагин оборачивает выражения в `createReactiveChild` для fine-grained обновлений)
1111
- 3. `mount()` -- первичный рендер JSX в DOM-фрагмент
1112
- 4. `onMount()` -- хук после первого монтирования (опциональный)
1113
- 5. `onAttached()` -- хук после вставки в document (опциональный). Вызывается при `attach(parent)`.
1114
- 6. `update()` -- структурное обновление (при смене корневого компонента, например при роутинге). Для одного и того же дочернего компонента обновляются только signals через `updateProps`
1115
- 7. `updateProps(newProps)` -- обновление prop-signals через `batch()` (fine-grained, без полного перерендера)
1076
+ 1. `constructor(props)` instance creation, `this.props` wrapped in reactive Proxy
1077
+ 2. `render()` returns JSX (**called once** on mount)
1078
+ 3. `mount()` initial JSX render to DOM fragment
1079
+ 4. `onMount()` hook after first mount (optional)
1080
+ 5. `onAttached()` hook after insertion into document (optional). Called on `attach(parent)`.
1081
+ 6. `update()` structural update (e.g. on route change). For the same child component, only signals are updated via `updateProps`
1082
+ 7. `updateProps(newProps)` updates prop signals (fine-grained, no full re-render)
1116
1083
 
1117
1084
  #### onMount vs onAttached
1118
1085
 
1119
1086
  | | `onMount()` | `onAttached()` |
1120
1087
  |---|---|---|
1121
- | **Когда** | Сразу после `mount()`, дерево построено, рефы назначены | После `attach(parent)`, элемент в document |
1122
- | **Элемент в document** | Может быть ещё нет (корень в fragment) | Да |
1123
- | **Рефы** | Доступны | Доступны |
1088
+ | **When** | Right after `mount()`, tree built, refs assigned | After `attach(parent)`, element in document |
1089
+ | **Element in document** | May not be yet (root in fragment) | Yes |
1090
+ | **Refs** | Available | Available |
1124
1091
 
1125
- **`onMount()`** — для инициализации, не требующей document:
1126
- - Подписки на store/observable/сервисы
1127
- - Настройка внутреннего состояния
1128
- - Добавление обработчиков (работают и на detached-узлах)
1092
+ **`onMount()`** — for initialization that doesn't need the document:
1093
+ - Subscriptions to store/observable/services
1094
+ - Internal state setup
1095
+ - Adding handlers (works on detached nodes too)
1129
1096
 
1130
- **`onAttached()`** — для операций, требующих элемент в document:
1097
+ **`onAttached()`** — for operations that require the element in the document:
1131
1098
  - `focus()`, `scrollIntoView()`
1132
- - `getBoundingClientRect()`, замер layout
1133
- - Любые DOM API, работающие только с подключённым узлом
1099
+ - `getBoundingClientRect()`, layout measurement
1100
+ - Any DOM API that only works on attached nodes
1134
1101
 
1135
1102
  ```tsx
1136
1103
  @View
@@ -1138,62 +1105,22 @@ class SearchInput {
1138
1105
  @ref private input!: HTMLInputElement;
1139
1106
 
1140
1107
  onMount() {
1141
- this.store.subscribe(this.handleChange); // подписка — document не нужен
1108
+ this.store.subscribe(this.handleChange); // subscription — document not needed
1142
1109
  }
1143
1110
 
1144
1111
  onAttached() {
1145
- this.input.focus(); // фокуснужен document
1112
+ this.input.focus(); // focusneeds document
1146
1113
  }
1147
1114
  }
1148
1115
  ```
1149
1116
 
1150
- ### Утилиты рендеринга ($)
1151
-
1152
- Компилятор трансформирует директивы в вызовы `$`, но их можно использовать и напрямую.
1153
-
1154
- **Статические** (условие вычисляется один раз):
1155
-
1156
- ```tsx
1157
- import { $ } from "@helfy/helfy";
1158
-
1159
- render() {
1160
- return (
1161
- <div>
1162
- {$._if(this.visible, <span>Видно</span>)}
1163
- {$._ifelse(this.count > 0,
1164
- <span>Положительное</span>,
1165
- <span>Отрицательное или ноль</span>
1166
- )}
1167
- {$._forin(this.items, (item, index) => (
1168
- <div $_key={item.id}>{index}: {item.name}</div>
1169
- ))}
1170
- </div>
1171
- );
1172
- }
1173
- ```
1174
-
1175
- **Реактивные** (условие и ветки — thunks, пересчитываются при изменении signals):
1176
-
1177
- ```tsx
1178
- {$._rIf(() => this.visible, () => <span>Видно</span>)}
1179
- {$._rIfElse(() => this.count > 0,
1180
- () => <span>Положительное</span>,
1181
- () => <span>Отрицательное или ноль</span>
1182
- )}
1183
- {$._rForin(() => this.items, (item, index) => (
1184
- <div $_key={item.id}>{index}: {item.name}</div>
1185
- ))}
1186
- ```
1187
-
1188
- Компилятор автоматически выбирает реактивный вариант (`_rIf`, `_rIfElse`, `_rForin`) при генерации кода из директив `@if` / `@for`.
1189
-
1190
- ### Слоты (content projection)
1117
+ ### Slots (content projection)
1191
1118
 
1192
- Helfy поддерживает именованные слоты с fallback‑содержимым и переопределением в дочерних компонентах.
1119
+ Helfy supports named slots with fallback content and override in child components.
1193
1120
 
1194
- #### Провайдер слота (`@View`‑компонент)
1121
+ #### Slot provider (`@View` component)
1195
1122
 
1196
- Слоты объявляются прямо в JSX через директиву `@slot:имя(...)` внутри `render()`:
1123
+ Slots are declared in JSX via the `@slot:name(...)` directive inside `render()`:
1197
1124
 
1198
1125
  ```tsx
1199
1126
  import { View } from "@helfy/helfy";
@@ -1203,24 +1130,24 @@ class AppLayout {
1203
1130
  render() {
1204
1131
  return (
1205
1132
  <section class="layout">
1206
- {/* Именованный слот header с fallback-разметкой */}
1207
- @slot:header({ title: "Список задач" }) fallback {
1133
+ {/* Named slot header with fallback markup */}
1134
+ @slot:header({ title: "Task list" }) fallback {
1208
1135
  <header class="mb-4">
1209
1136
  <h1 class="text-2xl font-bold text-gray-900">
1210
- Список задач
1137
+ Task list
1211
1138
  </h1>
1212
1139
  </header>
1213
1140
  }
1214
1141
 
1215
- {/* Именованный слот content с fallback и директивой @if внутри */}
1142
+ {/* Named slot content with fallback and @if inside */}
1216
1143
  @slot:content({ store: this.props.store, filtered: this.props.filtered, hasTodos: this.props.hasTodos }) fallback {
1217
1144
  @if (this.props.hasTodos) {
1218
1145
  <section class="pt-3 border-t border-gray-200 text-sm text-gray-600">
1219
1146
  <p class="mb-1">
1220
- Всего задач: {this.props.store.todos.length}, активных: {this.props.store.activeCount},
1221
- завершённых: {this.props.store.completedCount}
1147
+ Total: {this.props.store.todos.length}, active: {this.props.store.activeCount},
1148
+ completed: {this.props.store.completedCount}
1222
1149
  </p>
1223
- <p>Фильтрованных к показу: {this.props.filtered.length}</p>
1150
+ <p>Filtered: {this.props.filtered.length}</p>
1224
1151
  </section>
1225
1152
  }
1226
1153
  }
@@ -1230,15 +1157,15 @@ class AppLayout {
1230
1157
  }
1231
1158
  ```
1232
1159
 
1233
- Правила для провайдера:
1160
+ Provider rules:
1234
1161
 
1235
- - `@slot:header({ ... })` — объявляет именованный слот `header` и вызывает его.
1236
- - Блок `fallback { ... }` (опциональный) задаёт разметку по умолчанию, если слот не переопределён.
1237
- - Внутри `fallback` можно использовать директивы `@if`, `@for` и обычный JSX.
1162
+ - `@slot:header({ ... })` — declares the named slot `header` and invokes it.
1163
+ - The `fallback { ... }` block (optional) defines default markup when the slot is not overridden.
1164
+ - Inside `fallback` you can use `@if`, `@for`, and regular JSX.
1238
1165
 
1239
- #### Потребитель слота (override в JSX)
1166
+ #### Slot consumer (override in JSX)
1240
1167
 
1241
- Переопределение слота в дочернем компоненте делается через `@slot.имя(...) { ... }` внутри JSX‑детей:
1168
+ Override a slot in a child component via `@slot.name(...) { ... }` inside JSX children:
1242
1169
 
1243
1170
  ```tsx
1244
1171
  @View
@@ -1254,20 +1181,20 @@ class TodoApp {
1254
1181
  filtered={filtered}
1255
1182
  hasTodos={this.hasTodos}
1256
1183
  >
1257
- {/* Переопределяем слот header */}
1184
+ {/* Override header slot */}
1258
1185
  @slot.header({ title }) {
1259
1186
  <header class="mb-5">
1260
1187
  <h1 class="mb-4 text-2xl font-bold text-gray-900">
1261
- Задачи ({title})
1188
+ Tasks ({title})
1262
1189
  </h1>
1263
1190
  <TodoInput
1264
- placeholder="Добавить задачу…"
1191
+ placeholder="Add task…"
1265
1192
  onSubmit={(text) => store.add(text)}
1266
1193
  />
1267
1194
  </header>
1268
1195
  }
1269
1196
 
1270
- {/* Переопределяем слот content, внутри можно использовать @if/@for */}
1197
+ {/* Override content slot, @if/@for allowed inside */}
1271
1198
  @slot.content({ store, filtered, hasTodos }) {
1272
1199
  @if (hasTodos) {
1273
1200
  <section class="pt-3 border-t border-gray-200">
@@ -1293,8 +1220,8 @@ class TodoApp {
1293
1220
  }
1294
1221
  ```
1295
1222
 
1296
- Кратко по синтаксису:
1223
+ Syntax summary:
1297
1224
 
1298
- - `@slot:имя({ props }) fallback { FallbackJSX }` — объявление и вызов слота в провайдере.
1299
- - `@slot.имя({ ctx }) { OverrideJSX }` — переопределение слота в потребителе.
1300
- - Внутри `FallbackJSX` и `OverrideJSX` поддерживаются все директивы (`@if`, `@for`, `@empty`) и обычный JSX.
1225
+ - `@slot:name({ props }) fallback { FallbackJSX }` — declare and invoke the slot in the provider.
1226
+ - `@slot.name({ ctx }) { OverrideJSX }` — override the slot in the consumer.
1227
+ - All directives (`@if`, `@for`, `@empty`) and regular JSX work inside `FallbackJSX` and `OverrideJSX`.