take4-console 0.15.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/CHANGELOG.md +365 -0
  2. package/README.md +1 -1
  3. package/dist/Screen/InterfaceBuilder.d.mts +15 -4
  4. package/dist/Screen/InterfaceBuilder.d.mts.map +1 -1
  5. package/dist/Screen/InterfaceBuilder.mjs +104 -8
  6. package/dist/Screen/InterfaceBuilder.mjs.map +1 -1
  7. package/dist/Screen/Pos.d.mts +12 -0
  8. package/dist/Screen/Pos.d.mts.map +1 -1
  9. package/dist/Screen/Pos.mjs +23 -1
  10. package/dist/Screen/Pos.mjs.map +1 -1
  11. package/dist/Screen/Screen.d.mts +77 -3
  12. package/dist/Screen/Screen.d.mts.map +1 -1
  13. package/dist/Screen/Screen.mjs +168 -3
  14. package/dist/Screen/Screen.mjs.map +1 -1
  15. package/dist/Screen/Size.d.mts +49 -6
  16. package/dist/Screen/Size.d.mts.map +1 -1
  17. package/dist/Screen/Size.mjs +81 -7
  18. package/dist/Screen/Size.mjs.map +1 -1
  19. package/dist/Screen/Window.d.mts +131 -20
  20. package/dist/Screen/Window.d.mts.map +1 -1
  21. package/dist/Screen/Window.mjs +474 -57
  22. package/dist/Screen/Window.mjs.map +1 -1
  23. package/dist/Screen/WindowManager.d.mts +85 -5
  24. package/dist/Screen/WindowManager.d.mts.map +1 -1
  25. package/dist/Screen/WindowManager.mjs +279 -26
  26. package/dist/Screen/WindowManager.mjs.map +1 -1
  27. package/dist/Screen/controls/ListBox.d.mts +34 -12
  28. package/dist/Screen/controls/ListBox.d.mts.map +1 -1
  29. package/dist/Screen/controls/ListBox.mjs +127 -25
  30. package/dist/Screen/controls/ListBox.mjs.map +1 -1
  31. package/dist/Screen/controls/TextArea.d.mts +15 -1
  32. package/dist/Screen/controls/TextArea.d.mts.map +1 -1
  33. package/dist/Screen/controls/TextArea.mjs +74 -1
  34. package/dist/Screen/controls/TextArea.mjs.map +1 -1
  35. package/dist/Screen/controls/TextBox.d.mts +13 -1
  36. package/dist/Screen/controls/TextBox.d.mts.map +1 -1
  37. package/dist/Screen/controls/TextBox.mjs +36 -1
  38. package/dist/Screen/controls/TextBox.mjs.map +1 -1
  39. package/dist/Screen/textWidth.d.mts +13 -0
  40. package/dist/Screen/textWidth.d.mts.map +1 -0
  41. package/dist/Screen/textWidth.mjs +188 -0
  42. package/dist/Screen/textWidth.mjs.map +1 -0
  43. package/dist/Screen/types.d.mts +336 -20
  44. package/dist/Screen/types.d.mts.map +1 -1
  45. package/dist/Screen/types.mjs.map +1 -1
  46. package/dist/index.d.mts +3 -2
  47. package/dist/index.d.mts.map +1 -1
  48. package/dist/index.mjs +3 -1
  49. package/dist/index.mjs.map +1 -1
  50. package/package.json +4 -4
  51. package/src/Screen/InterfaceBuilder.mts +116 -20
  52. package/src/Screen/Pos.mts +24 -1
  53. package/src/Screen/Screen.mts +192 -4
  54. package/src/Screen/Size.mts +97 -12
  55. package/src/Screen/Window.mts +463 -63
  56. package/src/Screen/WindowManager.mts +301 -29
  57. package/src/Screen/controls/ListBox.mts +151 -32
  58. package/src/Screen/controls/TextArea.mts +82 -1
  59. package/src/Screen/controls/TextBox.mts +40 -1
  60. package/src/Screen/textWidth.mts +186 -0
  61. package/src/Screen/types.mts +328 -23
  62. package/src/demo.mts +232 -20
  63. package/src/index.mts +23 -3
  64. package/src/layout.yaml +56 -24
package/CHANGELOG.md CHANGED
@@ -1,5 +1,370 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.25.0] – 2026-04-18
4
+
5
+ ### Added — backlog P0-10 (InterfaceBuilder: register custom types)
6
+ - **`InterfaceBuilder#registerType(name, factory)`** — rejestruje niestandardową
7
+ fabrykę kontrolki adresowalną przez `type: <name>` w YAML. Fabryka otrzymuje
8
+ surowy `YamlWindowDef` oraz `CustomTypeContext` z wstępnie zresolvowanym
9
+ `wp: WindowProperties`, aktywnym `registry: StyleRegistry` oraz pomocniczym
10
+ `resolveCallback(id)` przekierowującym do wcześniej zarejestrowanych
11
+ callbacków. Próba rejestracji pod nazwą wbudowanego typu rzuca wyjątkiem,
12
+ żeby uniknąć cichego shadowingu.
13
+ - **`YamlWindowDef.props`** — wolne pole `Record<string, unknown>` forwardowane
14
+ w całości do fabryki; wbudowane typy je ignorują.
15
+ - **Auto-focus registration** — jeśli fabryka zwróci `Window` z metodą
16
+ `handleKey()`, builder automatycznie rejestruje go w `WindowManager` razem
17
+ z łańcuchem rodziców, tak samo jak wbudowane `Button`/`TextBox`/itd.
18
+ - **Eksporty**: nowe typy `CustomTypeContext` oraz `CustomTypeFactory` są
19
+ publicznie dostępne z `take4-console`.
20
+ - **Demo**: w `src/demo.mts` rejestrowany jest custom typ `badge` (klasa
21
+ `Badge extends Window` malująca etykietę w render()), a `src/layout.yaml`
22
+ używa go w pasku nagłówka (`id: buildBadge`, `props: { text, color }`).
23
+
24
+ ## [0.24.0] – 2026-04-18
25
+
26
+ ### Added — backlog P0-3 (flex layout / auto-sizing)
27
+ - **`WindowProperties.layout: 'absolute' | 'row' | 'column' | 'grid'`** — nowy,
28
+ per-okienny tryb layoutu dla bezpośrednich dzieci. Domyślnie `'absolute'`,
29
+ czyli dotychczasowe zachowanie (`Pos`/`Size` rozwiązuje każde dziecko
30
+ niezależnie). `'row'` i `'column'` uruchamiają silnik flex (rozdzielenie osi
31
+ głównej pro-rata `grow`, skurcz `shrink`, wyrównanie cross-axis), `'grid'`
32
+ rozkłada dzieci w równe komórki wiersz-po-wierszu.
33
+ - **Nowe pola `WindowProperties`**: `gap` (odstęp między dziećmi w cellach),
34
+ `padding` (uniform number | `[v, h]` tuple | per-side record; wpływa na
35
+ `getInnerSize()` / `getInnerOffset()` — stackuje na border inset),
36
+ `gridColumns` (liczba kolumn dla `grid`), `alignItems`
37
+ (`start | center | end | stretch`, domyślnie `'stretch'`) oraz
38
+ `justifyContent` (`start | center | end | space-between | space-around`).
39
+ - **`Pos.flex(order?)` / `Pos#getFlexOrder()`** — marker pozycji flex. Silnik
40
+ layoutu parenta ustawia finalne `child.x / child.y`; `order` sortuje dzieci
41
+ niezależnie od kolejności `addChild` (stabilne ties → kolejność wstawiania).
42
+ - **`Size.flex(grow?, shrink?, basis?)` / `Size.content()`** oraz osobne
43
+ fabryki `flex()` i `content()` — `Size.flex(...)` zaznacza oba axes jako
44
+ flex (silnik decyduje który jest main vs cross na podstawie parenta),
45
+ `Size.content()` używa aktualnych wymiarów regionu dziecka jako naturalnego
46
+ rozmiaru. Dla mieszanych osi: `new Size(flex(), 10)` / `new Size(content(),
47
+ pct(50))`. `Size.isAbsolute()` zwraca `false` dla flex/content — `resolve()`
48
+ daje bezpieczny fallback (basis dla flex, 1 dla content) dopóki silnik nie
49
+ nadpisze wartości. Nowe gettery `getWidthSpec()` / `getHeightSpec()`.
50
+ - **Silnik layoutu w `Window`** — `addChild` i `setSize` (→ `reflowChildren`)
51
+ delegują do `runLayout()`. Absolute path zachowany 1:1 dla back-compat;
52
+ row / column liczą basis, rozdzielają `remainder` przez `grow` (całkowite,
53
+ reszta od truncation trafia do ostatniego flex-a), skracają przy ujemnym
54
+ remainderze przez `shrink`, aplikują stretch/align cross-axis i — gdy nic
55
+ nie zjada slack'u — uruchamiają `justifyContent`. `grid` liczy równe komórki
56
+ `(inner - gap * (cols|rows - 1)) / (cols|rows)`. Niewidoczne dzieci są
57
+ pomijane, więc `setVisible(false)` wyjmuje je ze stacka.
58
+ - **YAML (InterfaceBuilder)** — wspiera `pos: flex` / `pos: { flex: N }`,
59
+ `size: flex` / `size: content` / `size: { flex: { grow, shrink, basis } }`,
60
+ a także per-axis `size: { width: content, height: { flex: { grow: 2 } } }`.
61
+ Nowe pola `layout`, `gap`, `padding`, `gridColumns`, `alignItems`,
62
+ `justifyContent` na każdej definicji okna.
63
+ - **`Window#blitChild`** czyta teraz `child.x / child.y` zamiast re-solve'ować
64
+ `posSpec`, więc absolute i flex lecą tym samym kodem kompozycji.
65
+
66
+ ### Demo
67
+ - `src/layout.yaml` — dolny pasek akcji (Delete / Cancel / Save) przekonwertowany
68
+ z trzech absolutnych pozycji na kontener `buttonRow` z `layout: row`,
69
+ `gap: 1`, `alignItems: stretch` i dzieckiem-spacerem `{ size: flex }` między
70
+ "Delete" a grupą "Cancel / Save" — ten sam wygląd co wcześniej, zapisany
71
+ deklaratywnie, automatycznie re-flow przy SIGWINCH.
72
+
73
+ ## [0.23.0] – 2026-04-16
74
+
75
+ ### Added — backlog P0-5 (WindowManager.pause / resume)
76
+ - **`WindowManager.pause(options?)` / `resume(options?)` / `isPaused()`** —
77
+ tymczasowo zwalnia kontrolę nad terminalem bez niszczenia focus tree,
78
+ rejestracji kontrolek ani stosu dialogów. `pause()` odłącza listener
79
+ stdin, wyłącza raw mode + mouse tracking, pokazuje kursor i opcjonalnie
80
+ (`{ leaveAltScreen: true }`) wychodzi z alt-screen buffer. `resume()`
81
+ re-enter-uje alt-screen (jeśli pause go zamknął), re-enable mouse,
82
+ ukrywa kursor z powrotem, re-attach stdin + raw mode i re-renderuje
83
+ klatkę (chyba że `{ rerender: false }`). Obie metody są idempotentne.
84
+ Typowe użycie: `pause({ leaveAltScreen: true })` → `spawnSync('$EDITOR')`
85
+ → `resume()`.
86
+ - **`WindowManager.stop()`** rozpoznaje stan pauzy i nie powtarza teardownu
87
+ stdin / mouse / raw mode, które pause już zrobiła — unika podwójnego
88
+ `stdin.off('data', …)`. Reszta semantyki stop bez zmian.
89
+
90
+ ### Demo
91
+ - `src/demo.mts` — `bindKey('ctrl+e')` pauzuje TUI (wychodzi z alt-screen),
92
+ drukuje prompt `--- paused … Press Enter to return ---`, blokuje na
93
+ `bash -c 'read -r _'` w podpowłoce i po `Enter` wywołuje `resume()`.
94
+ Focus, helpMode i historia sparkline-ów są zachowane przez cały cykl.
95
+ - Status bar dostaje `Ctrl+E` w obu trybach (help / normal).
96
+
97
+ ## [0.22.0] – 2026-04-16
98
+
99
+ ### Added — backlog P0-8 (Window.setVisible)
100
+ - **`Window.setVisible(visible: boolean)` / `Window.isVisible(): boolean`** —
101
+ ortogonalny do `disabled` przełącznik widoczności. Okna konstruują się
102
+ jako `visible: true`; `setVisible(false)` zamienia `render()` w no-op
103
+ (żadna faza `paintBackground → blitContent → paintBorder → children` się
104
+ nie odpala), a przy przeglądzie dzieci rodzic **pomija** ukryte dziecko,
105
+ więc jego dotychczasowy region nie jest blitowany — widoczne jest tło
106
+ rodzica. `getCell` na ukrytym oknie rzuca wyjątek; `setVisible(true)`
107
+ przywraca okno z nienaruszoną zawartością `content` (hide/show to
108
+ logiczne ukrycie, a nie kasowanie buforu).
109
+ - **`WindowManager` respektuje widoczność** — nowy prywatny helper
110
+ `isFocusable(control)` = `!disabled && visible`. Tab / Shift-Tab,
111
+ `setFocus()`, auto-init focusa oraz mouse click hit-test pomijają
112
+ niewidoczne kontrolki tak samo, jak od dawna pomijają disabled.
113
+
114
+ ### Demo
115
+ - `src/demo.mts` — `bindKey('v')` toggle'uje widoczność panelu
116
+ `chartsPanel` (Tabs). Status bar dostaje skrót `v` w trybie normal
117
+ i help, żeby feature był używalny bez sięgania do docs.
118
+
119
+ ## [0.21.0] – 2026-04-16
120
+
121
+ ### Added — backlog P0-6 (onChange / onSubmit / onKeyDown w TextBox + TextArea)
122
+ - **`TextBoxProperties` / `TextAreaProperties`** rozszerzone o
123
+ `onChange(value)`, `onSubmit(value)` i `onKeyDown(key)` (pre-dispatch
124
+ hook z semantyką `boolean | void` — return `true` = consumed,
125
+ identycznie jak `WindowManagerOptions.onKey` z P0-4).
126
+ - **`TextAreaProperties.insertTabAsSpaces`** (domyślnie `0`) — gdy
127
+ `> 0`, Tab wstawia N spacji zamiast cyklować focus; Shift-Tab
128
+ **zawsze** cykluje focus, dzięki czemu user ma kontrolowany escape
129
+ z multi-line input-a.
130
+ - **`TextAreaProperties.ctrlDDeletesForward`** (domyślnie `false`) —
131
+ opt-in forward-delete pod `\x04`.
132
+ - **`Focusable.capturesTab?()`** — opcjonalny hook sprawdzany przez
133
+ `WindowManager`. `TextArea` implementuje go tak, by zwracał `true`
134
+ wtedy i tylko wtedy gdy `insertTabAsSpaces > 0`.
135
+ - **Settery**: `TextBox.setOnChange/SetOnSubmit/SetOnKeyDown`,
136
+ `TextArea.setOnChange/SetOnSubmit/SetOnKeyDown` — do wstrzykiwania
137
+ callbacków po konstrukcji (np. przy YAML-buildowanych kontrolkach).
138
+ - **YAML / `InterfaceBuilder`** — nowe pola `YamlWindowDef`:
139
+ `onSubmit`, `onKeyDown`, `insertTabAsSpaces`, `ctrlDDeletesForward`.
140
+ Callbacki wiązane przez istniejące `ib.registerCallback(id, fn)`.
141
+
142
+ ### Changed
143
+ - **`TextBox.handleKey`**: Enter (`\r` / `\n` / `'enter'`) nie
144
+ wstawia znaku — odpala `onSubmit(value)`.
145
+ - **`TextArea.handleKey`**: alias `'ctrl+enter'` odpala `onSubmit`;
146
+ plain Enter bez zmian (wstawia newline).
147
+ - **`WindowManager.handleInput`**: Tab najpierw pyta focused control
148
+ o `capturesTab()` — gdy odpowiedź to `true`, `handleKey('\t')`
149
+ dostaje klawisz zamiast standardowego `moveFocus(+1)`.
150
+
151
+ ### Demo
152
+ - `layout.yaml` — `tbUsername` wiąże `onSubmit: usernameSubmitted`.
153
+ - `src/demo.mts` — registerCallback `usernameSubmitted` dopisuje
154
+ event do log-u po Enter w polu username.
155
+
156
+ ## [0.20.0] – 2026-04-16
157
+
158
+ ### Added — backlog P0-4 (onKey preventDefault + kolejność)
159
+ - **`WindowManagerOptions.onKey`** — nowa sygnatura
160
+ `(key: string, ctx: KeyContext) => boolean | void`. Zwrot `true`
161
+ *konsumuje* klawisz: pomija exit-key check, Tab/Shift-Tab navigation
162
+ i dispatch do focused control. Zwrot `void` / `false` zachowuje
163
+ dotychczasowe pass-through — istniejący kod działa bez zmian.
164
+ - **`KeyContext`** — nowy typ publiczny: `{ focusedControl, inDialog,
165
+ dialogDepth }`. Snapshot wzięty przed wywołaniem global handlerów,
166
+ więc widzą spójny widok focus / dialog stack.
167
+ - **`WindowManager.bindKey(keySpec, handler)` / `unbindKey(keySpec, handler?)`**
168
+ — register-based alternative dla global shortcut-ów. `bindKey` zwraca
169
+ unbind-funkcję. `keySpec` akceptuje raw strings, nazwy (`enter`,
170
+ `space`, `esc`, strzałki, `pageup/down`, `home/end`, …) oraz
171
+ `ctrl+<letter>`. Wiele handlerów pod jeden klawisz — fire w kolejności
172
+ rejestracji aż pierwszy zwróci `true`.
173
+ - **`KeyBindHandler`** — typ publiczny: `(ctx: KeyContext) => boolean | void`.
174
+
175
+ ### Changed
176
+ - **Kolejność dispatchu** w `WindowManager.handleInput` — global
177
+ handlery (`bindKey` → `onKey`) **przed** exit-key check / Tab /
178
+ dispatch. Dzięki temu global shortcut może zablokować `q` exit
179
+ (np. confirmation dialog), a TextBox nie "połyka" `?` / `:` / itp.
180
+ - **Demo (`src/demo.mts`)** — `bindKey('?')` toggluje help mode
181
+ w status barze; `bindKey('ctrl+r')` wymusza `tick()` out-of-band.
182
+ Status bar stale pokazuje skrót `?` zamiast tylko `q`.
183
+
184
+ ## [0.19.0] – 2026-04-16
185
+
186
+ ### Added — backlog P0-11 (Screen alt-screen + hide-cursor opcja)
187
+ - **`ScreenOptions`** — nowy interfejs publiczny: `altScreen?`, `hideCursor?`,
188
+ `targetFps?`. `new Screen({ altScreen: true, hideCursor: true })` sam
189
+ wchodzi w alt-screen buffer i ukrywa kursor — koniec boilerplate'u dla
190
+ konsumentów-bez-WindowManagera.
191
+ - **`Screen.enterAltScreen` / `exitAltScreen` / `hideHardwareCursor` /
192
+ `showHardwareCursor`** + odpowiadające `is…` queries. Wszystkie idempotentne;
193
+ pozwalają zewnętrznemu kodowi (w tym `WindowManager`-owi) dzielić ten sam
194
+ state machine.
195
+ - **`Screen.dispose`** — przywraca każdą zmianę w stanie terminala i
196
+ odpina listenery sygnałów. Idempotentne. Połączone z `process.on('exit')`,
197
+ więc cleanup last-chance odpalą się też przy `process.exit()` /
198
+ naturalnym end-of-loop.
199
+ - **`Screen.getTargetFps`** — soft cap z `ScreenOptions` zwracany do inspekcji
200
+ (pełny enforcement w P2-47).
201
+
202
+ ### Added — backlog P0-12 (SIGWINCH autoresize + event)
203
+ - **`Screen` event API**: `screen.on('resize', listener)` i
204
+ `screen.on('frame', listener)` (typed overloads). Pod spodem
205
+ composed `EventEmitter`, więc Window class hierarchy zostaje płaska.
206
+ - **`Screen.resize(width?, height?)`** — pobiera nowe wymiary z
207
+ `process.stdout` (lub z explicit args), reflowuje dzieci procentowe,
208
+ emituje `'resize'`. Wywoływane automatycznie przez SIGWINCH handler.
209
+ - **`render()`** mierzy wall-clock i emituje `'frame'` z `{ ms }`
210
+ po każdym wywołaniu — daje hak do telemetrii FPS / animation loopów.
211
+ - **`Window.setSize(width, height)`** — publiczny wrapper na
212
+ `resizeRegions` (zmienione z `private` na `protected`). Reflowuje
213
+ dzieci procentowe pod nowy parent area.
214
+
215
+ ### Changed
216
+ - **`WindowManager.run/stop`** — używa `screen.enterAltScreen` /
217
+ `hideHardwareCursor` zamiast pisać escape'y wprost. Tracking ownership
218
+ przez `ownsAltScreen` / `ownsCursor`: jeżeli `Screen` ustawił stan
219
+ w konstruktorze (`ScreenOptions`), `stop()` go nie wycofuje — alt-screen
220
+ przeżywa restart WM-a aż do `Screen.dispose()`.
221
+ - **Demo (`src/demo.mts`)** — `new Screen({ altScreen: true, hideCursor: true })`,
222
+ `screen.on('resize', …)` aktualizujący status bar i wykonujący `screen.render()`,
223
+ `screen.dispose()` w `onExit` callbacku WindowManagera. Status bar wydzielony
224
+ jako `redrawStatusBar()` żeby etykieta `${width}×${height}` mogła być
225
+ przerysowana po resize.
226
+ - **`Screen` listener cap** — statyczny licznik aktywnych instancji bumpuje
227
+ `process.setMaxListeners(10 + n*2)`, dispose dekrementuje. Eliminuje
228
+ `MaxListenersExceededWarning` w suite-ach testowych z wieloma `new Screen()`.
229
+
230
+ ### Tests
231
+ - 9 nowych testów w `tests/Screen.test.mts` (ScreenOptions, dispose,
232
+ resize, frame event) + `afterEach(dispose)`.
233
+ - 1 nowy test w `tests/WindowManager.test.mts` (lifecycle: brak podwójnego
234
+ toggle dla alt-screen i kursora) + `afterEach(dispose)`.
235
+ - Pełna suita: 499 testów, wszystkie zielone.
236
+
237
+ ### Docs
238
+ - Dodano `doc/p0-11-and-p0-12-screen-lifecycle.md` — pełny opis API,
239
+ cyklu życia, interakcji z WindowManager, backwards compatibility i
240
+ zmienionych plików.
241
+
242
+ ## [0.18.0] – 2026-04-16
243
+
244
+ ### Added — backlog P0-2 (rich text / multi-style writeText)
245
+ - **`Window.writeText`** akceptuje teraz `WriteTextInput = string | WriteTextSegment[]`.
246
+ Segmenty są renderowane inline — kursor „płynie" przez kolejne segmenty bez
247
+ resetowania X, więc info-bary, history rows czy command completion nie
248
+ wymagają ręcznego liczenia pozycji. Każdy segment może podać własny
249
+ `style: StyleId` (mergowany z base) albo `attrs: CellAttributes` (rejestrowane
250
+ ad hoc w `StyleRegistry`). Pusta tablica jest no-opem.
251
+ - **`Window.writeMarkup(template, options?)`** — mini-markup `{name}…{/}` dla
252
+ nazwanych stylów z `StyleRegistry`. Tagi wspierają zagnieżdżanie
253
+ (wewnętrzny styl jest mergowany na zewnętrzny), `{/}` zamyka najbliższy
254
+ otwarty tag, `{{` / `}}` to escape literalnych nawiasów klamrowych, nieznane
255
+ nazwy nie zmieniają stylu. Template kompilowany jest do `WriteTextSegment[]`
256
+ i puszczany przez `writeText`, więc layout/width/clipping idą tą samą ścieżką.
257
+ - **`WriteTextSegment`**, **`WriteTextInput`** — nowe typy publiczne w
258
+ `src/Screen/types.mts`, eksportowane z `take4-console` barrel-a.
259
+
260
+ ### Changed
261
+ - **`Window.writeText` pętla renderująca**: zachowana (East-Asian width,
262
+ zero-width skip, sentinel `''` dla wide chars, clipping). Dodano zewnętrzną
263
+ pętlę po segmentach i jednorazowe policzenie bazowego stylu.
264
+ - **Demo (`src/demo.mts`)** — nagłówek ekranu używa `writeMarkup` z nazwanymi
265
+ stylami `hdr:app` / `hdr:mode` / `hdr:sep` (rejestrowane przez
266
+ `Screen.setBuiltinStyle`). Status bar przeszedł na segmentowy `writeText`
267
+ z inline'owymi skrótami, separatorem i sekcją dim pokazującą emoji/CJK
268
+ (double-width) — cała linia jednym wywołaniem.
269
+
270
+ ### Docs
271
+ - Dodano `doc/p0-2-rich-text-writetext.md` — pełny opis API, algorytmu,
272
+ gramatyki markup-u i kompatybilności wstecznej.
273
+
274
+ ## [0.17.0] – 2026-04-16
275
+
276
+ ### Added — backlog P0-7 (text measurement z East-Asian width)
277
+ - **`src/Screen/textWidth.mts`** — nowy moduł z funkcjami `charWidth(cp)`,
278
+ `stringWidth(str)`, `setPuaWidth(1|2)`, `getPuaWidth()`. Tabela szerokości
279
+ oparta o Unicode East Asian Width (kategorie W i F) plus zero-width
280
+ control / combining / format / variation selectors / ZWJ / BOM.
281
+ - **`Window.getTextWidth(text)`** — zwraca display width tekstu w komórkach
282
+ terminala, odpowiednik `stringWidth` dostępny na każdym Window/kontrolce.
283
+ - **`Window.writeText`** uwzględnia szerokość znaków:
284
+ - znaki szerokie (CJK, emoji, double-width NerdFonts) zajmują dwie kolejne
285
+ komórki — `''` w komórce kontynuacyjnej jako sentinel;
286
+ - kursor wewnątrz pętli zaawansowuje się o `charWidth(ch)`;
287
+ - znaki o szerokości 0 (combining, control) są pomijane;
288
+ - wide znaki, których prawa połówka wykraczałaby poza inner-width, są
289
+ pomijane w całości (zachowanie alignmentu).
290
+ - **`Screen.render`** pomija continuation cells (`ch === ''`), dzięki czemu
291
+ ANSI output nie emituje stub'a — terminal po wide znaku już ma kursor
292
+ zaawansowany o 2 kolumny.
293
+ - **Konfigurowalna szerokość PUA** (NerdFonts) przez `setPuaWidth(1|2)` —
294
+ domyślnie `1` (większość patched fontów ma single-cell glyphs).
295
+ - Eksportowane z `take4-console` barrel-a: `charWidth`, `stringWidth`,
296
+ `setPuaWidth`, `getPuaWidth`.
297
+
298
+ ### Changed
299
+ - **Demo (`src/demo.mts`)** — kilka templates eventów ma double-width
300
+ emoji (`'cache warmed 🚀'`, `'job completed 💾'`); demo prezentuje, że
301
+ wide glyphs nie psują wyrównania w `ListBox` (przy emoji surrogate-pair,
302
+ gdzie `length=2 = displayWidth=2`).
303
+
304
+ ### Added — backlog P0-9 (rozszerzenie BorderStyle)
305
+ - **`BorderStyle`** rozszerzony o cztery nowe warianty:
306
+ - `'thick'` — heavy box-drawing (`━┃┏┓┗┛`),
307
+ - `'dashed'` — dashed lines z light corners (`╌╎┌┐└┘`),
308
+ - `'ascii'` — fallback ASCII (`-|+`),
309
+ - `'none'` — jawny placeholder równoważny brakowi ramki (bez insetów).
310
+ - **`BorderChars`** — nowy interfejs publiczny opisujący komplet glifów
311
+ (krawędzie, narożniki, opcjonalne T-junctions i cross dla
312
+ przyszłych kontrolek typu Table).
313
+ - **`WindowBorder.chars?: Partial<BorderChars>`** — per-glyph override
314
+ aplikowany na bazowy zestaw wybranego `style`. Pozwala podmienić tylko
315
+ wybrane znaki (np. narożniki) bez redefiniowania całej tabeli.
316
+ - Eksportowane z `take4-console` barrel-a: typ `BorderChars`.
317
+
318
+ ### Changed — backlog P0-9
319
+ - **`Window.borderInset`** traktuje `style: 'none'` jako brak ramki —
320
+ `getInnerOffset()` / `getInnerSize()` nie odejmują 1 cell-a po stronach.
321
+ - **`Window.paintBorder`** mergeuje `border.chars` na bazowy zestaw
322
+ (`{ ...baseChars, ...border.chars }`) i przerywa się natychmiast dla
323
+ `'none'`.
324
+ - **Internal `BORDER_CHARS`** używa pełnych nazw glifów z `BorderChars`
325
+ (`horizontal`/`vertical`/`topLeft`/…) zamiast wcześniejszych skrótów
326
+ (`h`/`v`/`tl`/…). Pole jest prywatne — bez wpływu na konsumentów.
327
+ - **Demo (`src/layout.yaml`)** — `monitorPanel` używa `style: thick`,
328
+ `eventsPanel` używa `style: dashed`, `leftPanel` demonstruje override
329
+ pojedynczych glifów (`chars: { topLeft: '◆', topRight: '◆' }`).
330
+
331
+ ### Docs
332
+ - Dodano `doc/p0-7-text-width.md` — pełny opis API, algorytmu sentinela
333
+ continuation cell, tabel Unicode i kompatybilności wstecznej.
334
+ - Dodano `doc/p0-9-border-styles.md` — opis nowych stylów, tabela
335
+ glifów, semantyka `'none'` i przykłady override `chars`.
336
+
337
+ ## [0.16.0] – 2026-04-16
338
+
339
+ ### Added — backlog P0-1 (custom per-row rendering w ListBox)
340
+ - **`ListBox<T>` jest teraz generyczny** — element listy może być dowolnym typem
341
+ (domyślnie `string`, co zachowuje wsteczną kompatybilność). `ListBoxProperties<T>`,
342
+ `setItems(items: T[])`, `getSelectedItem(): T | undefined`, `onChange(idx, item: T)`.
343
+ - **`renderItem(item, ctx): string | ListBoxRowSegment[]`** — opcjonalny per-row
344
+ renderer. Obsługuje segmenty z wyrównaniem `left` / `right` / `fill`; style
345
+ segmentów są mergowane ze stylem bazowym wiersza (tło selekcji pozostaje widoczne).
346
+ - **`rowHeight`** — wysokość slotu w komórkach (domyślnie 1). `handleKey` PgUp/PgDn
347
+ uwzględnia liczbę widocznych slotów, nie surowych wierszy.
348
+ - **`keyFn`** — stały klucz per-item, dostępny przez `getItemKey(item)`; przechowywany
349
+ pod kątem późniejszej reconciliation w `setItems()`.
350
+ - **`setRenderItem(fn)`** — podmiana renderera po konstrukcji.
351
+ - Nowe typy w `src/Screen/types.mts`: `ListBoxRenderContext`, `ListBoxRowSegment`,
352
+ `ListBoxRowSegments`. Eksportowane z `take4-console` barrel-a.
353
+
354
+ ### Changed
355
+ - **Demo (`src/demo.mts` + `src/layout.yaml`)** — lista zdarzeń (`eventsList`) to teraz
356
+ `ListBox<EventRow>` z customowym rendererem pokazującym kolorowe ikony (✓/⚠/✖),
357
+ wyciemniony timestamp i licznik wyrównany do prawej krawędzi.
358
+
359
+ ### Docs
360
+ - Dodano `doc/p0-1-listbox-custom-render.md` — pełny opis API, algorytmu
361
+ renderowania segmentów i kompatybilności wstecznej.
362
+
363
+ ## [0.15.1] – 2026-04-12
364
+
365
+ ### Fixed
366
+ - Corrected `repository.url` in `package.json` (`take4-console` → `take4_console`).
367
+
3
368
  ## [0.15.0] – 2026-04-12
4
369
 
5
370
  ### Changed
package/README.md CHANGED
@@ -139,7 +139,7 @@ in [section 7](#7-yaml--interfacebuilder-integration).
139
139
  The repository ships a live demo that exercises every built-in control:
140
140
 
141
141
  ```bash
142
- git clone https://github.com/arcymag/take4-console.git
142
+ git clone https://github.com/wiecznyPoszukiwacz/take4_console.git
143
143
  cd take4-console
144
144
  npm install
145
145
  npm run demo # tsx src/demo.mts
@@ -1,3 +1,4 @@
1
+ import type { CustomTypeFactory } from './types.mjs';
1
2
  import { Window } from './Window.mjs';
2
3
  import { Screen } from './Screen.mjs';
3
4
  import { WindowManager } from './WindowManager.mjs';
@@ -5,9 +6,11 @@ import { WindowManager } from './WindowManager.mjs';
5
6
  *
6
7
  * Usage:
7
8
  * 1. Create an InterfaceBuilder and call registerCallback() for any onPress/onChange IDs.
8
- * 2. Call build(yamlText, screen) or buildFromFile(path, screen).
9
- * 3. Optionally pass a WindowManager to automatically register all focusable controls.
10
- * 4. The returned Map<string, Window> gives access to windows by their YAML id.
9
+ * 2. Optionally call registerType(name, factory) to teach the builder about
10
+ * user-defined controls addressable via `type: <name>` in YAML.
11
+ * 3. Call build(yamlText, screen) or buildFromFile(path, screen).
12
+ * 4. Optionally pass a WindowManager to automatically register all focusable controls.
13
+ * 5. The returned Map<string, Window> gives access to windows by their YAML id.
11
14
  *
12
15
  * YAML schema:
13
16
  * windows:
@@ -22,10 +25,18 @@ import { WindowManager } from './WindowManager.mjs';
22
25
  */
23
26
  export declare class InterfaceBuilder {
24
27
  private callbacks;
25
- /** Creates an InterfaceBuilder with an empty callback registry. */
28
+ private customTypes;
29
+ /** Creates an InterfaceBuilder with empty callback and custom-type registries. */
26
30
  constructor();
27
31
  /** Registers a named callback for use with onPress or onChange in YAML definitions. */
28
32
  registerCallback(id: string, fn: (...args: unknown[]) => void): void;
33
+ /** Registers a custom control factory addressable by `type: <name>` in YAML.
34
+ * The factory receives the raw YAML node plus a context with pre-resolved
35
+ * `wp` (WindowProperties), the style registry, and a callback resolver.
36
+ * Custom names cannot override a built-in type tag. If the returned window
37
+ * implements Focusable, it is auto-registered with WindowManager (when one
38
+ * is supplied to `build()`). */
39
+ registerType(name: string, factory: CustomTypeFactory): void;
29
40
  /** Builds the UI from a YAML string, adds all top-level windows to Screen,
30
41
  * and registers focusable controls with WindowManager if provided.
31
42
  * Styles defined in the `styles:` section are registered before any windows are built.
@@ -1 +1 @@
1
- {"version":3,"file":"InterfaceBuilder.d.mts","sourceRoot":"","sources":["../../src/Screen/InterfaceBuilder.mts"],"names":[],"mappings":"AAYA,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAsFpD;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,SAAS,CAA4C;IAE7D,mEAAmE;;IAKnE,uFAAuF;IAChF,gBAAgB,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,GAAG,IAAI;IAI3E;;;4EAGwE;IACjE,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,aAAa,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC;IAmCvF,+DAA+D;IAClD,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAO9G;;yEAEqE;IACrE,OAAO,CAAC,SAAS;IAoNjB,8EAA8E;IAC9E,OAAO,CAAC,WAAW;CAOpB"}
1
+ {"version":3,"file":"InterfaceBuilder.d.mts","sourceRoot":"","sources":["../../src/Screen/InterfaceBuilder.mts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAUV,iBAAiB,EAElB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAiIpD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,SAAS,CAA8C;IAC/D,OAAO,CAAC,WAAW,CAAiC;IAEpD,kFAAkF;;IAMlF,uFAAuF;IAChF,gBAAgB,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,GAAG,IAAI;IAI3E;;;;;qCAKiC;IAC1B,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,GAAG,IAAI;IAOnE;;;4EAGwE;IACjE,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,aAAa,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC;IAmCvF,+DAA+D;IAClD,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAO9G;;yEAEqE;IACrE,OAAO,CAAC,SAAS;IAqPjB,8EAA8E;IAC9E,OAAO,CAAC,WAAW;CAOpB"}
@@ -2,7 +2,7 @@ import { parse } from 'yaml';
2
2
  import { readFile } from 'node:fs/promises';
3
3
  import { Window } from './Window.mjs';
4
4
  import { Pos, pct } from './Pos.mjs';
5
- import { Size } from './Size.mjs';
5
+ import { Size, flex, content } from './Size.mjs';
6
6
  import { getRegistry } from './RegistryHolder.mjs';
7
7
  import { Button } from './controls/Button.mjs';
8
8
  import { TextBox } from './controls/TextBox.mjs';
@@ -18,6 +18,21 @@ import { ListBox } from './controls/ListBox.mjs';
18
18
  import { Tabs } from './controls/Tabs.mjs';
19
19
  import { Sparkline } from './controls/Sparkline.mjs';
20
20
  import { Spinner } from './controls/Spinner.mjs';
21
+ // ── Internal helpers ──────────────────────────────────────────────────────────
22
+ /** Reserved `type:` tags owned by the built-in widget switch — custom factories
23
+ * cannot override these. */
24
+ /** Structural check: returns true when the window opts into keyboard input by
25
+ * exposing `handleKey` (on top of the focus helpers inherited from Window).
26
+ * Used to auto-register custom-type controls with WindowManager. */
27
+ function isFocusable(win) {
28
+ const w = win;
29
+ return typeof w.handleKey === 'function';
30
+ }
31
+ const BUILTIN_TYPE_NAMES = new Set([
32
+ 'window', 'button', 'textbox', 'textarea', 'checkbox', 'radio',
33
+ 'statusled', 'progressbar', 'progressbarv', 'linechart', 'barchart',
34
+ 'listbox', 'tabs', 'sparkline', 'spinner',
35
+ ]);
21
36
  /** Converts a YamlAxisValue ("N%", or a number) to a number or Pct instance. */
22
37
  function parseAxisValue(v) {
23
38
  if (typeof v === 'string') {
@@ -42,6 +57,8 @@ function parsePos(spec) {
42
57
  return Pos.bottomLeft();
43
58
  if (spec === 'bottomRight')
44
59
  return Pos.bottomRight();
60
+ if (spec === 'flex')
61
+ return Pos.flex();
45
62
  if (typeof spec === 'object') {
46
63
  if ('preset' in spec) {
47
64
  const off = spec.offset !== undefined ? parseAxisValue(spec.offset) : 0;
@@ -52,23 +69,52 @@ function parsePos(spec) {
52
69
  case 'bottom': return Pos.bottom(off);
53
70
  }
54
71
  }
72
+ if ('flex' in spec) {
73
+ return Pos.flex(spec.flex ?? 0);
74
+ }
55
75
  if ('x' in spec && 'y' in spec) {
56
76
  return new Pos(parseAxisValue(spec.x), parseAxisValue(spec.y));
57
77
  }
58
78
  }
59
79
  throw new Error(`Invalid pos spec: ${JSON.stringify(spec)}`);
60
80
  }
81
+ /** Converts a single axis value within a Size spec — extends the plain-axis
82
+ * parser with the 'flex' / 'content' shorthands and the `{ flex: {...} }` /
83
+ * `{ content: true }` objects so each axis can be chosen independently. */
84
+ function parseDimValue(v) {
85
+ if (v === 'flex')
86
+ return flex();
87
+ if (v === 'content')
88
+ return content();
89
+ if (typeof v === 'object' && v !== null) {
90
+ if ('flex' in v) {
91
+ const basis = v.flex.basis !== undefined ? parseAxisValue(v.flex.basis) : 0;
92
+ return flex(v.flex.grow ?? 1, v.flex.shrink ?? 1, basis);
93
+ }
94
+ if ('content' in v && v.content === true)
95
+ return content();
96
+ }
97
+ return parseAxisValue(v);
98
+ }
61
99
  /** Converts a YamlSizeSpec to a Size instance. */
62
100
  function parseSize(spec) {
63
101
  if (spec === 'fill')
64
102
  return Size.fill();
103
+ if (spec === 'flex')
104
+ return Size.flex();
105
+ if (spec === 'content')
106
+ return Size.content();
65
107
  if (typeof spec === 'object') {
66
108
  if ('fillWidth' in spec)
67
109
  return Size.fillWidth(parseAxisValue(spec.fillWidth));
68
110
  if ('fillHeight' in spec)
69
111
  return Size.fillHeight(parseAxisValue(spec.fillHeight));
112
+ if ('flex' in spec) {
113
+ const basis = spec.flex.basis !== undefined ? parseAxisValue(spec.flex.basis) : 0;
114
+ return Size.flex(spec.flex.grow ?? 1, spec.flex.shrink ?? 1, basis);
115
+ }
70
116
  if ('width' in spec && 'height' in spec) {
71
- return new Size(parseAxisValue(spec.width), parseAxisValue(spec.height));
117
+ return new Size(parseDimValue(spec.width), parseDimValue(spec.height));
72
118
  }
73
119
  }
74
120
  throw new Error(`Invalid size spec: ${JSON.stringify(spec)}`);
@@ -88,9 +134,11 @@ function resolveBackground(bg) {
88
134
  *
89
135
  * Usage:
90
136
  * 1. Create an InterfaceBuilder and call registerCallback() for any onPress/onChange IDs.
91
- * 2. Call build(yamlText, screen) or buildFromFile(path, screen).
92
- * 3. Optionally pass a WindowManager to automatically register all focusable controls.
93
- * 4. The returned Map<string, Window> gives access to windows by their YAML id.
137
+ * 2. Optionally call registerType(name, factory) to teach the builder about
138
+ * user-defined controls addressable via `type: <name>` in YAML.
139
+ * 3. Call build(yamlText, screen) or buildFromFile(path, screen).
140
+ * 4. Optionally pass a WindowManager to automatically register all focusable controls.
141
+ * 5. The returned Map<string, Window> gives access to windows by their YAML id.
94
142
  *
95
143
  * YAML schema:
96
144
  * windows:
@@ -105,14 +153,28 @@ function resolveBackground(bg) {
105
153
  */
106
154
  export class InterfaceBuilder {
107
155
  callbacks;
108
- /** Creates an InterfaceBuilder with an empty callback registry. */
156
+ customTypes;
157
+ /** Creates an InterfaceBuilder with empty callback and custom-type registries. */
109
158
  constructor() {
110
159
  this.callbacks = new Map();
160
+ this.customTypes = new Map();
111
161
  }
112
162
  /** Registers a named callback for use with onPress or onChange in YAML definitions. */
113
163
  registerCallback(id, fn) {
114
164
  this.callbacks.set(id, fn);
115
165
  }
166
+ /** Registers a custom control factory addressable by `type: <name>` in YAML.
167
+ * The factory receives the raw YAML node plus a context with pre-resolved
168
+ * `wp` (WindowProperties), the style registry, and a callback resolver.
169
+ * Custom names cannot override a built-in type tag. If the returned window
170
+ * implements Focusable, it is auto-registered with WindowManager (when one
171
+ * is supplied to `build()`). */
172
+ registerType(name, factory) {
173
+ if (BUILTIN_TYPE_NAMES.has(name)) {
174
+ throw new Error(`Cannot register custom type "${name}": name is reserved by a built-in type.`);
175
+ }
176
+ this.customTypes.set(name, factory);
177
+ }
116
178
  /** Builds the UI from a YAML string, adds all top-level windows to Screen,
117
179
  * and registers focusable controls with WindowManager if provided.
118
180
  * Styles defined in the `styles:` section are registered before any windows are built.
@@ -168,6 +230,12 @@ export class InterfaceBuilder {
168
230
  focused: def.focused,
169
231
  disabled: def.disabled,
170
232
  label: def.label,
233
+ layout: def.layout,
234
+ gap: def.gap,
235
+ padding: def.padding,
236
+ gridColumns: def.gridColumns,
237
+ alignItems: def.alignItems,
238
+ justifyContent: def.justifyContent,
171
239
  };
172
240
  let win;
173
241
  switch (def.type ?? 'window') {
@@ -183,9 +251,15 @@ export class InterfaceBuilder {
183
251
  }
184
252
  case 'textbox': {
185
253
  wp.size = wp.size ?? this.requireSize(def);
254
+ const tbChange = def.onChange ? this.callbacks.get(def.onChange) : undefined;
255
+ const tbSubmit = def.onSubmit ? this.callbacks.get(def.onSubmit) : undefined;
256
+ const tbKeyDown = def.onKeyDown ? this.callbacks.get(def.onKeyDown) : undefined;
186
257
  const tb = new TextBox(wp, {
187
258
  value: def.value,
188
259
  placeholder: def.placeholder,
260
+ onChange: tbChange,
261
+ onSubmit: tbSubmit,
262
+ onKeyDown: tbKeyDown,
189
263
  });
190
264
  pending.push({ control: tb, parents: [...parentChain] });
191
265
  win = tb;
@@ -193,9 +267,17 @@ export class InterfaceBuilder {
193
267
  }
194
268
  case 'textarea': {
195
269
  wp.size = wp.size ?? this.requireSize(def);
270
+ const taChange = def.onChange ? this.callbacks.get(def.onChange) : undefined;
271
+ const taSubmit = def.onSubmit ? this.callbacks.get(def.onSubmit) : undefined;
272
+ const taKeyDown = def.onKeyDown ? this.callbacks.get(def.onKeyDown) : undefined;
196
273
  const ta = new TextArea(wp, {
197
274
  value: def.value,
198
275
  placeholder: def.placeholder,
276
+ onChange: taChange,
277
+ onSubmit: taSubmit,
278
+ onKeyDown: taKeyDown,
279
+ insertTabAsSpaces: def.insertTabAsSpaces,
280
+ ctrlDDeletesForward: def.ctrlDDeletesForward,
199
281
  });
200
282
  pending.push({ control: ta, parents: [...parentChain] });
201
283
  win = ta;
@@ -320,8 +402,22 @@ export class InterfaceBuilder {
320
402
  break;
321
403
  }
322
404
  default: {
323
- wp.size = wp.size ?? this.requireSize(def);
324
- win = new Window(wp);
405
+ const customFactory = def.type ? this.customTypes.get(def.type) : undefined;
406
+ if (customFactory) {
407
+ const ctx = {
408
+ wp,
409
+ registry: getRegistry(),
410
+ resolveCallback: (id) => (id ? this.callbacks.get(id) : undefined),
411
+ };
412
+ win = customFactory(def, ctx);
413
+ if (isFocusable(win)) {
414
+ pending.push({ control: win, parents: [...parentChain] });
415
+ }
416
+ }
417
+ else {
418
+ wp.size = wp.size ?? this.requireSize(def);
419
+ win = new Window(wp);
420
+ }
325
421
  break;
326
422
  }
327
423
  }