@mosaicoo/kanban 0.1.0 → 0.2.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.
@@ -0,0 +1,2570 @@
1
+ import * as i0 from '@angular/core';
2
+ import { signal, computed, Injectable, inject, ChangeDetectionStrategy, Component, input, output, effect, untracked } from '@angular/core';
3
+ import * as i7 from '@angular/material/button';
4
+ import { MatButtonModule } from '@angular/material/button';
5
+ import * as i1$1 from '@angular/material/tabs';
6
+ import { MatTabsModule } from '@angular/material/tabs';
7
+ import { MosKanbanBoard } from '@mosaicoo/kanban/ui';
8
+ import { NgTemplateOutlet } from '@angular/common';
9
+ import * as i1 from '@angular/forms';
10
+ import { FormsModule } from '@angular/forms';
11
+ import * as i6 from '@angular/material/checkbox';
12
+ import { MatCheckboxModule } from '@angular/material/checkbox';
13
+ import * as i2 from '@angular/material/form-field';
14
+ import { MatFormFieldModule } from '@angular/material/form-field';
15
+ import * as i3 from '@angular/material/input';
16
+ import { MatInputModule } from '@angular/material/input';
17
+ import * as i4 from '@angular/material/select';
18
+ import { MatSelectModule } from '@angular/material/select';
19
+ import * as i5 from '@angular/material/slide-toggle';
20
+ import { MatSlideToggleModule } from '@angular/material/slide-toggle';
21
+ import { validateConfig, createBoard } from '@mosaicoo/kanban/core';
22
+ import * as i7$1 from '@angular/material/expansion';
23
+ import { MatExpansionModule } from '@angular/material/expansion';
24
+
25
+ /** Espelho de styles/kanban-tokens.css (defaults de fábrica). */
26
+ const TOKEN_GROUPS = [
27
+ {
28
+ label: 'Espaçamento',
29
+ tokens: [
30
+ { token: '--mkb-column-gap', label: 'Entre colunas', kind: 'text', default: '12px' },
31
+ { token: '--mkb-lane-gap', label: 'Entre lanes', kind: 'text', default: '12px' },
32
+ { token: '--mkb-card-gap', label: 'Entre cartões', kind: 'text', default: '8px' },
33
+ { token: '--mkb-entry-gap', label: 'Entre itens do cartão', kind: 'text', default: '4px' },
34
+ { token: '--mkb-block-gap', label: 'Entre blocos do cartão', kind: 'text', default: '8px' },
35
+ { token: '--mkb-fact-gap', label: 'Entre fatos (facts)', kind: 'text', default: '10px' },
36
+ { token: '--mkb-card-padding', label: 'Interno do cartão', kind: 'text', default: '10px 12px' },
37
+ { token: '--mkb-cell-padding', label: 'Interno da célula', kind: 'text', default: '4px 8px 10px' },
38
+ { token: '--mkb-header-padding', label: 'Interno dos cabeçalhos', kind: 'text', default: '10px 12px' },
39
+ { token: '--mkb-column-width', label: 'Largura da coluna', kind: 'text', default: 'clamp(220px, 26vw, 320px)' },
40
+ { token: '--mkb-grid-column-width', label: 'Largura da coluna (grade de lanes)', kind: 'text', default: 'var(--mkb-column-width)' },
41
+ { token: '--mkb-lane-label-width', label: 'Largura do rótulo da lane', kind: 'text', default: '140px' },
42
+ ],
43
+ },
44
+ {
45
+ label: 'Cores',
46
+ tokens: [
47
+ { token: '--mkb-surface', label: 'Fundo do board', kind: 'color', default: '#f6f5f8' },
48
+ { token: '--mkb-column-bg', label: 'Fundo da coluna', kind: 'color', default: '#efedf3' },
49
+ { token: '--mkb-card-bg', label: 'Fundo do cartão', kind: 'color', default: '#ffffff' },
50
+ { token: '--mkb-border', label: 'Borda', kind: 'color', default: '#e3e0ea' },
51
+ { token: '--mkb-border-strong', label: 'Borda forte', kind: 'color', default: '#c9c4d4' },
52
+ { token: '--mkb-foreground', label: 'Texto', kind: 'color', default: '#241f33' },
53
+ { token: '--mkb-muted', label: 'Texto secundário', kind: 'color', default: '#6f6a7c' },
54
+ { token: '--mkb-accent', label: 'Realce (accent)', kind: 'color', default: '#5e35b1' },
55
+ { token: '--mkb-accent-contrast', label: 'Contraste do realce', kind: 'color', default: '#ffffff' },
56
+ { token: '--mkb-danger', label: 'Perigo', kind: 'color', default: '#b3261e' },
57
+ { token: '--mkb-warning', label: 'Alerta', kind: 'color', default: '#9a6b00' },
58
+ { token: '--mkb-success', label: 'Sucesso', kind: 'color', default: '#1b6e3c' },
59
+ { token: '--mkb-info', label: 'Informação', kind: 'color', default: '#2264c4' },
60
+ ],
61
+ },
62
+ {
63
+ label: 'Forma e movimento',
64
+ tokens: [
65
+ { token: '--mkb-radius', label: 'Raio (containers)', kind: 'text', default: '10px' },
66
+ { token: '--mkb-radius-sm', label: 'Raio (cartões/controles)', kind: 'text', default: '6px' },
67
+ { token: '--mkb-dur', label: 'Duração (hover/foco)', kind: 'text', default: '150ms' },
68
+ { token: '--mkb-dur-move', label: 'Duração (movimento/settle)', kind: 'text', default: '180ms' },
69
+ ],
70
+ },
71
+ ];
72
+ const COMMON = [
73
+ { key: 'label', label: 'Rótulo (label)', kind: 'text' },
74
+ { key: 'class', label: 'Classes CSS (separadas por espaço)', kind: 'csv' },
75
+ ];
76
+ const FIELD = {
77
+ key: 'field',
78
+ label: 'Campo do item',
79
+ kind: 'text',
80
+ hint: 'Nome do campo lido no card (ex.: titulo)',
81
+ };
82
+ const TONES = ['neutral', 'info', 'success', 'warning', 'danger'];
83
+ /** Descritores dos campos editáveis de cada tipo de bloco. */
84
+ const BLOCK_FIELDS = {
85
+ text: [
86
+ FIELD,
87
+ { key: 'variant', label: 'Variante', kind: 'select', options: ['', 'title', 'subtitle', 'body', 'caption', 'highlight'] },
88
+ { key: 'prefix', label: 'Prefixo', kind: 'text' },
89
+ { key: 'suffix', label: 'Sufixo', kind: 'text' },
90
+ { key: 'format', label: 'Formatador (format:*)', kind: 'text', hint: 'Nome no registry do host' },
91
+ { key: 'clamp', label: 'Máx. de linhas', kind: 'number' },
92
+ ...COMMON,
93
+ ],
94
+ number: [
95
+ FIELD,
96
+ { key: 'variant', label: 'Variante', kind: 'select', options: ['', 'body', 'highlight'] },
97
+ { key: 'style', label: 'Estilo', kind: 'select', options: ['', 'decimal', 'currency', 'percent'] },
98
+ { key: 'currency', label: 'Moeda (ISO)', kind: 'text', hint: 'ex.: BRL' },
99
+ { key: 'decimals', label: 'Casas decimais', kind: 'number' },
100
+ { key: 'prefix', label: 'Prefixo', kind: 'text' },
101
+ { key: 'suffix', label: 'Sufixo', kind: 'text' },
102
+ ...COMMON,
103
+ ],
104
+ date: [
105
+ FIELD,
106
+ { key: 'dateStyle', label: 'Formato', kind: 'select', options: ['', 'short', 'medium', 'long', 'full'] },
107
+ { key: 'relative', label: 'Relativo (há 2 dias)', kind: 'toggle' },
108
+ ...COMMON,
109
+ ],
110
+ time: [
111
+ FIELD,
112
+ { key: 'timeStyle', label: 'Formato', kind: 'select', options: ['', 'short', 'medium'] },
113
+ ...COMMON,
114
+ ],
115
+ datetime: [
116
+ FIELD,
117
+ { key: 'dateStyle', label: 'Formato (data)', kind: 'select', options: ['', 'short', 'medium', 'long', 'full'] },
118
+ { key: 'timeStyle', label: 'Formato (hora)', kind: 'select', options: ['', 'short', 'medium'] },
119
+ { key: 'relative', label: 'Relativo', kind: 'toggle' },
120
+ ...COMMON,
121
+ ],
122
+ image: [
123
+ FIELD,
124
+ { key: 'alt', label: 'Texto alternativo', kind: 'text' },
125
+ { key: 'rounded', label: 'Arredondada', kind: 'toggle' },
126
+ ...COMMON,
127
+ ],
128
+ tag: [
129
+ FIELD,
130
+ { key: 'tone', label: 'Tom', kind: 'select', options: ['', ...TONES] },
131
+ { key: 'map', label: 'Mapa valor → estilo (JSON)', kind: 'json', hint: '{"ok": {"label": "OK", "tone": "success", "icon": "check"}}' },
132
+ ...COMMON,
133
+ ],
134
+ badge: [
135
+ FIELD,
136
+ { key: 'icon', label: 'Ícone (icon:*)', kind: 'text' },
137
+ { key: 'tone', label: 'Tom', kind: 'select', options: ['', ...TONES] },
138
+ { key: 'showText', label: 'Mostrar texto', kind: 'toggle' },
139
+ { key: 'map', label: 'Mapa valor → estilo (JSON)', kind: 'json' },
140
+ ...COMMON,
141
+ ],
142
+ icon: [
143
+ { key: 'icon', label: 'Ícone (icon:*)', kind: 'text' },
144
+ { key: 'tone', label: 'Tom', kind: 'select', options: ['', ...TONES] },
145
+ ...COMMON,
146
+ ],
147
+ avatar: [
148
+ { key: 'nameField', label: 'Campo do nome', kind: 'text' },
149
+ { key: 'photoField', label: 'Campo da foto (URL)', kind: 'text' },
150
+ ...COMMON,
151
+ ],
152
+ facts: [{ key: 'items', label: 'Fatos', kind: 'facts' }, ...COMMON],
153
+ items: [
154
+ FIELD,
155
+ { key: 'labelField', label: 'Campo do rótulo', kind: 'text', hint: 'default: label' },
156
+ { key: 'checkField', label: 'Campo de conclusão', kind: 'text', hint: 'ex.: done' },
157
+ { key: 'capabilities', label: 'Capacidades', kind: 'capabilities' },
158
+ { key: 'actions', label: 'Ações por item', kind: 'card-actions' },
159
+ { key: 'showProgress', label: 'Mostrar progresso (N/M)', kind: 'toggle' },
160
+ { key: 'maxVisible', label: 'Máx. visíveis', kind: 'number' },
161
+ ...COMMON,
162
+ ],
163
+ alert: [
164
+ FIELD,
165
+ { key: 'tone', label: 'Tom', kind: 'select', options: ['', ...TONES] },
166
+ { key: 'icon', label: 'Ícone (icon:*)', kind: 'text' },
167
+ ...COMMON,
168
+ ],
169
+ progress: [
170
+ FIELD,
171
+ { key: 'max', label: 'Máximo', kind: 'number' },
172
+ { key: 'tone', label: 'Tom', kind: 'select', options: ['', ...TONES] },
173
+ ...COMMON,
174
+ ],
175
+ divider: [...COMMON],
176
+ actions: [{ key: 'items', label: 'Ações', kind: 'card-actions' }, ...COMMON],
177
+ row: [
178
+ { key: 'align', label: 'Alinhamento', kind: 'select', options: ['', 'start', 'center', 'end', 'between'] },
179
+ { key: 'wrap', label: 'Quebrar linha', kind: 'toggle' },
180
+ ...COMMON,
181
+ ],
182
+ custom: [
183
+ { key: 'renderer', label: 'Renderer (renderer:*)', kind: 'text', hint: 'Nome no registry do host' },
184
+ ...COMMON,
185
+ ],
186
+ };
187
+ /** Tipos de bloco oferecidos no "adicionar bloco" (ordem de exibição). */
188
+ const BLOCK_TYPE_LABELS = [
189
+ { type: 'text', label: 'Texto' },
190
+ { type: 'number', label: 'Número' },
191
+ { type: 'date', label: 'Data' },
192
+ { type: 'time', label: 'Hora' },
193
+ { type: 'datetime', label: 'Data e hora' },
194
+ { type: 'image', label: 'Imagem' },
195
+ { type: 'tag', label: 'Tag' },
196
+ { type: 'badge', label: 'Selo (badge)' },
197
+ { type: 'icon', label: 'Ícone' },
198
+ { type: 'avatar', label: 'Avatar' },
199
+ { type: 'facts', label: 'Fatos (ícone + valor)' },
200
+ { type: 'items', label: 'Itens internos (checklist/ações)' },
201
+ { type: 'alert', label: 'Alerta' },
202
+ { type: 'progress', label: 'Progresso' },
203
+ { type: 'divider', label: 'Divisor' },
204
+ { type: 'actions', label: 'Ações do cartão' },
205
+ { type: 'row', label: 'Linha (composição horizontal)' },
206
+ { type: 'custom', label: 'Custom (renderer do host)' },
207
+ ];
208
+
209
+ const DEFAULT_CONFIG = {
210
+ columns: {
211
+ groups: [
212
+ { id: 'todo', title: 'A Fazer' },
213
+ { id: 'doing', title: 'Fazendo' },
214
+ { id: 'done', title: 'Feito' },
215
+ ],
216
+ },
217
+ card: { blocks: [{ type: 'text', field: 'title', variant: 'title' }] },
218
+ items: [
219
+ { id: 'item-1', column: 'todo', title: 'Primeiro cartão' },
220
+ { id: 'item-2', column: 'doing', title: 'Segundo cartão' },
221
+ ],
222
+ };
223
+ /**
224
+ * Estado do Designer — instância POR componente (provider do
225
+ * MosKanbanDesigner; nunca singleton de módulo). Fonte da verdade é um
226
+ * BoardConfig serializável em forma canônica de edição (eixos como objeto);
227
+ * toda mutação passa por `update()` (clone → muta → valida → re-preview).
228
+ */
229
+ class DesignerStore {
230
+ config = signal(structuredClone(DEFAULT_CONFIG), ...(ngDevMode ? [{ debugName: "config" }] : /* istanbul ignore next */ []));
231
+ theme = signal({}, ...(ngDevMode ? [{ debugName: "theme" }] : /* istanbul ignore next */ []));
232
+ registry;
233
+ /** Validação contínua da config em edição. */
234
+ validation = computed(() => validateConfig(this.config()), ...(ngDevMode ? [{ debugName: "validation" }] : /* istanbul ignore next */ []));
235
+ errors = computed(() => this.validation().errors, ...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
236
+ /** Board do preview — recriado a cada config VÁLIDA (a inválida mantém o
237
+ * último preview bom, com os erros listados ao lado). */
238
+ lastGood = null;
239
+ previewBoard = computed(() => {
240
+ const cfg = this.config();
241
+ if (!validateConfig(cfg).valid)
242
+ return this.lastGood;
243
+ try {
244
+ this.lastGood?.destroy();
245
+ this.lastGood = createBoard(structuredClone(cfg), { registry: this.registry });
246
+ }
247
+ catch {
248
+ /* estrutural já validado; regra de runtime inesperada mantém o último */
249
+ }
250
+ return this.lastGood;
251
+ }, ...(ngDevMode ? [{ debugName: "previewBoard" }] : /* istanbul ignore next */ []));
252
+ /** String de estilo com os tokens alterados (aplicada no preview). */
253
+ themeStyle = computed(() => Object.entries(this.theme())
254
+ .map(([token, value]) => `${token}: ${value}`)
255
+ .join('; '), ...(ngDevMode ? [{ debugName: "themeStyle" }] : /* istanbul ignore next */ []));
256
+ // ---- mutações ---------------------------------------------------------------
257
+ /** Toda mutação: clona, aplica, publica (imutável p/ OnPush). */
258
+ update(mutator) {
259
+ const draft = structuredClone(this.config());
260
+ mutator(draft);
261
+ this.config.set(draft);
262
+ }
263
+ load(config, theme) {
264
+ this.config.set(this.canonicalize(structuredClone(config)));
265
+ if (theme)
266
+ this.theme.set({ ...theme });
267
+ }
268
+ setToken(token, value) {
269
+ const def = TOKEN_GROUPS.flatMap((g) => g.tokens).find((t) => t.token === token);
270
+ const next = { ...this.theme() };
271
+ if (!value || value === def?.default)
272
+ delete next[token];
273
+ else
274
+ next[token] = value;
275
+ this.theme.set(next);
276
+ }
277
+ resetTheme() {
278
+ this.theme.set({});
279
+ }
280
+ // ---- eixos em forma canônica --------------------------------------------------
281
+ /** Eixo de colunas SEMPRE como objeto (açúcar de array normalizado). */
282
+ columnsAxis() {
283
+ return this.axisOf(this.config().columns);
284
+ }
285
+ lanesAxis() {
286
+ const lanes = this.config().lanes;
287
+ return lanes ? this.axisOf(lanes) : null;
288
+ }
289
+ axisOf(axis) {
290
+ if (Array.isArray(axis)) {
291
+ return {
292
+ groups: axis.map((g) => (typeof g === 'string' ? { id: g } : { ...g })),
293
+ };
294
+ }
295
+ return {
296
+ ...axis,
297
+ groups: axis.groups?.map((g) => (typeof g === 'string' ? { id: g } : { ...g })),
298
+ };
299
+ }
300
+ /** Importa qualquer config para a forma canônica de edição. */
301
+ canonicalize(config) {
302
+ config.columns = this.axisOf(config.columns);
303
+ if (config.lanes)
304
+ config.lanes = this.axisOf(config.lanes);
305
+ return config;
306
+ }
307
+ // ---- import / export ----------------------------------------------------------
308
+ /** Aceita BoardConfig puro OU KanbanDesignFile; retorna erros sem aplicar
309
+ * quando a config é inválida. */
310
+ import(json) {
311
+ let parsed;
312
+ try {
313
+ parsed = JSON.parse(json);
314
+ }
315
+ catch (e) {
316
+ return { ok: false, errors: [{ path: '', message: `JSON inválido: ${e.message}` }] };
317
+ }
318
+ const asFile = parsed;
319
+ const isDesignFile = !!asFile.config;
320
+ const config = (asFile.config ?? parsed);
321
+ const result = validateConfig(config);
322
+ if (!result.valid)
323
+ return { ok: false, errors: result.errors };
324
+ // Design file descreve o design INTEIRO: sem `theme` = tema no padrão, o
325
+ // que ZERA os tokens em edição. BoardConfig puro só traz estrutura, então
326
+ // preserva o tema atual.
327
+ this.load(config, isDesignFile ? (asFile.theme ?? {}) : undefined);
328
+ return { ok: true, errors: [] };
329
+ }
330
+ exportConfigJson() {
331
+ return JSON.stringify(this.config(), null, 2);
332
+ }
333
+ exportDesignJson() {
334
+ const file = { version: 1, config: this.config() };
335
+ if (Object.keys(this.theme()).length)
336
+ file.theme = this.theme();
337
+ return JSON.stringify(file, null, 2);
338
+ }
339
+ /** Snippet CSS só com os tokens alterados (nada = string vazia). */
340
+ exportThemeCss() {
341
+ const entries = Object.entries(this.theme());
342
+ if (!entries.length)
343
+ return '';
344
+ const lines = entries.map(([token, value]) => ` ${token}: ${value};`);
345
+ return `/* Tema @mosaicoo/kanban — gerado pelo Designer */\n:root {\n${lines.join('\n')}\n}\n`;
346
+ }
347
+ // ---- rascunho local -------------------------------------------------------------
348
+ saveDraft(storageKey) {
349
+ try {
350
+ localStorage.setItem(storageKey, this.exportDesignJson());
351
+ }
352
+ catch {
353
+ /* storage cheio/indisponível: rascunho é conveniência, não requisito */
354
+ }
355
+ }
356
+ loadDraft(storageKey) {
357
+ try {
358
+ const raw = localStorage.getItem(storageKey);
359
+ return raw ? this.import(raw).ok : false;
360
+ }
361
+ catch {
362
+ return false;
363
+ }
364
+ }
365
+ clearDraft(storageKey) {
366
+ try {
367
+ localStorage.removeItem(storageKey);
368
+ }
369
+ catch {
370
+ /* idem */
371
+ }
372
+ }
373
+ destroy() {
374
+ this.lastGood?.destroy();
375
+ this.lastGood = null;
376
+ }
377
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: DesignerStore, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
378
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: DesignerStore });
379
+ }
380
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: DesignerStore, decorators: [{
381
+ type: Injectable
382
+ }] });
383
+
384
+ const TYPE_LABEL = new Map(BLOCK_TYPE_LABELS.map((t) => [t.type, t.label]));
385
+ /** Bloco novo mínimo VÁLIDO por tipo (campos obrigatórios preenchidos). */
386
+ function blankBlock(type) {
387
+ switch (type) {
388
+ case 'icon':
389
+ return { type, icon: 'star' };
390
+ case 'avatar':
391
+ return { type, nameField: 'owner' };
392
+ case 'facts':
393
+ return { type, items: [{ field: 'campo' }] };
394
+ case 'actions':
395
+ return { type, items: [{ id: 'acao-1', label: 'Ação' }] };
396
+ case 'divider':
397
+ return { type };
398
+ case 'row':
399
+ return { type, blocks: [] };
400
+ case 'custom':
401
+ return { type, renderer: 'renderer:custom' };
402
+ default:
403
+ return { type, field: 'campo' };
404
+ }
405
+ }
406
+ /**
407
+ * Editor do CARTÃO: árvore de blocos (com `row` de um nível) + painel de
408
+ * propriedades GERADO a partir de BLOCK_FIELDS — novos campos entram na
409
+ * tabela, não em template escrito à mão.
410
+ */
411
+ class MkbCardEditor {
412
+ store = inject(DesignerStore);
413
+ config = this.store.config;
414
+ blockTypes = BLOCK_TYPE_LABELS;
415
+ tones = ['', 'neutral', 'info', 'success', 'warning', 'danger'];
416
+ allCapabilities = ['check', 'reorder', 'move-across', 'action'];
417
+ newType = 'text';
418
+ selectedPath = signal(null, ...(ngDevMode ? [{ debugName: "selectedPath" }] : /* istanbul ignore next */ []));
419
+ jsonError = signal(null, ...(ngDevMode ? [{ debugName: "jsonError" }] : /* istanbul ignore next */ []));
420
+ selectedBlock = computed(() => {
421
+ const path = this.selectedPath();
422
+ return path ? this.blockAt(this.config().card?.blocks ?? [], path) : null;
423
+ }, ...(ngDevMode ? [{ debugName: "selectedBlock" }] : /* istanbul ignore next */ []));
424
+ blocks() {
425
+ return this.config().card?.blocks ?? [];
426
+ }
427
+ rowChildren(block) {
428
+ return block.type === 'row' ? block.blocks : [];
429
+ }
430
+ typeLabel(type) {
431
+ return TYPE_LABEL.get(type) ?? type;
432
+ }
433
+ summary(block) {
434
+ const b = block;
435
+ return String(b['field'] ?? b['icon'] ?? b['renderer'] ?? b['nameField'] ?? '');
436
+ }
437
+ fieldsFor(type) {
438
+ return BLOCK_FIELDS[type];
439
+ }
440
+ // ---- seleção / árvore --------------------------------------------------------
441
+ select(path) {
442
+ this.selectedPath.set(path);
443
+ this.jsonError.set(null);
444
+ }
445
+ isSelected(path) {
446
+ const sel = this.selectedPath();
447
+ return !!sel && sel.length === path.length && sel.every((v, i) => v === path[i]);
448
+ }
449
+ selectedIsRow() {
450
+ return this.selectedBlock()?.type === 'row';
451
+ }
452
+ blockAt(blocks, path) {
453
+ const top = blocks[path[0]];
454
+ if (!top)
455
+ return null;
456
+ if (path.length === 1)
457
+ return top;
458
+ return top.type === 'row' ? (top.blocks[path[1]] ?? null) : null;
459
+ }
460
+ /** Lista IRMÃ do caminho (raiz ou blocos de uma row) dentro do draft. */
461
+ siblingsAt(draft, path) {
462
+ const blocks = draft.card?.blocks;
463
+ if (!blocks)
464
+ return null;
465
+ if (path.length === 1)
466
+ return blocks;
467
+ const parent = blocks[path[0]];
468
+ return parent?.type === 'row' ? parent.blocks : null;
469
+ }
470
+ add(intoSelectedRow) {
471
+ const block = blankBlock(this.newType);
472
+ const rowPath = intoSelectedRow ? this.selectedPath() : null;
473
+ this.store.update((d) => {
474
+ d.card ??= { blocks: [] };
475
+ if (rowPath) {
476
+ const row = this.blockAt(d.card.blocks, rowPath);
477
+ if (row?.type === 'row') {
478
+ row.blocks.push(block);
479
+ return;
480
+ }
481
+ }
482
+ d.card.blocks.push(block);
483
+ });
484
+ if (rowPath) {
485
+ const row = this.blockAt(this.blocks(), rowPath);
486
+ this.select([...rowPath, (row?.type === 'row' ? row.blocks.length : 1) - 1]);
487
+ }
488
+ else {
489
+ this.select([this.blocks().length - 1]);
490
+ }
491
+ }
492
+ remove(path) {
493
+ this.store.update((d) => {
494
+ this.siblingsAt(d, path)?.splice(path[path.length - 1], 1);
495
+ });
496
+ this.selectedPath.set(null);
497
+ }
498
+ move(path, delta) {
499
+ const index = path[path.length - 1];
500
+ const target = index + delta;
501
+ let moved = false;
502
+ this.store.update((d) => {
503
+ const siblings = this.siblingsAt(d, path);
504
+ if (!siblings || target < 0 || target >= siblings.length)
505
+ return;
506
+ const [b] = siblings.splice(index, 1);
507
+ siblings.splice(target, 0, b);
508
+ moved = true;
509
+ });
510
+ if (moved && this.isSelected(path)) {
511
+ this.select([...path.slice(0, -1), target]);
512
+ }
513
+ }
514
+ // ---- leitura de propriedades ----------------------------------------------------
515
+ str(block, key) {
516
+ const v = block[key];
517
+ return typeof v === 'string' ? v : '';
518
+ }
519
+ num(block, key) {
520
+ const v = block[key];
521
+ return typeof v === 'number' ? v : null;
522
+ }
523
+ bool(block, key) {
524
+ return block[key] === true;
525
+ }
526
+ csv(block, key) {
527
+ const v = block[key];
528
+ return Array.isArray(v) ? v.join(' ') : typeof v === 'string' ? v : '';
529
+ }
530
+ json(block, key) {
531
+ const v = block[key];
532
+ return v === undefined ? '' : JSON.stringify(v, null, 2);
533
+ }
534
+ facts(block) {
535
+ return block.type === 'facts' ? block.items : [];
536
+ }
537
+ actions(block, key) {
538
+ const v = block[key];
539
+ return Array.isArray(v) ? v : [];
540
+ }
541
+ hasCapability(block, cap) {
542
+ return block.type === 'items' && !!block.capabilities?.includes(cap);
543
+ }
544
+ whenColumns(action) {
545
+ const col = action.when?.column;
546
+ return Array.isArray(col) ? col.join(', ') : (col ?? '');
547
+ }
548
+ // ---- escrita de propriedades ------------------------------------------------------
549
+ /** Escreve no bloco selecionado; undefined REMOVE a chave. */
550
+ setProp(key, value) {
551
+ const path = this.selectedPath();
552
+ if (!path)
553
+ return;
554
+ this.store.update((d) => {
555
+ const block = this.blockAt(d.card?.blocks ?? [], path);
556
+ if (!block)
557
+ return;
558
+ if (value === undefined)
559
+ delete block[key];
560
+ else
561
+ block[key] = value;
562
+ });
563
+ }
564
+ setCsv(key, raw) {
565
+ const list = raw.split(/[\s,]+/).filter(Boolean);
566
+ this.setProp(key, list.length ? list : undefined);
567
+ }
568
+ setJson(key, event) {
569
+ const raw = event.target.value.trim();
570
+ if (!raw) {
571
+ this.jsonError.set(null);
572
+ this.setProp(key, undefined);
573
+ return;
574
+ }
575
+ try {
576
+ this.setProp(key, JSON.parse(raw));
577
+ this.jsonError.set(null);
578
+ }
579
+ catch (e) {
580
+ this.jsonError.set(`JSON inválido: ${e.message}`);
581
+ }
582
+ }
583
+ toggleCapability(cap, enabled) {
584
+ const block = this.selectedBlock();
585
+ if (block?.type !== 'items')
586
+ return;
587
+ const caps = new Set(block.capabilities ?? []);
588
+ if (enabled)
589
+ caps.add(cap);
590
+ else
591
+ caps.delete(cap);
592
+ this.setProp('capabilities', caps.size ? [...caps] : undefined);
593
+ }
594
+ // ---- listas internas (facts / actions) --------------------------------------------
595
+ addFact(key) {
596
+ this.mutateList(key, (list) => list.push({ field: 'campo' }));
597
+ }
598
+ setFact(index, prop, value) {
599
+ this.mutateList('items', (list) => {
600
+ const fact = list[index];
601
+ if (!fact)
602
+ return;
603
+ if (value === undefined)
604
+ delete fact[prop];
605
+ else
606
+ fact[prop] = value;
607
+ });
608
+ }
609
+ addAction(key) {
610
+ this.mutateList(key, (list) => list.push({ id: `acao-${list.length + 1}`, label: 'Ação' }));
611
+ }
612
+ setAction(key, index, prop, value) {
613
+ this.mutateList(key, (list) => {
614
+ const action = list[index];
615
+ if (!action)
616
+ return;
617
+ if (value === undefined)
618
+ delete action[prop];
619
+ else
620
+ action[prop] = value;
621
+ });
622
+ }
623
+ setActionWhen(key, index, raw) {
624
+ const columns = raw.split(/[\s,]+/).filter(Boolean);
625
+ this.mutateList(key, (list) => {
626
+ const action = list[index];
627
+ if (!action)
628
+ return;
629
+ if (!columns.length) {
630
+ if (action.when)
631
+ delete action.when.column;
632
+ if (action.when && !action.when.lane)
633
+ delete action.when;
634
+ }
635
+ else {
636
+ action.when = { ...(action.when ?? {}), column: columns.length === 1 ? columns[0] : columns };
637
+ }
638
+ });
639
+ }
640
+ removeListItem(key, index) {
641
+ this.mutateList(key, (list) => list.splice(index, 1));
642
+ }
643
+ mutateList(key, mutator) {
644
+ const path = this.selectedPath();
645
+ if (!path)
646
+ return;
647
+ this.store.update((d) => {
648
+ const block = this.blockAt(d.card?.blocks ?? [], path);
649
+ if (!block)
650
+ return;
651
+ block[key] ??= [];
652
+ mutator(block[key]);
653
+ });
654
+ }
655
+ // ---- geral -------------------------------------------------------------------------
656
+ setDragHandle(value) {
657
+ this.store.update((d) => {
658
+ d.card ??= { blocks: [] };
659
+ if (value)
660
+ d.card.dragHandle = true;
661
+ else
662
+ delete d.card.dragHandle;
663
+ });
664
+ }
665
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: MkbCardEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
666
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.18", type: MkbCardEditor, isStandalone: true, selector: "mkb-card-editor", ngImport: i0, template: `
667
+ <div class="mkbd-row">
668
+ <mat-slide-toggle
669
+ [ngModel]="config().card?.dragHandle ?? false"
670
+ (ngModelChange)="setDragHandle($event)"
671
+ >
672
+ Arrastar só pela alça ⠿
673
+ </mat-slide-toggle>
674
+ </div>
675
+
676
+ <h3 class="mkbd-h">Blocos do cartão</h3>
677
+ <div class="mkbd-tree" role="tree" aria-label="Blocos do cartão">
678
+ @for (block of blocks(); track $index; let i = $index) {
679
+ <ng-container
680
+ *ngTemplateOutlet="blockRow; context: { block, path: [i], siblings: blocks().length }"
681
+ />
682
+ @if (block.type === 'row') {
683
+ @for (child of rowChildren(block); track $index; let j = $index) {
684
+ <ng-container
685
+ *ngTemplateOutlet="
686
+ blockRow;
687
+ context: { block: child, path: [i, j], siblings: rowChildren(block).length }
688
+ "
689
+ />
690
+ }
691
+ }
692
+ } @empty {
693
+ <p class="mkbd-hint">Nenhum bloco — o cartão renderiza vazio. Adicione abaixo.</p>
694
+ }
695
+ </div>
696
+
697
+ <ng-template #blockRow let-block="block" let-path="path" let-siblings="siblings">
698
+ <div
699
+ class="mkbd-node"
700
+ role="treeitem"
701
+ tabindex="0"
702
+ [class.mkbd-node-child]="path.length > 1"
703
+ [class.mkbd-node-selected]="isSelected(path)"
704
+ [attr.aria-selected]="isSelected(path)"
705
+ (click)="select(path)"
706
+ (keydown.enter)="select(path)"
707
+ >
708
+ <span class="mkbd-node-type">{{ typeLabel(block.type) }}</span>
709
+ <span class="mkbd-node-summary">{{ summary(block) }}</span>
710
+ <span class="mkbd-spacer"></span>
711
+ <button matIconButton type="button" (click)="move(path, -1); $event.stopPropagation()" [disabled]="path[path.length - 1] === 0" aria-label="Subir bloco">↑</button>
712
+ <button matIconButton type="button" (click)="move(path, 1); $event.stopPropagation()" [disabled]="path[path.length - 1] === siblings - 1" aria-label="Descer bloco">↓</button>
713
+ <button matIconButton type="button" (click)="remove(path); $event.stopPropagation()" aria-label="Remover bloco">✕</button>
714
+ </div>
715
+ </ng-template>
716
+
717
+ <div class="mkbd-row mkbd-mt">
718
+ <mat-form-field appearance="outline" class="mkbd-slim">
719
+ <mat-label>Tipo do novo bloco</mat-label>
720
+ <mat-select [(ngModel)]="newType">
721
+ @for (t of blockTypes; track t.type) {
722
+ <mat-option [value]="t.type">{{ t.label }}</mat-option>
723
+ }
724
+ </mat-select>
725
+ </mat-form-field>
726
+ <button matButton type="button" (click)="add(false)">+ Adicionar</button>
727
+ @if (selectedIsRow()) {
728
+ <button matButton type="button" (click)="add(true)">+ Adicionar DENTRO da linha</button>
729
+ }
730
+ </div>
731
+
732
+ @if (selectedBlock(); as block) {
733
+ <h3 class="mkbd-h">Propriedades — {{ typeLabel(block.type) }}</h3>
734
+ <div class="mkbd-props">
735
+ @for (def of fieldsFor(block.type); track def.key) {
736
+ @switch (def.kind) {
737
+ @case ('text') {
738
+ <mat-form-field appearance="outline" class="mkbd-slim">
739
+ <mat-label>{{ def.label }}</mat-label>
740
+ <input
741
+ matInput
742
+ [ngModel]="str(block, def.key)"
743
+ (ngModelChange)="setProp(def.key, $event || undefined)"
744
+ />
745
+ @if (def.hint) { <mat-hint>{{ def.hint }}</mat-hint> }
746
+ </mat-form-field>
747
+ }
748
+ @case ('number') {
749
+ <mat-form-field appearance="outline" class="mkbd-tiny">
750
+ <mat-label>{{ def.label }}</mat-label>
751
+ <input
752
+ matInput
753
+ type="number"
754
+ [ngModel]="num(block, def.key)"
755
+ (ngModelChange)="setProp(def.key, $event === null || $event === '' ? undefined : +$event)"
756
+ />
757
+ </mat-form-field>
758
+ }
759
+ @case ('toggle') {
760
+ <mat-slide-toggle
761
+ [ngModel]="bool(block, def.key)"
762
+ (ngModelChange)="setProp(def.key, $event || undefined)"
763
+ >
764
+ {{ def.label }}
765
+ </mat-slide-toggle>
766
+ }
767
+ @case ('select') {
768
+ <mat-form-field appearance="outline" class="mkbd-slim">
769
+ <mat-label>{{ def.label }}</mat-label>
770
+ <mat-select
771
+ [ngModel]="str(block, def.key)"
772
+ (ngModelChange)="setProp(def.key, $event || undefined)"
773
+ >
774
+ @for (opt of def.options; track opt) {
775
+ <mat-option [value]="opt">{{ opt || '—' }}</mat-option>
776
+ }
777
+ </mat-select>
778
+ </mat-form-field>
779
+ }
780
+ @case ('csv') {
781
+ <mat-form-field appearance="outline" class="mkbd-grow">
782
+ <mat-label>{{ def.label }}</mat-label>
783
+ <input
784
+ matInput
785
+ [ngModel]="csv(block, def.key)"
786
+ (ngModelChange)="setCsv(def.key, $event)"
787
+ />
788
+ </mat-form-field>
789
+ }
790
+ @case ('json') {
791
+ <mat-form-field appearance="outline" class="mkbd-full">
792
+ <mat-label>{{ def.label }}</mat-label>
793
+ <textarea
794
+ matInput
795
+ rows="4"
796
+ [value]="json(block, def.key)"
797
+ (change)="setJson(def.key, $event)"
798
+ ></textarea>
799
+ <mat-hint [class.mkbd-error]="jsonError()">
800
+ {{ jsonError() || def.hint || 'Objeto JSON; vazio remove.' }}
801
+ </mat-hint>
802
+ </mat-form-field>
803
+ }
804
+ @case ('capabilities') {
805
+ <div class="mkbd-caps">
806
+ <span class="mkbd-caps-label">{{ def.label }}:</span>
807
+ @for (cap of allCapabilities; track cap) {
808
+ <mat-checkbox
809
+ [ngModel]="hasCapability(block, cap)"
810
+ (ngModelChange)="toggleCapability(cap, $event)"
811
+ >
812
+ {{ cap }}
813
+ </mat-checkbox>
814
+ }
815
+ </div>
816
+ }
817
+ @case ('facts') {
818
+ <div class="mkbd-sub">
819
+ <span class="mkbd-caps-label">{{ def.label }}</span>
820
+ @for (fact of facts(block); track $index; let fi = $index) {
821
+ <div class="mkbd-subrow">
822
+ <mat-form-field appearance="outline" class="mkbd-tiny2">
823
+ <mat-label>Campo</mat-label>
824
+ <input matInput [ngModel]="fact.field" (ngModelChange)="setFact(fi, 'field', $event)" />
825
+ </mat-form-field>
826
+ <mat-form-field appearance="outline" class="mkbd-tiny2">
827
+ <mat-label>Ícone</mat-label>
828
+ <input matInput [ngModel]="fact.icon ?? ''" (ngModelChange)="setFact(fi, 'icon', $event || undefined)" />
829
+ </mat-form-field>
830
+ <mat-form-field appearance="outline" class="mkbd-tiny2">
831
+ <mat-label>Rótulo</mat-label>
832
+ <input matInput [ngModel]="fact.label ?? ''" (ngModelChange)="setFact(fi, 'label', $event || undefined)" />
833
+ </mat-form-field>
834
+ <mat-form-field appearance="outline" class="mkbd-tiny2">
835
+ <mat-label>Prefixo</mat-label>
836
+ <input matInput [ngModel]="fact.prefix ?? ''" (ngModelChange)="setFact(fi, 'prefix', $event || undefined)" />
837
+ </mat-form-field>
838
+ <mat-form-field appearance="outline" class="mkbd-tiny2">
839
+ <mat-label>Sufixo</mat-label>
840
+ <input matInput [ngModel]="fact.suffix ?? ''" (ngModelChange)="setFact(fi, 'suffix', $event || undefined)" />
841
+ </mat-form-field>
842
+ <button matIconButton type="button" (click)="removeListItem(def.key, fi)" aria-label="Remover fato">✕</button>
843
+ </div>
844
+ }
845
+ <button matButton type="button" (click)="addFact(def.key)">+ Fato</button>
846
+ </div>
847
+ }
848
+ @case ('card-actions') {
849
+ <div class="mkbd-sub">
850
+ <span class="mkbd-caps-label">{{ def.label }}</span>
851
+ @for (action of actions(block, def.key); track $index; let ai = $index) {
852
+ <div class="mkbd-subrow">
853
+ <mat-form-field appearance="outline" class="mkbd-tiny2">
854
+ <mat-label>Id</mat-label>
855
+ <input matInput [ngModel]="action.id" (ngModelChange)="setAction(def.key, ai, 'id', $event)" />
856
+ </mat-form-field>
857
+ <mat-form-field appearance="outline" class="mkbd-tiny2">
858
+ <mat-label>Rótulo</mat-label>
859
+ <input matInput [ngModel]="action.label" (ngModelChange)="setAction(def.key, ai, 'label', $event)" />
860
+ </mat-form-field>
861
+ <mat-form-field appearance="outline" class="mkbd-tiny2">
862
+ <mat-label>Ícone</mat-label>
863
+ <input matInput [ngModel]="action.icon ?? ''" (ngModelChange)="setAction(def.key, ai, 'icon', $event || undefined)" />
864
+ </mat-form-field>
865
+ <mat-form-field appearance="outline" class="mkbd-tiny2">
866
+ <mat-label>Tom</mat-label>
867
+ <mat-select [ngModel]="action.tone ?? ''" (ngModelChange)="setAction(def.key, ai, 'tone', $event || undefined)">
868
+ @for (tone of tones; track tone) {
869
+ <mat-option [value]="tone">{{ tone || '—' }}</mat-option>
870
+ }
871
+ </mat-select>
872
+ </mat-form-field>
873
+ <mat-form-field appearance="outline" class="mkbd-tiny2">
874
+ <mat-label>Só nas colunas</mat-label>
875
+ <input
876
+ matInput
877
+ placeholder="col-a, col-b"
878
+ [ngModel]="whenColumns(action)"
879
+ (ngModelChange)="setActionWhen(def.key, ai, $event)"
880
+ />
881
+ </mat-form-field>
882
+ <button matIconButton type="button" (click)="removeListItem(def.key, ai)" aria-label="Remover ação">✕</button>
883
+ </div>
884
+ }
885
+ <button matButton type="button" (click)="addAction(def.key)">+ Ação</button>
886
+ </div>
887
+ }
888
+ }
889
+ }
890
+ </div>
891
+ } @else {
892
+ <p class="mkbd-hint mkbd-mt">Selecione um bloco para editar as propriedades.</p>
893
+ }
894
+ `, isInline: true, styles: [":host{display:block}.mkbd-h{margin:18px 0 8px;font-size:13px;font-weight:600}.mkbd-hint{font-size:11.5px;color:var(--mkb-muted, #6f6a7c)}.mkbd-row{display:flex;flex-wrap:wrap;gap:10px;align-items:center}.mkbd-mt{margin-top:12px}.mkbd-tree{display:flex;flex-direction:column;gap:3px}.mkbd-node{display:flex;align-items:center;gap:8px;padding:3px 10px;border:1px solid var(--mkb-border, #e3e0ea);border-radius:8px;cursor:pointer;background:var(--mkb-card-bg, #fff)}.mkbd-node:focus-visible{outline:2px solid var(--mkb-accent, #5e35b1);outline-offset:1px}.mkbd-node-child{margin-left:26px}.mkbd-node-selected{border-color:var(--mkb-accent, #5e35b1);box-shadow:inset 2px 0 0 var(--mkb-accent, #5e35b1)}.mkbd-node-type{font-size:12px;font-weight:600}.mkbd-node-summary{font-size:11.5px;color:var(--mkb-muted, #6f6a7c);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mkbd-spacer{flex:1}.mkbd-props{display:flex;flex-wrap:wrap;gap:18px 10px;align-items:center}.mkbd-slim{width:200px}.mkbd-tiny{width:110px}.mkbd-tiny2{width:130px}.mkbd-grow{flex:1;min-width:220px}.mkbd-full{width:100%}.mkbd-caps,.mkbd-sub{display:flex;flex-wrap:wrap;gap:10px;align-items:center;width:100%}.mkbd-sub{flex-direction:column;align-items:stretch;gap:4px}.mkbd-subrow{display:flex;flex-wrap:wrap;gap:8px;align-items:center}.mkbd-caps-label{font-size:12px;color:var(--mkb-muted, #6f6a7c)}.mkbd-error{color:var(--mkb-danger, #b3261e)}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i2.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i2.MatLabel, selector: "mat-label" }, { kind: "directive", type: i2.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i3.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i4.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i4.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i5.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i6.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i7.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i7.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
895
+ }
896
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: MkbCardEditor, decorators: [{
897
+ type: Component,
898
+ args: [{ selector: 'mkb-card-editor', changeDetection: ChangeDetectionStrategy.OnPush, imports: [
899
+ NgTemplateOutlet,
900
+ FormsModule,
901
+ MatFormFieldModule,
902
+ MatInputModule,
903
+ MatSelectModule,
904
+ MatSlideToggleModule,
905
+ MatCheckboxModule,
906
+ MatButtonModule,
907
+ ], template: `
908
+ <div class="mkbd-row">
909
+ <mat-slide-toggle
910
+ [ngModel]="config().card?.dragHandle ?? false"
911
+ (ngModelChange)="setDragHandle($event)"
912
+ >
913
+ Arrastar só pela alça ⠿
914
+ </mat-slide-toggle>
915
+ </div>
916
+
917
+ <h3 class="mkbd-h">Blocos do cartão</h3>
918
+ <div class="mkbd-tree" role="tree" aria-label="Blocos do cartão">
919
+ @for (block of blocks(); track $index; let i = $index) {
920
+ <ng-container
921
+ *ngTemplateOutlet="blockRow; context: { block, path: [i], siblings: blocks().length }"
922
+ />
923
+ @if (block.type === 'row') {
924
+ @for (child of rowChildren(block); track $index; let j = $index) {
925
+ <ng-container
926
+ *ngTemplateOutlet="
927
+ blockRow;
928
+ context: { block: child, path: [i, j], siblings: rowChildren(block).length }
929
+ "
930
+ />
931
+ }
932
+ }
933
+ } @empty {
934
+ <p class="mkbd-hint">Nenhum bloco — o cartão renderiza vazio. Adicione abaixo.</p>
935
+ }
936
+ </div>
937
+
938
+ <ng-template #blockRow let-block="block" let-path="path" let-siblings="siblings">
939
+ <div
940
+ class="mkbd-node"
941
+ role="treeitem"
942
+ tabindex="0"
943
+ [class.mkbd-node-child]="path.length > 1"
944
+ [class.mkbd-node-selected]="isSelected(path)"
945
+ [attr.aria-selected]="isSelected(path)"
946
+ (click)="select(path)"
947
+ (keydown.enter)="select(path)"
948
+ >
949
+ <span class="mkbd-node-type">{{ typeLabel(block.type) }}</span>
950
+ <span class="mkbd-node-summary">{{ summary(block) }}</span>
951
+ <span class="mkbd-spacer"></span>
952
+ <button matIconButton type="button" (click)="move(path, -1); $event.stopPropagation()" [disabled]="path[path.length - 1] === 0" aria-label="Subir bloco">↑</button>
953
+ <button matIconButton type="button" (click)="move(path, 1); $event.stopPropagation()" [disabled]="path[path.length - 1] === siblings - 1" aria-label="Descer bloco">↓</button>
954
+ <button matIconButton type="button" (click)="remove(path); $event.stopPropagation()" aria-label="Remover bloco">✕</button>
955
+ </div>
956
+ </ng-template>
957
+
958
+ <div class="mkbd-row mkbd-mt">
959
+ <mat-form-field appearance="outline" class="mkbd-slim">
960
+ <mat-label>Tipo do novo bloco</mat-label>
961
+ <mat-select [(ngModel)]="newType">
962
+ @for (t of blockTypes; track t.type) {
963
+ <mat-option [value]="t.type">{{ t.label }}</mat-option>
964
+ }
965
+ </mat-select>
966
+ </mat-form-field>
967
+ <button matButton type="button" (click)="add(false)">+ Adicionar</button>
968
+ @if (selectedIsRow()) {
969
+ <button matButton type="button" (click)="add(true)">+ Adicionar DENTRO da linha</button>
970
+ }
971
+ </div>
972
+
973
+ @if (selectedBlock(); as block) {
974
+ <h3 class="mkbd-h">Propriedades — {{ typeLabel(block.type) }}</h3>
975
+ <div class="mkbd-props">
976
+ @for (def of fieldsFor(block.type); track def.key) {
977
+ @switch (def.kind) {
978
+ @case ('text') {
979
+ <mat-form-field appearance="outline" class="mkbd-slim">
980
+ <mat-label>{{ def.label }}</mat-label>
981
+ <input
982
+ matInput
983
+ [ngModel]="str(block, def.key)"
984
+ (ngModelChange)="setProp(def.key, $event || undefined)"
985
+ />
986
+ @if (def.hint) { <mat-hint>{{ def.hint }}</mat-hint> }
987
+ </mat-form-field>
988
+ }
989
+ @case ('number') {
990
+ <mat-form-field appearance="outline" class="mkbd-tiny">
991
+ <mat-label>{{ def.label }}</mat-label>
992
+ <input
993
+ matInput
994
+ type="number"
995
+ [ngModel]="num(block, def.key)"
996
+ (ngModelChange)="setProp(def.key, $event === null || $event === '' ? undefined : +$event)"
997
+ />
998
+ </mat-form-field>
999
+ }
1000
+ @case ('toggle') {
1001
+ <mat-slide-toggle
1002
+ [ngModel]="bool(block, def.key)"
1003
+ (ngModelChange)="setProp(def.key, $event || undefined)"
1004
+ >
1005
+ {{ def.label }}
1006
+ </mat-slide-toggle>
1007
+ }
1008
+ @case ('select') {
1009
+ <mat-form-field appearance="outline" class="mkbd-slim">
1010
+ <mat-label>{{ def.label }}</mat-label>
1011
+ <mat-select
1012
+ [ngModel]="str(block, def.key)"
1013
+ (ngModelChange)="setProp(def.key, $event || undefined)"
1014
+ >
1015
+ @for (opt of def.options; track opt) {
1016
+ <mat-option [value]="opt">{{ opt || '—' }}</mat-option>
1017
+ }
1018
+ </mat-select>
1019
+ </mat-form-field>
1020
+ }
1021
+ @case ('csv') {
1022
+ <mat-form-field appearance="outline" class="mkbd-grow">
1023
+ <mat-label>{{ def.label }}</mat-label>
1024
+ <input
1025
+ matInput
1026
+ [ngModel]="csv(block, def.key)"
1027
+ (ngModelChange)="setCsv(def.key, $event)"
1028
+ />
1029
+ </mat-form-field>
1030
+ }
1031
+ @case ('json') {
1032
+ <mat-form-field appearance="outline" class="mkbd-full">
1033
+ <mat-label>{{ def.label }}</mat-label>
1034
+ <textarea
1035
+ matInput
1036
+ rows="4"
1037
+ [value]="json(block, def.key)"
1038
+ (change)="setJson(def.key, $event)"
1039
+ ></textarea>
1040
+ <mat-hint [class.mkbd-error]="jsonError()">
1041
+ {{ jsonError() || def.hint || 'Objeto JSON; vazio remove.' }}
1042
+ </mat-hint>
1043
+ </mat-form-field>
1044
+ }
1045
+ @case ('capabilities') {
1046
+ <div class="mkbd-caps">
1047
+ <span class="mkbd-caps-label">{{ def.label }}:</span>
1048
+ @for (cap of allCapabilities; track cap) {
1049
+ <mat-checkbox
1050
+ [ngModel]="hasCapability(block, cap)"
1051
+ (ngModelChange)="toggleCapability(cap, $event)"
1052
+ >
1053
+ {{ cap }}
1054
+ </mat-checkbox>
1055
+ }
1056
+ </div>
1057
+ }
1058
+ @case ('facts') {
1059
+ <div class="mkbd-sub">
1060
+ <span class="mkbd-caps-label">{{ def.label }}</span>
1061
+ @for (fact of facts(block); track $index; let fi = $index) {
1062
+ <div class="mkbd-subrow">
1063
+ <mat-form-field appearance="outline" class="mkbd-tiny2">
1064
+ <mat-label>Campo</mat-label>
1065
+ <input matInput [ngModel]="fact.field" (ngModelChange)="setFact(fi, 'field', $event)" />
1066
+ </mat-form-field>
1067
+ <mat-form-field appearance="outline" class="mkbd-tiny2">
1068
+ <mat-label>Ícone</mat-label>
1069
+ <input matInput [ngModel]="fact.icon ?? ''" (ngModelChange)="setFact(fi, 'icon', $event || undefined)" />
1070
+ </mat-form-field>
1071
+ <mat-form-field appearance="outline" class="mkbd-tiny2">
1072
+ <mat-label>Rótulo</mat-label>
1073
+ <input matInput [ngModel]="fact.label ?? ''" (ngModelChange)="setFact(fi, 'label', $event || undefined)" />
1074
+ </mat-form-field>
1075
+ <mat-form-field appearance="outline" class="mkbd-tiny2">
1076
+ <mat-label>Prefixo</mat-label>
1077
+ <input matInput [ngModel]="fact.prefix ?? ''" (ngModelChange)="setFact(fi, 'prefix', $event || undefined)" />
1078
+ </mat-form-field>
1079
+ <mat-form-field appearance="outline" class="mkbd-tiny2">
1080
+ <mat-label>Sufixo</mat-label>
1081
+ <input matInput [ngModel]="fact.suffix ?? ''" (ngModelChange)="setFact(fi, 'suffix', $event || undefined)" />
1082
+ </mat-form-field>
1083
+ <button matIconButton type="button" (click)="removeListItem(def.key, fi)" aria-label="Remover fato">✕</button>
1084
+ </div>
1085
+ }
1086
+ <button matButton type="button" (click)="addFact(def.key)">+ Fato</button>
1087
+ </div>
1088
+ }
1089
+ @case ('card-actions') {
1090
+ <div class="mkbd-sub">
1091
+ <span class="mkbd-caps-label">{{ def.label }}</span>
1092
+ @for (action of actions(block, def.key); track $index; let ai = $index) {
1093
+ <div class="mkbd-subrow">
1094
+ <mat-form-field appearance="outline" class="mkbd-tiny2">
1095
+ <mat-label>Id</mat-label>
1096
+ <input matInput [ngModel]="action.id" (ngModelChange)="setAction(def.key, ai, 'id', $event)" />
1097
+ </mat-form-field>
1098
+ <mat-form-field appearance="outline" class="mkbd-tiny2">
1099
+ <mat-label>Rótulo</mat-label>
1100
+ <input matInput [ngModel]="action.label" (ngModelChange)="setAction(def.key, ai, 'label', $event)" />
1101
+ </mat-form-field>
1102
+ <mat-form-field appearance="outline" class="mkbd-tiny2">
1103
+ <mat-label>Ícone</mat-label>
1104
+ <input matInput [ngModel]="action.icon ?? ''" (ngModelChange)="setAction(def.key, ai, 'icon', $event || undefined)" />
1105
+ </mat-form-field>
1106
+ <mat-form-field appearance="outline" class="mkbd-tiny2">
1107
+ <mat-label>Tom</mat-label>
1108
+ <mat-select [ngModel]="action.tone ?? ''" (ngModelChange)="setAction(def.key, ai, 'tone', $event || undefined)">
1109
+ @for (tone of tones; track tone) {
1110
+ <mat-option [value]="tone">{{ tone || '—' }}</mat-option>
1111
+ }
1112
+ </mat-select>
1113
+ </mat-form-field>
1114
+ <mat-form-field appearance="outline" class="mkbd-tiny2">
1115
+ <mat-label>Só nas colunas</mat-label>
1116
+ <input
1117
+ matInput
1118
+ placeholder="col-a, col-b"
1119
+ [ngModel]="whenColumns(action)"
1120
+ (ngModelChange)="setActionWhen(def.key, ai, $event)"
1121
+ />
1122
+ </mat-form-field>
1123
+ <button matIconButton type="button" (click)="removeListItem(def.key, ai)" aria-label="Remover ação">✕</button>
1124
+ </div>
1125
+ }
1126
+ <button matButton type="button" (click)="addAction(def.key)">+ Ação</button>
1127
+ </div>
1128
+ }
1129
+ }
1130
+ }
1131
+ </div>
1132
+ } @else {
1133
+ <p class="mkbd-hint mkbd-mt">Selecione um bloco para editar as propriedades.</p>
1134
+ }
1135
+ `, styles: [":host{display:block}.mkbd-h{margin:18px 0 8px;font-size:13px;font-weight:600}.mkbd-hint{font-size:11.5px;color:var(--mkb-muted, #6f6a7c)}.mkbd-row{display:flex;flex-wrap:wrap;gap:10px;align-items:center}.mkbd-mt{margin-top:12px}.mkbd-tree{display:flex;flex-direction:column;gap:3px}.mkbd-node{display:flex;align-items:center;gap:8px;padding:3px 10px;border:1px solid var(--mkb-border, #e3e0ea);border-radius:8px;cursor:pointer;background:var(--mkb-card-bg, #fff)}.mkbd-node:focus-visible{outline:2px solid var(--mkb-accent, #5e35b1);outline-offset:1px}.mkbd-node-child{margin-left:26px}.mkbd-node-selected{border-color:var(--mkb-accent, #5e35b1);box-shadow:inset 2px 0 0 var(--mkb-accent, #5e35b1)}.mkbd-node-type{font-size:12px;font-weight:600}.mkbd-node-summary{font-size:11.5px;color:var(--mkb-muted, #6f6a7c);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mkbd-spacer{flex:1}.mkbd-props{display:flex;flex-wrap:wrap;gap:18px 10px;align-items:center}.mkbd-slim{width:200px}.mkbd-tiny{width:110px}.mkbd-tiny2{width:130px}.mkbd-grow{flex:1;min-width:220px}.mkbd-full{width:100%}.mkbd-caps,.mkbd-sub{display:flex;flex-wrap:wrap;gap:10px;align-items:center;width:100%}.mkbd-sub{flex-direction:column;align-items:stretch;gap:4px}.mkbd-subrow{display:flex;flex-wrap:wrap;gap:8px;align-items:center}.mkbd-caps-label{font-size:12px;color:var(--mkb-muted, #6f6a7c)}.mkbd-error{color:var(--mkb-danger, #b3261e)}\n"] }]
1136
+ }] });
1137
+
1138
+ /** Converte texto digitado no tipo mais provável (números e booleanos). */
1139
+ function coerce(raw) {
1140
+ if (raw === 'true')
1141
+ return true;
1142
+ if (raw === 'false')
1143
+ return false;
1144
+ if (/^-?\d+(\.\d+)?$/.test(raw))
1145
+ return Number(raw);
1146
+ return raw;
1147
+ }
1148
+ /**
1149
+ * Editor de DADOS: os itens de exemplo do board (`config.items`), suas
1150
+ * entries (blocos `items` do cartão) e os filtros iniciais. São os dados
1151
+ * que viajam junto na exportação — no runtime real o host normalmente
1152
+ * substitui por um adapter.
1153
+ */
1154
+ class MkbDataEditor {
1155
+ store = inject(DesignerStore);
1156
+ config = this.store.config;
1157
+ filtersError = signal(null, ...(ngDevMode ? [{ debugName: "filtersError" }] : /* istanbul ignore next */ []));
1158
+ items() {
1159
+ return this.config().items ?? [];
1160
+ }
1161
+ columnField() {
1162
+ return this.store.columnsAxis().field ?? 'column';
1163
+ }
1164
+ laneField() {
1165
+ return this.store.lanesAxis()?.field ?? 'lane';
1166
+ }
1167
+ hasLanes() {
1168
+ return !!this.store.lanesAxis();
1169
+ }
1170
+ columnIds() {
1171
+ return (this.store.columnsAxis().groups ?? []).map((g) => g.id);
1172
+ }
1173
+ laneIds() {
1174
+ return (this.store.lanesAxis()?.groups ?? []).map((g) => g.id);
1175
+ }
1176
+ label(item) {
1177
+ const title = item['title'] ?? item['titulo'] ?? item['name'];
1178
+ return typeof title === 'string' && title ? title : item.id;
1179
+ }
1180
+ position(item) {
1181
+ const col = this.str(item, this.columnField());
1182
+ const lane = this.hasLanes() ? this.str(item, this.laneField()) : '';
1183
+ return lane ? `${col} · ${lane}` : col;
1184
+ }
1185
+ str(record, key) {
1186
+ const v = record[key];
1187
+ return typeof v === 'string' ? v : v == null ? '' : String(v);
1188
+ }
1189
+ display(item, key) {
1190
+ const v = item[key];
1191
+ return typeof v === 'object' ? JSON.stringify(v) : String(v ?? '');
1192
+ }
1193
+ coerceValue(raw) {
1194
+ return coerce(raw);
1195
+ }
1196
+ /** Campos livres do item: tudo menos id, eixos e campos de entries. */
1197
+ freeFields(item) {
1198
+ const reserved = new Set([
1199
+ 'id',
1200
+ this.columnField(),
1201
+ this.laneField(),
1202
+ ...this.entryDefs().map((d) => d.field),
1203
+ ]);
1204
+ return Object.keys(item).filter((k) => !reserved.has(k));
1205
+ }
1206
+ /** Blocos `items` declarados no cartão (recursivo em rows). */
1207
+ entryDefs() {
1208
+ const defs = [];
1209
+ const walk = (blocks) => {
1210
+ for (const block of blocks) {
1211
+ if (block.type === 'items') {
1212
+ defs.push({
1213
+ field: block.field,
1214
+ labelField: block.labelField ?? 'label',
1215
+ checkField: block.checkField,
1216
+ });
1217
+ }
1218
+ else if (block.type === 'row') {
1219
+ walk(block.blocks);
1220
+ }
1221
+ }
1222
+ };
1223
+ walk(this.config().card?.blocks ?? []);
1224
+ return defs;
1225
+ }
1226
+ entriesOf(item, field) {
1227
+ const v = item[field];
1228
+ return Array.isArray(v) ? v : [];
1229
+ }
1230
+ // ---- mutações de item ---------------------------------------------------------
1231
+ addItem() {
1232
+ const column = this.columnIds()[0] ?? 'todo';
1233
+ const lane = this.hasLanes() ? this.laneIds()[0] : undefined;
1234
+ this.store.update((d) => {
1235
+ d.items ??= [];
1236
+ const item = {
1237
+ id: `item-${d.items.length + 1}`,
1238
+ [this.columnField()]: column,
1239
+ title: 'Novo item',
1240
+ };
1241
+ if (lane)
1242
+ item[this.laneField()] = lane;
1243
+ d.items.push(item);
1244
+ });
1245
+ }
1246
+ duplicateItem(index) {
1247
+ this.store.update((d) => {
1248
+ const source = d.items?.[index];
1249
+ if (!source || !d.items)
1250
+ return;
1251
+ const copy = structuredClone(source);
1252
+ copy['id'] = `${source.id}-copia`;
1253
+ d.items.splice(index + 1, 0, copy);
1254
+ });
1255
+ }
1256
+ removeItem(index) {
1257
+ this.store.update((d) => d.items?.splice(index, 1));
1258
+ }
1259
+ setItemProp(index, key, value) {
1260
+ this.store.update((d) => {
1261
+ const item = d.items?.[index];
1262
+ if (!item)
1263
+ return;
1264
+ if (value === undefined || value === '')
1265
+ delete item[key];
1266
+ else
1267
+ item[key] = value;
1268
+ });
1269
+ }
1270
+ addField(index) {
1271
+ this.store.update((d) => {
1272
+ const item = d.items?.[index];
1273
+ if (!item)
1274
+ return;
1275
+ let n = 1;
1276
+ while (`campo-${n}` in item)
1277
+ n++;
1278
+ item[`campo-${n}`] = '';
1279
+ });
1280
+ }
1281
+ renameField(index, oldKey, event) {
1282
+ const newKey = event.target.value.trim();
1283
+ if (!newKey || newKey === oldKey)
1284
+ return;
1285
+ this.store.update((d) => {
1286
+ const item = d.items?.[index];
1287
+ if (!item || newKey in item)
1288
+ return;
1289
+ item[newKey] = item[oldKey];
1290
+ delete item[oldKey];
1291
+ });
1292
+ }
1293
+ removeField(index, key) {
1294
+ this.setItemProp(index, key, undefined);
1295
+ }
1296
+ // ---- entries -------------------------------------------------------------------
1297
+ addEntry(index, def) {
1298
+ this.store.update((d) => {
1299
+ const item = d.items?.[index];
1300
+ if (!item)
1301
+ return;
1302
+ const list = (item[def.field] ??= []);
1303
+ const entry = {
1304
+ id: `entry-${list.length + 1}`,
1305
+ [def.labelField]: 'Novo item interno',
1306
+ };
1307
+ if (def.checkField)
1308
+ entry[def.checkField] = false;
1309
+ list.push(entry);
1310
+ });
1311
+ }
1312
+ setEntryProp(index, field, entryIndex, key, value) {
1313
+ this.store.update((d) => {
1314
+ const item = d.items?.[index];
1315
+ const list = item?.[field];
1316
+ const entry = list?.[entryIndex];
1317
+ if (!entry)
1318
+ return;
1319
+ entry[key] = value;
1320
+ });
1321
+ }
1322
+ removeEntry(index, field, entryIndex) {
1323
+ this.store.update((d) => {
1324
+ const item = d.items?.[index];
1325
+ const list = item?.[field];
1326
+ list?.splice(entryIndex, 1);
1327
+ });
1328
+ }
1329
+ // ---- filtros ---------------------------------------------------------------------
1330
+ filtersJson() {
1331
+ const filters = this.config().filters;
1332
+ return filters?.length ? JSON.stringify(filters, null, 2) : '';
1333
+ }
1334
+ setFilters(event) {
1335
+ const raw = event.target.value.trim();
1336
+ if (!raw) {
1337
+ this.filtersError.set(null);
1338
+ this.store.update((d) => delete d.filters);
1339
+ return;
1340
+ }
1341
+ try {
1342
+ const parsed = JSON.parse(raw);
1343
+ if (!Array.isArray(parsed))
1344
+ throw new Error('esperado um ARRAY de filtros');
1345
+ this.filtersError.set(null);
1346
+ this.store.update((d) => (d.filters = parsed));
1347
+ }
1348
+ catch (e) {
1349
+ this.filtersError.set(`JSON inválido: ${e.message}`);
1350
+ }
1351
+ }
1352
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: MkbDataEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
1353
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.18", type: MkbDataEditor, isStandalone: true, selector: "mkb-data-editor", ngImport: i0, template: `
1354
+ <h3 class="mkbd-h">Itens ({{ items().length }})</h3>
1355
+ <mat-accordion displayMode="flat" [multi]="false">
1356
+ @for (item of items(); track item.id; let i = $index) {
1357
+ <mat-expansion-panel class="mat-elevation-z0 mkbd-panel">
1358
+ <mat-expansion-panel-header>
1359
+ <mat-panel-title>{{ label(item) }}</mat-panel-title>
1360
+ <mat-panel-description>{{ position(item) }}</mat-panel-description>
1361
+ </mat-expansion-panel-header>
1362
+
1363
+ <div class="mkbd-row">
1364
+ <mat-form-field appearance="outline" class="mkbd-slim">
1365
+ <mat-label>Id</mat-label>
1366
+ <input matInput [ngModel]="item.id" (ngModelChange)="setItemProp(i, 'id', $event)" />
1367
+ </mat-form-field>
1368
+ @if (columnIds().length) {
1369
+ <mat-form-field appearance="outline" class="mkbd-slim">
1370
+ <mat-label>Coluna ({{ columnField() }})</mat-label>
1371
+ <mat-select
1372
+ [ngModel]="str(item, columnField())"
1373
+ (ngModelChange)="setItemProp(i, columnField(), $event)"
1374
+ >
1375
+ @for (id of columnIds(); track id) {
1376
+ <mat-option [value]="id">{{ id }}</mat-option>
1377
+ }
1378
+ </mat-select>
1379
+ </mat-form-field>
1380
+ } @else {
1381
+ <mat-form-field appearance="outline" class="mkbd-slim">
1382
+ <mat-label>Coluna ({{ columnField() }})</mat-label>
1383
+ <input matInput [ngModel]="str(item, columnField())" (ngModelChange)="setItemProp(i, columnField(), $event)" />
1384
+ </mat-form-field>
1385
+ }
1386
+ @if (hasLanes()) {
1387
+ @if (laneIds().length) {
1388
+ <mat-form-field appearance="outline" class="mkbd-slim">
1389
+ <mat-label>Lane ({{ laneField() }})</mat-label>
1390
+ <mat-select [ngModel]="str(item, laneField())" (ngModelChange)="setItemProp(i, laneField(), $event)">
1391
+ @for (id of laneIds(); track id) {
1392
+ <mat-option [value]="id">{{ id }}</mat-option>
1393
+ }
1394
+ </mat-select>
1395
+ </mat-form-field>
1396
+ } @else {
1397
+ <mat-form-field appearance="outline" class="mkbd-slim">
1398
+ <mat-label>Lane ({{ laneField() }})</mat-label>
1399
+ <input matInput [ngModel]="str(item, laneField())" (ngModelChange)="setItemProp(i, laneField(), $event)" />
1400
+ </mat-form-field>
1401
+ }
1402
+ }
1403
+ </div>
1404
+
1405
+ <h4 class="mkbd-h4">Campos</h4>
1406
+ @for (key of freeFields(item); track key) {
1407
+ <div class="mkbd-row">
1408
+ <mat-form-field appearance="outline" class="mkbd-slim">
1409
+ <mat-label>Campo</mat-label>
1410
+ <input matInput [ngModel]="key" (change)="renameField(i, key, $event)" />
1411
+ </mat-form-field>
1412
+ <mat-form-field appearance="outline" class="mkbd-grow">
1413
+ <mat-label>Valor</mat-label>
1414
+ <input matInput [ngModel]="display(item, key)" (ngModelChange)="setItemProp(i, key, coerceValue($event))" />
1415
+ </mat-form-field>
1416
+ <button matIconButton type="button" (click)="removeField(i, key)" aria-label="Remover campo">✕</button>
1417
+ </div>
1418
+ }
1419
+ <button matButton type="button" (click)="addField(i)">+ Campo</button>
1420
+
1421
+ @for (def of entryDefs(); track def.field) {
1422
+ <h4 class="mkbd-h4">Itens internos — {{ def.field }}</h4>
1423
+ @for (entry of entriesOf(item, def.field); track entry.id; let ei = $index) {
1424
+ <div class="mkbd-row">
1425
+ @if (def.checkField; as checkField) {
1426
+ <mat-checkbox
1427
+ [ngModel]="entry[checkField] === true"
1428
+ (ngModelChange)="setEntryProp(i, def.field, ei, checkField, $event)"
1429
+ aria-label="Concluído"
1430
+ />
1431
+ }
1432
+ <mat-form-field appearance="outline" class="mkbd-grow">
1433
+ <mat-label>{{ def.labelField }}</mat-label>
1434
+ <input
1435
+ matInput
1436
+ [ngModel]="str(entry, def.labelField)"
1437
+ (ngModelChange)="setEntryProp(i, def.field, ei, def.labelField, $event)"
1438
+ />
1439
+ </mat-form-field>
1440
+ <button matIconButton type="button" (click)="removeEntry(i, def.field, ei)" aria-label="Remover item interno">✕</button>
1441
+ </div>
1442
+ }
1443
+ <button matButton type="button" (click)="addEntry(i, def)">+ Item interno</button>
1444
+ }
1445
+
1446
+ <div class="mkbd-row mkbd-mt">
1447
+ <button matButton type="button" (click)="duplicateItem(i)">Duplicar</button>
1448
+ <button matButton type="button" (click)="removeItem(i)">Remover item</button>
1449
+ </div>
1450
+ </mat-expansion-panel>
1451
+ }
1452
+ </mat-accordion>
1453
+ <button matButton class="mkbd-mt" type="button" (click)="addItem()">+ Adicionar item</button>
1454
+
1455
+ <h3 class="mkbd-h">Filtros iniciais (JSON)</h3>
1456
+ <p class="mkbd-hint">Array de KanbanFilter aplicado na criação do board; vazio remove.</p>
1457
+ <mat-form-field appearance="outline" class="mkbd-full">
1458
+ <mat-label>filters</mat-label>
1459
+ <textarea matInput rows="4" [value]="filtersJson()" (change)="setFilters($event)"></textarea>
1460
+ @if (filtersError(); as err) {
1461
+ <mat-hint class="mkbd-error">{{ err }}</mat-hint>
1462
+ }
1463
+ </mat-form-field>
1464
+ `, isInline: true, styles: [":host{display:block}.mkbd-h{margin:18px 0 8px;font-size:13px;font-weight:600}.mkbd-h:first-child{margin-top:0}.mkbd-h4{margin:12px 0 6px;font-size:12px;font-weight:600;color:var(--mkb-muted, #6f6a7c)}.mkbd-hint{margin:0 0 8px;font-size:11.5px;color:var(--mkb-muted, #6f6a7c)}.mkbd-row{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:2px}.mkbd-mt{margin-top:10px}.mkbd-slim{width:180px}.mkbd-grow{flex:1;min-width:180px}.mkbd-full{width:100%}.mkbd-panel{border:1px solid var(--mkb-border, #e3e0ea);border-radius:8px!important;margin-bottom:6px}.mkbd-error{color:var(--mkb-danger, #b3261e)}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i2.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i2.MatLabel, selector: "mat-label" }, { kind: "directive", type: i2.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i3.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i4.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i4.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i6.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i7.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i7.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatExpansionModule }, { kind: "directive", type: i7$1.MatAccordion, selector: "mat-accordion", inputs: ["hideToggle", "displayMode", "togglePosition"], exportAs: ["matAccordion"] }, { kind: "component", type: i7$1.MatExpansionPanel, selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i7$1.MatExpansionPanelHeader, selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "directive", type: i7$1.MatExpansionPanelTitle, selector: "mat-panel-title" }, { kind: "directive", type: i7$1.MatExpansionPanelDescription, selector: "mat-panel-description" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1465
+ }
1466
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: MkbDataEditor, decorators: [{
1467
+ type: Component,
1468
+ args: [{ selector: 'mkb-data-editor', changeDetection: ChangeDetectionStrategy.OnPush, imports: [
1469
+ FormsModule,
1470
+ MatFormFieldModule,
1471
+ MatInputModule,
1472
+ MatSelectModule,
1473
+ MatCheckboxModule,
1474
+ MatButtonModule,
1475
+ MatExpansionModule,
1476
+ ], template: `
1477
+ <h3 class="mkbd-h">Itens ({{ items().length }})</h3>
1478
+ <mat-accordion displayMode="flat" [multi]="false">
1479
+ @for (item of items(); track item.id; let i = $index) {
1480
+ <mat-expansion-panel class="mat-elevation-z0 mkbd-panel">
1481
+ <mat-expansion-panel-header>
1482
+ <mat-panel-title>{{ label(item) }}</mat-panel-title>
1483
+ <mat-panel-description>{{ position(item) }}</mat-panel-description>
1484
+ </mat-expansion-panel-header>
1485
+
1486
+ <div class="mkbd-row">
1487
+ <mat-form-field appearance="outline" class="mkbd-slim">
1488
+ <mat-label>Id</mat-label>
1489
+ <input matInput [ngModel]="item.id" (ngModelChange)="setItemProp(i, 'id', $event)" />
1490
+ </mat-form-field>
1491
+ @if (columnIds().length) {
1492
+ <mat-form-field appearance="outline" class="mkbd-slim">
1493
+ <mat-label>Coluna ({{ columnField() }})</mat-label>
1494
+ <mat-select
1495
+ [ngModel]="str(item, columnField())"
1496
+ (ngModelChange)="setItemProp(i, columnField(), $event)"
1497
+ >
1498
+ @for (id of columnIds(); track id) {
1499
+ <mat-option [value]="id">{{ id }}</mat-option>
1500
+ }
1501
+ </mat-select>
1502
+ </mat-form-field>
1503
+ } @else {
1504
+ <mat-form-field appearance="outline" class="mkbd-slim">
1505
+ <mat-label>Coluna ({{ columnField() }})</mat-label>
1506
+ <input matInput [ngModel]="str(item, columnField())" (ngModelChange)="setItemProp(i, columnField(), $event)" />
1507
+ </mat-form-field>
1508
+ }
1509
+ @if (hasLanes()) {
1510
+ @if (laneIds().length) {
1511
+ <mat-form-field appearance="outline" class="mkbd-slim">
1512
+ <mat-label>Lane ({{ laneField() }})</mat-label>
1513
+ <mat-select [ngModel]="str(item, laneField())" (ngModelChange)="setItemProp(i, laneField(), $event)">
1514
+ @for (id of laneIds(); track id) {
1515
+ <mat-option [value]="id">{{ id }}</mat-option>
1516
+ }
1517
+ </mat-select>
1518
+ </mat-form-field>
1519
+ } @else {
1520
+ <mat-form-field appearance="outline" class="mkbd-slim">
1521
+ <mat-label>Lane ({{ laneField() }})</mat-label>
1522
+ <input matInput [ngModel]="str(item, laneField())" (ngModelChange)="setItemProp(i, laneField(), $event)" />
1523
+ </mat-form-field>
1524
+ }
1525
+ }
1526
+ </div>
1527
+
1528
+ <h4 class="mkbd-h4">Campos</h4>
1529
+ @for (key of freeFields(item); track key) {
1530
+ <div class="mkbd-row">
1531
+ <mat-form-field appearance="outline" class="mkbd-slim">
1532
+ <mat-label>Campo</mat-label>
1533
+ <input matInput [ngModel]="key" (change)="renameField(i, key, $event)" />
1534
+ </mat-form-field>
1535
+ <mat-form-field appearance="outline" class="mkbd-grow">
1536
+ <mat-label>Valor</mat-label>
1537
+ <input matInput [ngModel]="display(item, key)" (ngModelChange)="setItemProp(i, key, coerceValue($event))" />
1538
+ </mat-form-field>
1539
+ <button matIconButton type="button" (click)="removeField(i, key)" aria-label="Remover campo">✕</button>
1540
+ </div>
1541
+ }
1542
+ <button matButton type="button" (click)="addField(i)">+ Campo</button>
1543
+
1544
+ @for (def of entryDefs(); track def.field) {
1545
+ <h4 class="mkbd-h4">Itens internos — {{ def.field }}</h4>
1546
+ @for (entry of entriesOf(item, def.field); track entry.id; let ei = $index) {
1547
+ <div class="mkbd-row">
1548
+ @if (def.checkField; as checkField) {
1549
+ <mat-checkbox
1550
+ [ngModel]="entry[checkField] === true"
1551
+ (ngModelChange)="setEntryProp(i, def.field, ei, checkField, $event)"
1552
+ aria-label="Concluído"
1553
+ />
1554
+ }
1555
+ <mat-form-field appearance="outline" class="mkbd-grow">
1556
+ <mat-label>{{ def.labelField }}</mat-label>
1557
+ <input
1558
+ matInput
1559
+ [ngModel]="str(entry, def.labelField)"
1560
+ (ngModelChange)="setEntryProp(i, def.field, ei, def.labelField, $event)"
1561
+ />
1562
+ </mat-form-field>
1563
+ <button matIconButton type="button" (click)="removeEntry(i, def.field, ei)" aria-label="Remover item interno">✕</button>
1564
+ </div>
1565
+ }
1566
+ <button matButton type="button" (click)="addEntry(i, def)">+ Item interno</button>
1567
+ }
1568
+
1569
+ <div class="mkbd-row mkbd-mt">
1570
+ <button matButton type="button" (click)="duplicateItem(i)">Duplicar</button>
1571
+ <button matButton type="button" (click)="removeItem(i)">Remover item</button>
1572
+ </div>
1573
+ </mat-expansion-panel>
1574
+ }
1575
+ </mat-accordion>
1576
+ <button matButton class="mkbd-mt" type="button" (click)="addItem()">+ Adicionar item</button>
1577
+
1578
+ <h3 class="mkbd-h">Filtros iniciais (JSON)</h3>
1579
+ <p class="mkbd-hint">Array de KanbanFilter aplicado na criação do board; vazio remove.</p>
1580
+ <mat-form-field appearance="outline" class="mkbd-full">
1581
+ <mat-label>filters</mat-label>
1582
+ <textarea matInput rows="4" [value]="filtersJson()" (change)="setFilters($event)"></textarea>
1583
+ @if (filtersError(); as err) {
1584
+ <mat-hint class="mkbd-error">{{ err }}</mat-hint>
1585
+ }
1586
+ </mat-form-field>
1587
+ `, styles: [":host{display:block}.mkbd-h{margin:18px 0 8px;font-size:13px;font-weight:600}.mkbd-h:first-child{margin-top:0}.mkbd-h4{margin:12px 0 6px;font-size:12px;font-weight:600;color:var(--mkb-muted, #6f6a7c)}.mkbd-hint{margin:0 0 8px;font-size:11.5px;color:var(--mkb-muted, #6f6a7c)}.mkbd-row{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:2px}.mkbd-mt{margin-top:10px}.mkbd-slim{width:180px}.mkbd-grow{flex:1;min-width:180px}.mkbd-full{width:100%}.mkbd-panel{border:1px solid var(--mkb-border, #e3e0ea);border-radius:8px!important;margin-bottom:6px}.mkbd-error{color:var(--mkb-danger, #b3261e)}\n"] }]
1588
+ }] });
1589
+
1590
+ /**
1591
+ * Editor do PIPELINE: eixos (colunas/lanes — estáticos ou derivados),
1592
+ * regras de transição/nomeadas e ordenação. Todos os recursos flexíveis
1593
+ * do BoardConfig fora do cartão.
1594
+ */
1595
+ class MkbPipelineEditor {
1596
+ store = inject(DesignerStore);
1597
+ config = this.store.config;
1598
+ columnIds() {
1599
+ return (this.store.columnsAxis().groups ?? []).map((g) => g.id);
1600
+ }
1601
+ // ---- eixos -----------------------------------------------------------------
1602
+ toggleLanes(enabled) {
1603
+ this.store.update((d) => {
1604
+ if (enabled) {
1605
+ d.lanes = { groups: [{ id: 'lane-1', title: 'Lane 1' }] };
1606
+ }
1607
+ else {
1608
+ delete d.lanes;
1609
+ }
1610
+ });
1611
+ }
1612
+ setAxisMode(key, mode) {
1613
+ this.store.update((d) => {
1614
+ const axis = this.draftAxis(d, key);
1615
+ if (mode === 'derived') {
1616
+ delete axis.groups;
1617
+ delete axis.reorderable;
1618
+ axis.field ||= key === 'columns' ? 'column' : 'lane';
1619
+ }
1620
+ else {
1621
+ axis.groups ??= [{ id: 'grupo-1', title: 'Grupo 1' }];
1622
+ }
1623
+ });
1624
+ }
1625
+ setAxisProp(key, prop, value) {
1626
+ this.store.update((d) => {
1627
+ const axis = this.draftAxis(d, key);
1628
+ if (value === undefined)
1629
+ delete axis[prop];
1630
+ else
1631
+ axis[prop] = value;
1632
+ });
1633
+ }
1634
+ setGroup(key, index, prop, value) {
1635
+ this.store.update((d) => {
1636
+ const group = this.draftAxis(d, key).groups?.[index];
1637
+ if (!group)
1638
+ return;
1639
+ if (value === undefined)
1640
+ delete group[prop];
1641
+ else
1642
+ group[prop] = value;
1643
+ });
1644
+ }
1645
+ groupColor(group) {
1646
+ const c = group.meta?.['color'];
1647
+ return typeof c === 'string' ? c : '#efedf3';
1648
+ }
1649
+ setGroupColor(key, index, color) {
1650
+ this.store.update((d) => {
1651
+ const group = this.draftAxis(d, key).groups?.[index];
1652
+ if (!group)
1653
+ return;
1654
+ group.meta = { ...(group.meta ?? {}), color };
1655
+ });
1656
+ }
1657
+ addGroup(key) {
1658
+ this.store.update((d) => {
1659
+ const groups = this.draftAxis(d, key).groups;
1660
+ groups?.push({ id: `grupo-${(groups.length + 1)}` });
1661
+ });
1662
+ }
1663
+ removeGroup(key, index) {
1664
+ this.store.update((d) => this.draftAxis(d, key).groups?.splice(index, 1));
1665
+ }
1666
+ moveGroup(key, index, delta) {
1667
+ this.store.update((d) => {
1668
+ const groups = this.draftAxis(d, key).groups;
1669
+ if (!groups)
1670
+ return;
1671
+ const target = index + delta;
1672
+ if (target < 0 || target >= groups.length)
1673
+ return;
1674
+ const [g] = groups.splice(index, 1);
1675
+ groups.splice(target, 0, g);
1676
+ });
1677
+ }
1678
+ draftAxis(d, key) {
1679
+ if (key === 'lanes') {
1680
+ d.lanes ??= {};
1681
+ if (Array.isArray(d.lanes))
1682
+ d.lanes = { groups: d.lanes };
1683
+ return d.lanes;
1684
+ }
1685
+ if (Array.isArray(d.columns))
1686
+ d.columns = { groups: d.columns };
1687
+ return d.columns;
1688
+ }
1689
+ // ---- regras ----------------------------------------------------------------
1690
+ transitions() {
1691
+ return this.config().rules?.transitions ?? [];
1692
+ }
1693
+ fromOf(t) {
1694
+ return typeof t.from === 'string' ? t.from : Array.isArray(t.from) ? (t.from[0] ?? '*') : '*';
1695
+ }
1696
+ toOf(t) {
1697
+ return typeof t.to === 'string' ? [t.to] : (t.to ?? []);
1698
+ }
1699
+ addTransition() {
1700
+ this.store.update((d) => {
1701
+ d.rules ??= {};
1702
+ d.rules.transitions ??= [];
1703
+ d.rules.transitions.push({});
1704
+ });
1705
+ }
1706
+ removeTransition(index) {
1707
+ this.store.update((d) => {
1708
+ d.rules?.transitions?.splice(index, 1);
1709
+ if (d.rules?.transitions?.length === 0)
1710
+ delete d.rules.transitions;
1711
+ });
1712
+ }
1713
+ setTransition(index, side, value) {
1714
+ this.store.update((d) => {
1715
+ const t = d.rules?.transitions?.[index];
1716
+ if (!t)
1717
+ return;
1718
+ if (side === 'from') {
1719
+ if (value === '*')
1720
+ delete t['from'];
1721
+ else
1722
+ t['from'] = value;
1723
+ }
1724
+ else {
1725
+ const list = value;
1726
+ if (!list.length)
1727
+ delete t['to'];
1728
+ else
1729
+ t['to'] = list.length === 1 ? list[0] : list;
1730
+ }
1731
+ });
1732
+ }
1733
+ namesOf(key) {
1734
+ return (this.config().rules?.[key] ?? []).join(', ');
1735
+ }
1736
+ setNames(key, csv) {
1737
+ const names = csv.split(/[\s,]+/).filter(Boolean);
1738
+ this.store.update((d) => {
1739
+ d.rules ??= {};
1740
+ if (names.length)
1741
+ d.rules[key] = names;
1742
+ else
1743
+ delete d.rules[key];
1744
+ });
1745
+ }
1746
+ // ---- ordenação / geral -------------------------------------------------------
1747
+ toggleSort(enabled) {
1748
+ this.store.update((d) => {
1749
+ if (enabled)
1750
+ d.sort = { field: 'title', direction: 'asc' };
1751
+ else
1752
+ delete d.sort;
1753
+ });
1754
+ }
1755
+ setSort(prop, value) {
1756
+ this.store.update((d) => {
1757
+ if (!d.sort)
1758
+ return;
1759
+ if (prop === 'field')
1760
+ d.sort.field = value;
1761
+ else
1762
+ d.sort.direction = value;
1763
+ });
1764
+ }
1765
+ setReadOnly(value) {
1766
+ this.store.update((d) => {
1767
+ if (value)
1768
+ d.readOnly = true;
1769
+ else
1770
+ delete d.readOnly;
1771
+ });
1772
+ }
1773
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: MkbPipelineEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
1774
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.18", type: MkbPipelineEditor, isStandalone: true, selector: "mkb-pipeline-editor", ngImport: i0, template: `
1775
+ <!-- ======================= COLUNAS ======================= -->
1776
+ <h3 class="mkbd-h">Colunas</h3>
1777
+ <ng-container
1778
+ *ngTemplateOutlet="axisEditor; context: { axis: store.columnsAxis(), key: 'columns' }"
1779
+ />
1780
+
1781
+ <!-- ======================= LANES ========================= -->
1782
+ <h3 class="mkbd-h">
1783
+ Lanes (swimlanes)
1784
+ <mat-slide-toggle
1785
+ [ngModel]="!!store.lanesAxis()"
1786
+ (ngModelChange)="toggleLanes($event)"
1787
+ aria-label="Usar lanes"
1788
+ />
1789
+ </h3>
1790
+ @if (store.lanesAxis(); as lanes) {
1791
+ <ng-container *ngTemplateOutlet="axisEditor; context: { axis: lanes, key: 'lanes' }" />
1792
+ }
1793
+
1794
+ <!-- ================= EDITOR DE EIXO (template) ============ -->
1795
+ <ng-template #axisEditor let-axis="axis" let-key="key">
1796
+ <div class="mkbd-row">
1797
+ <mat-form-field appearance="outline" class="mkbd-slim">
1798
+ <mat-label>Modo</mat-label>
1799
+ <mat-select
1800
+ [ngModel]="axis.groups ? 'static' : 'derived'"
1801
+ (ngModelChange)="setAxisMode(key, $event)"
1802
+ >
1803
+ <mat-option value="static">Estático (grupos declarados)</mat-option>
1804
+ <mat-option value="derived">Derivado (agrupamento dinâmico)</mat-option>
1805
+ </mat-select>
1806
+ </mat-form-field>
1807
+ <mat-form-field appearance="outline" class="mkbd-slim">
1808
+ <mat-label>Campo do item</mat-label>
1809
+ <input
1810
+ matInput
1811
+ [ngModel]="axis.field ?? ''"
1812
+ (ngModelChange)="setAxisProp(key, 'field', $event || undefined)"
1813
+ [placeholder]="key === 'columns' ? 'column' : 'lane'"
1814
+ />
1815
+ </mat-form-field>
1816
+ <mat-form-field appearance="outline" class="mkbd-slim">
1817
+ <mat-label>Ordenar grupos</mat-label>
1818
+ <mat-select
1819
+ [ngModel]="axis.sortGroups ?? ''"
1820
+ (ngModelChange)="setAxisProp(key, 'sortGroups', $event || undefined)"
1821
+ >
1822
+ <mat-option value="">Ordem declarada / de aparição</mat-option>
1823
+ <mat-option value="asc">Crescente</mat-option>
1824
+ <mat-option value="desc">Decrescente</mat-option>
1825
+ </mat-select>
1826
+ </mat-form-field>
1827
+ @if (axis.groups) {
1828
+ <mat-slide-toggle
1829
+ [ngModel]="axis.reorderable ?? false"
1830
+ (ngModelChange)="setAxisProp(key, 'reorderable', $event || undefined)"
1831
+ >
1832
+ Reordenável (arrastar)
1833
+ </mat-slide-toggle>
1834
+ }
1835
+ </div>
1836
+
1837
+ @if (axis.groups; as groups) {
1838
+ <div class="mkbd-groups">
1839
+ @for (group of groups; track $index; let i = $index) {
1840
+ <div class="mkbd-group">
1841
+ <mat-form-field appearance="outline" class="mkbd-slim">
1842
+ <mat-label>Id</mat-label>
1843
+ <input matInput [ngModel]="group.id" (ngModelChange)="setGroup(key, i, 'id', $event)" />
1844
+ </mat-form-field>
1845
+ <mat-form-field appearance="outline" class="mkbd-slim">
1846
+ <mat-label>Título</mat-label>
1847
+ <input matInput [ngModel]="group.title ?? ''" (ngModelChange)="setGroup(key, i, 'title', $event || undefined)" />
1848
+ </mat-form-field>
1849
+ <label class="mkbd-color">
1850
+ Cor
1851
+ <input
1852
+ type="color"
1853
+ [ngModel]="groupColor(group)"
1854
+ (ngModelChange)="setGroupColor(key, i, $event)"
1855
+ />
1856
+ </label>
1857
+ <mat-form-field appearance="outline" class="mkbd-tiny">
1858
+ <mat-label>WIP</mat-label>
1859
+ <input
1860
+ matInput
1861
+ type="number"
1862
+ min="0"
1863
+ [ngModel]="group.limit ?? null"
1864
+ (ngModelChange)="setGroup(key, i, 'limit', $event === null || $event === '' ? undefined : +$event)"
1865
+ />
1866
+ </mat-form-field>
1867
+ <mat-checkbox
1868
+ [ngModel]="group.locked ?? false"
1869
+ (ngModelChange)="setGroup(key, i, 'locked', $event || undefined)"
1870
+ >
1871
+ Travado
1872
+ </mat-checkbox>
1873
+ <span class="mkbd-spacer"></span>
1874
+ <button matIconButton type="button" (click)="moveGroup(key, i, -1)" [disabled]="i === 0" aria-label="Subir">↑</button>
1875
+ <button matIconButton type="button" (click)="moveGroup(key, i, 1)" [disabled]="i === groups.length - 1" aria-label="Descer">↓</button>
1876
+ <button matIconButton type="button" (click)="removeGroup(key, i)" aria-label="Remover grupo">✕</button>
1877
+ </div>
1878
+ }
1879
+ <button matButton type="button" (click)="addGroup(key)">+ Adicionar {{ key === 'columns' ? 'coluna' : 'lane' }}</button>
1880
+ </div>
1881
+ }
1882
+ </ng-template>
1883
+
1884
+ <!-- ======================= REGRAS ========================= -->
1885
+ <h3 class="mkbd-h">Regras de transição (colunas)</h3>
1886
+ <p class="mkbd-hint">
1887
+ Sem linhas = qualquer transição é permitida. Com linhas, SÓ as listadas passam.
1888
+ </p>
1889
+ @for (t of transitions(); track $index; let i = $index) {
1890
+ <div class="mkbd-row">
1891
+ <mat-form-field appearance="outline" class="mkbd-slim">
1892
+ <mat-label>De</mat-label>
1893
+ <mat-select [ngModel]="fromOf(t)" (ngModelChange)="setTransition(i, 'from', $event)">
1894
+ <mat-option value="*">Qualquer</mat-option>
1895
+ @for (id of columnIds(); track id) {
1896
+ <mat-option [value]="id">{{ id }}</mat-option>
1897
+ }
1898
+ </mat-select>
1899
+ </mat-form-field>
1900
+ <mat-form-field appearance="outline" class="mkbd-grow">
1901
+ <mat-label>Para</mat-label>
1902
+ <mat-select multiple [ngModel]="toOf(t)" (ngModelChange)="setTransition(i, 'to', $event)">
1903
+ @for (id of columnIds(); track id) {
1904
+ <mat-option [value]="id">{{ id }}</mat-option>
1905
+ }
1906
+ </mat-select>
1907
+ </mat-form-field>
1908
+ <button matIconButton type="button" (click)="removeTransition(i)" aria-label="Remover transição">✕</button>
1909
+ </div>
1910
+ }
1911
+ <button matButton type="button" (click)="addTransition()">+ Adicionar transição</button>
1912
+
1913
+ <div class="mkbd-row mkbd-mt">
1914
+ <mat-form-field appearance="outline" class="mkbd-grow">
1915
+ <mat-label>Regras nomeadas de cartão (rule:*)</mat-label>
1916
+ <input
1917
+ matInput
1918
+ [ngModel]="namesOf('rules')"
1919
+ (ngModelChange)="setNames('rules', $event)"
1920
+ placeholder="rule:berco-livre, rule:sla"
1921
+ />
1922
+ </mat-form-field>
1923
+ <mat-form-field appearance="outline" class="mkbd-grow">
1924
+ <mat-label>Regras nomeadas de itens (entryRules)</mat-label>
1925
+ <input matInput [ngModel]="namesOf('entryRules')" (ngModelChange)="setNames('entryRules', $event)" />
1926
+ </mat-form-field>
1927
+ </div>
1928
+ <p class="mkbd-hint">Os nomes referenciam código registrado pelo host no KanbanRegistry.</p>
1929
+
1930
+ <!-- ================== ORDENAÇÃO / GERAL ==================== -->
1931
+ <h3 class="mkbd-h">Ordenação e geral</h3>
1932
+ <div class="mkbd-row">
1933
+ <mat-slide-toggle [ngModel]="!!config().sort" (ngModelChange)="toggleSort($event)">
1934
+ Ordenar itens por campo
1935
+ </mat-slide-toggle>
1936
+ @if (config().sort; as sort) {
1937
+ <mat-form-field appearance="outline" class="mkbd-slim">
1938
+ <mat-label>Campo</mat-label>
1939
+ <input matInput [ngModel]="sort.field" (ngModelChange)="setSort('field', $event)" />
1940
+ </mat-form-field>
1941
+ <mat-form-field appearance="outline" class="mkbd-slim">
1942
+ <mat-label>Direção</mat-label>
1943
+ <mat-select [ngModel]="sort.direction ?? 'asc'" (ngModelChange)="setSort('direction', $event)">
1944
+ <mat-option value="asc">Crescente</mat-option>
1945
+ <mat-option value="desc">Decrescente</mat-option>
1946
+ </mat-select>
1947
+ </mat-form-field>
1948
+ }
1949
+ <mat-slide-toggle
1950
+ [ngModel]="config().readOnly ?? false"
1951
+ (ngModelChange)="setReadOnly($event)"
1952
+ >
1953
+ Somente leitura
1954
+ </mat-slide-toggle>
1955
+ </div>
1956
+ `, isInline: true, styles: [":host{display:block}.mkbd-h{margin:18px 0 8px;font-size:13px;font-weight:600;display:flex;align-items:center;gap:12px}.mkbd-h:first-child{margin-top:0}.mkbd-hint{margin:0 0 8px;font-size:11.5px;color:var(--mkb-muted, #6f6a7c)}.mkbd-row{display:flex;flex-wrap:wrap;gap:10px;align-items:center}.mkbd-mt{margin-top:10px}.mkbd-groups{display:flex;flex-direction:column;gap:4px}.mkbd-group{display:flex;flex-wrap:wrap;gap:8px;align-items:center;padding:4px 8px;border:1px dashed var(--mkb-border, #e3e0ea);border-radius:8px}.mkbd-slim{width:170px}.mkbd-tiny{width:90px}.mkbd-grow{flex:1;min-width:200px}.mkbd-spacer{flex:1}.mkbd-color{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--mkb-muted, #6f6a7c)}.mkbd-color input{width:34px;height:28px;border:none;background:none;padding:0;cursor:pointer}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i2.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i2.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i3.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i4.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i4.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i5.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i6.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i7.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i7.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatExpansionModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1957
+ }
1958
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: MkbPipelineEditor, decorators: [{
1959
+ type: Component,
1960
+ args: [{ selector: 'mkb-pipeline-editor', changeDetection: ChangeDetectionStrategy.OnPush, imports: [
1961
+ NgTemplateOutlet,
1962
+ FormsModule,
1963
+ MatFormFieldModule,
1964
+ MatInputModule,
1965
+ MatSelectModule,
1966
+ MatSlideToggleModule,
1967
+ MatCheckboxModule,
1968
+ MatButtonModule,
1969
+ MatExpansionModule,
1970
+ ], template: `
1971
+ <!-- ======================= COLUNAS ======================= -->
1972
+ <h3 class="mkbd-h">Colunas</h3>
1973
+ <ng-container
1974
+ *ngTemplateOutlet="axisEditor; context: { axis: store.columnsAxis(), key: 'columns' }"
1975
+ />
1976
+
1977
+ <!-- ======================= LANES ========================= -->
1978
+ <h3 class="mkbd-h">
1979
+ Lanes (swimlanes)
1980
+ <mat-slide-toggle
1981
+ [ngModel]="!!store.lanesAxis()"
1982
+ (ngModelChange)="toggleLanes($event)"
1983
+ aria-label="Usar lanes"
1984
+ />
1985
+ </h3>
1986
+ @if (store.lanesAxis(); as lanes) {
1987
+ <ng-container *ngTemplateOutlet="axisEditor; context: { axis: lanes, key: 'lanes' }" />
1988
+ }
1989
+
1990
+ <!-- ================= EDITOR DE EIXO (template) ============ -->
1991
+ <ng-template #axisEditor let-axis="axis" let-key="key">
1992
+ <div class="mkbd-row">
1993
+ <mat-form-field appearance="outline" class="mkbd-slim">
1994
+ <mat-label>Modo</mat-label>
1995
+ <mat-select
1996
+ [ngModel]="axis.groups ? 'static' : 'derived'"
1997
+ (ngModelChange)="setAxisMode(key, $event)"
1998
+ >
1999
+ <mat-option value="static">Estático (grupos declarados)</mat-option>
2000
+ <mat-option value="derived">Derivado (agrupamento dinâmico)</mat-option>
2001
+ </mat-select>
2002
+ </mat-form-field>
2003
+ <mat-form-field appearance="outline" class="mkbd-slim">
2004
+ <mat-label>Campo do item</mat-label>
2005
+ <input
2006
+ matInput
2007
+ [ngModel]="axis.field ?? ''"
2008
+ (ngModelChange)="setAxisProp(key, 'field', $event || undefined)"
2009
+ [placeholder]="key === 'columns' ? 'column' : 'lane'"
2010
+ />
2011
+ </mat-form-field>
2012
+ <mat-form-field appearance="outline" class="mkbd-slim">
2013
+ <mat-label>Ordenar grupos</mat-label>
2014
+ <mat-select
2015
+ [ngModel]="axis.sortGroups ?? ''"
2016
+ (ngModelChange)="setAxisProp(key, 'sortGroups', $event || undefined)"
2017
+ >
2018
+ <mat-option value="">Ordem declarada / de aparição</mat-option>
2019
+ <mat-option value="asc">Crescente</mat-option>
2020
+ <mat-option value="desc">Decrescente</mat-option>
2021
+ </mat-select>
2022
+ </mat-form-field>
2023
+ @if (axis.groups) {
2024
+ <mat-slide-toggle
2025
+ [ngModel]="axis.reorderable ?? false"
2026
+ (ngModelChange)="setAxisProp(key, 'reorderable', $event || undefined)"
2027
+ >
2028
+ Reordenável (arrastar)
2029
+ </mat-slide-toggle>
2030
+ }
2031
+ </div>
2032
+
2033
+ @if (axis.groups; as groups) {
2034
+ <div class="mkbd-groups">
2035
+ @for (group of groups; track $index; let i = $index) {
2036
+ <div class="mkbd-group">
2037
+ <mat-form-field appearance="outline" class="mkbd-slim">
2038
+ <mat-label>Id</mat-label>
2039
+ <input matInput [ngModel]="group.id" (ngModelChange)="setGroup(key, i, 'id', $event)" />
2040
+ </mat-form-field>
2041
+ <mat-form-field appearance="outline" class="mkbd-slim">
2042
+ <mat-label>Título</mat-label>
2043
+ <input matInput [ngModel]="group.title ?? ''" (ngModelChange)="setGroup(key, i, 'title', $event || undefined)" />
2044
+ </mat-form-field>
2045
+ <label class="mkbd-color">
2046
+ Cor
2047
+ <input
2048
+ type="color"
2049
+ [ngModel]="groupColor(group)"
2050
+ (ngModelChange)="setGroupColor(key, i, $event)"
2051
+ />
2052
+ </label>
2053
+ <mat-form-field appearance="outline" class="mkbd-tiny">
2054
+ <mat-label>WIP</mat-label>
2055
+ <input
2056
+ matInput
2057
+ type="number"
2058
+ min="0"
2059
+ [ngModel]="group.limit ?? null"
2060
+ (ngModelChange)="setGroup(key, i, 'limit', $event === null || $event === '' ? undefined : +$event)"
2061
+ />
2062
+ </mat-form-field>
2063
+ <mat-checkbox
2064
+ [ngModel]="group.locked ?? false"
2065
+ (ngModelChange)="setGroup(key, i, 'locked', $event || undefined)"
2066
+ >
2067
+ Travado
2068
+ </mat-checkbox>
2069
+ <span class="mkbd-spacer"></span>
2070
+ <button matIconButton type="button" (click)="moveGroup(key, i, -1)" [disabled]="i === 0" aria-label="Subir">↑</button>
2071
+ <button matIconButton type="button" (click)="moveGroup(key, i, 1)" [disabled]="i === groups.length - 1" aria-label="Descer">↓</button>
2072
+ <button matIconButton type="button" (click)="removeGroup(key, i)" aria-label="Remover grupo">✕</button>
2073
+ </div>
2074
+ }
2075
+ <button matButton type="button" (click)="addGroup(key)">+ Adicionar {{ key === 'columns' ? 'coluna' : 'lane' }}</button>
2076
+ </div>
2077
+ }
2078
+ </ng-template>
2079
+
2080
+ <!-- ======================= REGRAS ========================= -->
2081
+ <h3 class="mkbd-h">Regras de transição (colunas)</h3>
2082
+ <p class="mkbd-hint">
2083
+ Sem linhas = qualquer transição é permitida. Com linhas, SÓ as listadas passam.
2084
+ </p>
2085
+ @for (t of transitions(); track $index; let i = $index) {
2086
+ <div class="mkbd-row">
2087
+ <mat-form-field appearance="outline" class="mkbd-slim">
2088
+ <mat-label>De</mat-label>
2089
+ <mat-select [ngModel]="fromOf(t)" (ngModelChange)="setTransition(i, 'from', $event)">
2090
+ <mat-option value="*">Qualquer</mat-option>
2091
+ @for (id of columnIds(); track id) {
2092
+ <mat-option [value]="id">{{ id }}</mat-option>
2093
+ }
2094
+ </mat-select>
2095
+ </mat-form-field>
2096
+ <mat-form-field appearance="outline" class="mkbd-grow">
2097
+ <mat-label>Para</mat-label>
2098
+ <mat-select multiple [ngModel]="toOf(t)" (ngModelChange)="setTransition(i, 'to', $event)">
2099
+ @for (id of columnIds(); track id) {
2100
+ <mat-option [value]="id">{{ id }}</mat-option>
2101
+ }
2102
+ </mat-select>
2103
+ </mat-form-field>
2104
+ <button matIconButton type="button" (click)="removeTransition(i)" aria-label="Remover transição">✕</button>
2105
+ </div>
2106
+ }
2107
+ <button matButton type="button" (click)="addTransition()">+ Adicionar transição</button>
2108
+
2109
+ <div class="mkbd-row mkbd-mt">
2110
+ <mat-form-field appearance="outline" class="mkbd-grow">
2111
+ <mat-label>Regras nomeadas de cartão (rule:*)</mat-label>
2112
+ <input
2113
+ matInput
2114
+ [ngModel]="namesOf('rules')"
2115
+ (ngModelChange)="setNames('rules', $event)"
2116
+ placeholder="rule:berco-livre, rule:sla"
2117
+ />
2118
+ </mat-form-field>
2119
+ <mat-form-field appearance="outline" class="mkbd-grow">
2120
+ <mat-label>Regras nomeadas de itens (entryRules)</mat-label>
2121
+ <input matInput [ngModel]="namesOf('entryRules')" (ngModelChange)="setNames('entryRules', $event)" />
2122
+ </mat-form-field>
2123
+ </div>
2124
+ <p class="mkbd-hint">Os nomes referenciam código registrado pelo host no KanbanRegistry.</p>
2125
+
2126
+ <!-- ================== ORDENAÇÃO / GERAL ==================== -->
2127
+ <h3 class="mkbd-h">Ordenação e geral</h3>
2128
+ <div class="mkbd-row">
2129
+ <mat-slide-toggle [ngModel]="!!config().sort" (ngModelChange)="toggleSort($event)">
2130
+ Ordenar itens por campo
2131
+ </mat-slide-toggle>
2132
+ @if (config().sort; as sort) {
2133
+ <mat-form-field appearance="outline" class="mkbd-slim">
2134
+ <mat-label>Campo</mat-label>
2135
+ <input matInput [ngModel]="sort.field" (ngModelChange)="setSort('field', $event)" />
2136
+ </mat-form-field>
2137
+ <mat-form-field appearance="outline" class="mkbd-slim">
2138
+ <mat-label>Direção</mat-label>
2139
+ <mat-select [ngModel]="sort.direction ?? 'asc'" (ngModelChange)="setSort('direction', $event)">
2140
+ <mat-option value="asc">Crescente</mat-option>
2141
+ <mat-option value="desc">Decrescente</mat-option>
2142
+ </mat-select>
2143
+ </mat-form-field>
2144
+ }
2145
+ <mat-slide-toggle
2146
+ [ngModel]="config().readOnly ?? false"
2147
+ (ngModelChange)="setReadOnly($event)"
2148
+ >
2149
+ Somente leitura
2150
+ </mat-slide-toggle>
2151
+ </div>
2152
+ `, styles: [":host{display:block}.mkbd-h{margin:18px 0 8px;font-size:13px;font-weight:600;display:flex;align-items:center;gap:12px}.mkbd-h:first-child{margin-top:0}.mkbd-hint{margin:0 0 8px;font-size:11.5px;color:var(--mkb-muted, #6f6a7c)}.mkbd-row{display:flex;flex-wrap:wrap;gap:10px;align-items:center}.mkbd-mt{margin-top:10px}.mkbd-groups{display:flex;flex-direction:column;gap:4px}.mkbd-group{display:flex;flex-wrap:wrap;gap:8px;align-items:center;padding:4px 8px;border:1px dashed var(--mkb-border, #e3e0ea);border-radius:8px}.mkbd-slim{width:170px}.mkbd-tiny{width:90px}.mkbd-grow{flex:1;min-width:200px}.mkbd-spacer{flex:1}.mkbd-color{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--mkb-muted, #6f6a7c)}.mkbd-color input{width:34px;height:28px;border:none;background:none;padding:0;cursor:pointer}\n"] }]
2153
+ }] });
2154
+
2155
+ /**
2156
+ * Editor de TEMA: personaliza os tokens --mkb-* (espaçamento, cores, forma).
2157
+ * Só os tokens ALTERADOS entram na exportação (design JSON / CSS) — o resto
2158
+ * continua herdando os defaults de kanban-tokens.css e do DS do host.
2159
+ */
2160
+ class MkbThemeEditor {
2161
+ store = inject(DesignerStore);
2162
+ groups = TOKEN_GROUPS;
2163
+ valueOf(def) {
2164
+ return this.store.theme()[def.token] ?? def.default;
2165
+ }
2166
+ isChanged(def) {
2167
+ return def.token in this.store.theme();
2168
+ }
2169
+ changedCount() {
2170
+ return Object.keys(this.store.theme()).length;
2171
+ }
2172
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: MkbThemeEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
2173
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.18", type: MkbThemeEditor, isStandalone: true, selector: "mkb-theme-editor", ngImport: i0, template: `
2174
+ <p class="mkbd-hint">
2175
+ Só o que você ALTERAR entra na exportação; o resto herda o default do
2176
+ pacote e os tokens do Design System do host.
2177
+ </p>
2178
+ @for (group of groups; track group.label) {
2179
+ <h3 class="mkbd-h">{{ group.label }}</h3>
2180
+ <div class="mkbd-grid">
2181
+ @for (def of group.tokens; track def.token) {
2182
+ <div class="mkbd-token" [class.mkbd-token-changed]="isChanged(def)">
2183
+ @if (def.kind === 'color') {
2184
+ <label class="mkbd-color">
2185
+ <input
2186
+ type="color"
2187
+ [ngModel]="valueOf(def)"
2188
+ (ngModelChange)="store.setToken(def.token, $event)"
2189
+ [attr.aria-label]="def.label"
2190
+ />
2191
+ <span>
2192
+ <strong>{{ def.label }}</strong>
2193
+ <code>{{ def.token }}</code>
2194
+ </span>
2195
+ </label>
2196
+ } @else {
2197
+ <mat-form-field appearance="outline" class="mkbd-field">
2198
+ <mat-label>{{ def.label }}</mat-label>
2199
+ <input
2200
+ matInput
2201
+ [ngModel]="valueOf(def)"
2202
+ (ngModelChange)="store.setToken(def.token, $event)"
2203
+ [placeholder]="def.default"
2204
+ />
2205
+ <mat-hint><code>{{ def.token }}</code></mat-hint>
2206
+ </mat-form-field>
2207
+ }
2208
+ @if (isChanged(def)) {
2209
+ <button matIconButton type="button" (click)="store.setToken(def.token, '')" [attr.aria-label]="'Restaurar ' + def.label">↺</button>
2210
+ }
2211
+ </div>
2212
+ }
2213
+ </div>
2214
+ }
2215
+ <div class="mkbd-actions">
2216
+ <button matButton type="button" (click)="store.resetTheme()" [disabled]="!changedCount()">
2217
+ Restaurar todos os defaults
2218
+ </button>
2219
+ <span class="mkbd-hint">{{ changedCount() }} token(s) alterado(s)</span>
2220
+ </div>
2221
+ `, isInline: true, styles: [":host{display:block}.mkbd-h{margin:18px 0 8px;font-size:13px;font-weight:600}.mkbd-hint{font-size:11.5px;color:var(--mkb-muted, #6f6a7c);margin:0}.mkbd-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:8px 14px}.mkbd-token{display:flex;align-items:center;gap:6px;border-radius:8px;padding:2px 6px}.mkbd-token-changed{background:color-mix(in srgb,var(--mkb-accent, #5e35b1) 8%,transparent)}.mkbd-field{width:100%}.mkbd-color{display:flex;align-items:center;gap:10px;cursor:pointer;padding:4px 0}.mkbd-color input{width:34px;height:28px;border:none;background:none;padding:0;cursor:pointer}.mkbd-color span{display:flex;flex-direction:column;font-size:12px}.mkbd-color code,.mkbd-field code{font-size:10.5px;color:var(--mkb-muted, #6f6a7c)}.mkbd-actions{display:flex;align-items:center;gap:12px;margin-top:16px}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i2.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i2.MatLabel, selector: "mat-label" }, { kind: "directive", type: i2.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i3.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i7.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i7.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2222
+ }
2223
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: MkbThemeEditor, decorators: [{
2224
+ type: Component,
2225
+ args: [{ selector: 'mkb-theme-editor', changeDetection: ChangeDetectionStrategy.OnPush, imports: [FormsModule, MatFormFieldModule, MatInputModule, MatButtonModule], template: `
2226
+ <p class="mkbd-hint">
2227
+ Só o que você ALTERAR entra na exportação; o resto herda o default do
2228
+ pacote e os tokens do Design System do host.
2229
+ </p>
2230
+ @for (group of groups; track group.label) {
2231
+ <h3 class="mkbd-h">{{ group.label }}</h3>
2232
+ <div class="mkbd-grid">
2233
+ @for (def of group.tokens; track def.token) {
2234
+ <div class="mkbd-token" [class.mkbd-token-changed]="isChanged(def)">
2235
+ @if (def.kind === 'color') {
2236
+ <label class="mkbd-color">
2237
+ <input
2238
+ type="color"
2239
+ [ngModel]="valueOf(def)"
2240
+ (ngModelChange)="store.setToken(def.token, $event)"
2241
+ [attr.aria-label]="def.label"
2242
+ />
2243
+ <span>
2244
+ <strong>{{ def.label }}</strong>
2245
+ <code>{{ def.token }}</code>
2246
+ </span>
2247
+ </label>
2248
+ } @else {
2249
+ <mat-form-field appearance="outline" class="mkbd-field">
2250
+ <mat-label>{{ def.label }}</mat-label>
2251
+ <input
2252
+ matInput
2253
+ [ngModel]="valueOf(def)"
2254
+ (ngModelChange)="store.setToken(def.token, $event)"
2255
+ [placeholder]="def.default"
2256
+ />
2257
+ <mat-hint><code>{{ def.token }}</code></mat-hint>
2258
+ </mat-form-field>
2259
+ }
2260
+ @if (isChanged(def)) {
2261
+ <button matIconButton type="button" (click)="store.setToken(def.token, '')" [attr.aria-label]="'Restaurar ' + def.label">↺</button>
2262
+ }
2263
+ </div>
2264
+ }
2265
+ </div>
2266
+ }
2267
+ <div class="mkbd-actions">
2268
+ <button matButton type="button" (click)="store.resetTheme()" [disabled]="!changedCount()">
2269
+ Restaurar todos os defaults
2270
+ </button>
2271
+ <span class="mkbd-hint">{{ changedCount() }} token(s) alterado(s)</span>
2272
+ </div>
2273
+ `, styles: [":host{display:block}.mkbd-h{margin:18px 0 8px;font-size:13px;font-weight:600}.mkbd-hint{font-size:11.5px;color:var(--mkb-muted, #6f6a7c);margin:0}.mkbd-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:8px 14px}.mkbd-token{display:flex;align-items:center;gap:6px;border-radius:8px;padding:2px 6px}.mkbd-token-changed{background:color-mix(in srgb,var(--mkb-accent, #5e35b1) 8%,transparent)}.mkbd-field{width:100%}.mkbd-color{display:flex;align-items:center;gap:10px;cursor:pointer;padding:4px 0}.mkbd-color input{width:34px;height:28px;border:none;background:none;padding:0;cursor:pointer}.mkbd-color span{display:flex;flex-direction:column;font-size:12px}.mkbd-color code,.mkbd-field code{font-size:10.5px;color:var(--mkb-muted, #6f6a7c)}.mkbd-actions{display:flex;align-items:center;gap:12px;margin-top:16px}\n"] }]
2274
+ }] });
2275
+
2276
+ /**
2277
+ * Designer do Board — tela OPT-IN (entry point isolado
2278
+ * `@mosaicoo/kanban/designer`): edita visualmente o BoardConfig (pipeline,
2279
+ * cartão, dados, tema) com preview ao vivo e importa/exporta no formato que
2280
+ * o kanban consome em runtime. Exige @angular/material + @angular/forms
2281
+ * (peers opcionais do pacote — quem não usa o designer não paga por eles).
2282
+ */
2283
+ class MosKanbanDesigner {
2284
+ store = inject(DesignerStore);
2285
+ /** Config inicial (senão: rascunho salvo → default). */
2286
+ config = input(null, ...(ngDevMode ? [{ debugName: "config" }] : /* istanbul ignore next */ []));
2287
+ /** Tema inicial (tokens alterados). */
2288
+ theme = input(null, ...(ngDevMode ? [{ debugName: "theme" }] : /* istanbul ignore next */ []));
2289
+ /** Registry do host — deixa o preview resolver rule:, icon: e format:. */
2290
+ registry = input(undefined, ...(ngDevMode ? [{ debugName: "registry" }] : /* istanbul ignore next */ []));
2291
+ /** Chave do rascunho automático em localStorage; '' desliga. */
2292
+ storageKey = input('mkb-designer-draft', ...(ngDevMode ? [{ debugName: "storageKey" }] : /* istanbul ignore next */ []));
2293
+ /** Config a cada mudança VÁLIDA (para persistência pelo host). */
2294
+ configChange = output();
2295
+ themeChange = output();
2296
+ importErrors = signal([], ...(ngDevMode ? [{ debugName: "importErrors" }] : /* istanbul ignore next */ []));
2297
+ initialized = false;
2298
+ constructor() {
2299
+ // Inicialização: input config > rascunho salvo > default do store.
2300
+ effect(() => {
2301
+ const config = this.config();
2302
+ const key = untracked(this.storageKey);
2303
+ if (!this.initialized) {
2304
+ this.initialized = true;
2305
+ if (config) {
2306
+ this.store.load(config, untracked(this.theme) ?? undefined);
2307
+ }
2308
+ else if (key) {
2309
+ this.store.loadDraft(key);
2310
+ }
2311
+ return;
2312
+ }
2313
+ if (config)
2314
+ this.store.load(config, untracked(this.theme) ?? undefined);
2315
+ });
2316
+ // Registry é referência viva (funções) — passa direto ao preview.
2317
+ effect(() => {
2318
+ this.store.registry = this.registry();
2319
+ });
2320
+ // Cada mudança válida: emite pro host + salva rascunho.
2321
+ effect(() => {
2322
+ const config = this.store.config();
2323
+ const theme = this.store.theme();
2324
+ if (!untracked(this.store.validation).valid)
2325
+ return;
2326
+ this.configChange.emit(config);
2327
+ this.themeChange.emit(theme);
2328
+ const key = untracked(this.storageKey);
2329
+ if (key)
2330
+ this.store.saveDraft(key);
2331
+ });
2332
+ }
2333
+ hasTheme() {
2334
+ return Object.keys(this.store.theme()).length > 0;
2335
+ }
2336
+ // ---- import / export -----------------------------------------------------------
2337
+ onImportFile(event) {
2338
+ const inputEl = event.target;
2339
+ const file = inputEl.files?.[0];
2340
+ inputEl.value = '';
2341
+ if (!file)
2342
+ return;
2343
+ file.text().then((text) => {
2344
+ const result = this.store.import(text);
2345
+ this.importErrors.set(result.ok ? [] : result.errors);
2346
+ });
2347
+ }
2348
+ applyJson(area) {
2349
+ const result = this.store.import(area.value);
2350
+ this.importErrors.set(result.ok ? [] : result.errors);
2351
+ // O textarea é editado pelo usuário, e o [value] só reescreve quando a
2352
+ // string ligada muda — sem isto ele fica com o texto digitado (design file
2353
+ // ou açúcar de eixo) em vez da config canônica que passou a valer.
2354
+ if (result.ok)
2355
+ area.value = this.store.exportConfigJson();
2356
+ }
2357
+ exportConfig() {
2358
+ this.download('board-config.json', this.store.exportConfigJson(), 'application/json');
2359
+ }
2360
+ exportDesign() {
2361
+ this.download('board-design.json', this.store.exportDesignJson(), 'application/json');
2362
+ }
2363
+ exportCss() {
2364
+ const css = this.store.exportThemeCss();
2365
+ if (css)
2366
+ this.download('kanban-theme.css', css, 'text/css');
2367
+ }
2368
+ clearDraft() {
2369
+ const key = this.storageKey();
2370
+ if (key)
2371
+ this.store.clearDraft(key);
2372
+ }
2373
+ download(name, content, mime) {
2374
+ const url = URL.createObjectURL(new Blob([content], { type: mime }));
2375
+ const a = document.createElement('a');
2376
+ a.href = url;
2377
+ a.download = name;
2378
+ a.click();
2379
+ URL.revokeObjectURL(url);
2380
+ }
2381
+ ngOnDestroy() {
2382
+ this.store.destroy();
2383
+ }
2384
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: MosKanbanDesigner, deps: [], target: i0.ɵɵFactoryTarget.Component });
2385
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.18", type: MosKanbanDesigner, isStandalone: true, selector: "mos-kanban-designer", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null }, theme: { classPropertyName: "theme", publicName: "theme", isSignal: true, isRequired: false, transformFunction: null }, registry: { classPropertyName: "registry", publicName: "registry", isSignal: true, isRequired: false, transformFunction: null }, storageKey: { classPropertyName: "storageKey", publicName: "storageKey", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { configChange: "configChange", themeChange: "themeChange" }, providers: [DesignerStore], ngImport: i0, template: `
2386
+ <div class="mkbd-toolbar">
2387
+ <span class="mkbd-title">Designer do Board</span>
2388
+ @if (store.errors().length; as count) {
2389
+ <span class="mkbd-chip mkbd-chip-error" role="status">{{ count }} erro(s)</span>
2390
+ } @else {
2391
+ <span class="mkbd-chip mkbd-chip-ok" role="status">Config válida</span>
2392
+ }
2393
+ <span class="mkbd-spacer"></span>
2394
+ <button matButton type="button" (click)="fileInput.click()">Importar</button>
2395
+ <input
2396
+ #fileInput
2397
+ type="file"
2398
+ accept=".json,application/json"
2399
+ hidden
2400
+ (change)="onImportFile($event)"
2401
+ />
2402
+ <button matButton type="button" (click)="exportConfig()">Exportar config</button>
2403
+ <button matButton type="button" (click)="exportDesign()">Exportar design</button>
2404
+ <button matButton type="button" (click)="exportCss()" [disabled]="!hasTheme()">Exportar CSS</button>
2405
+ @if (storageKey()) {
2406
+ <button matButton type="button" (click)="clearDraft()">Limpar rascunho</button>
2407
+ }
2408
+ </div>
2409
+
2410
+ @if (importErrors().length) {
2411
+ <div class="mkbd-import-errors" role="alert">
2412
+ <strong>Importação rejeitada:</strong>
2413
+ <ul>
2414
+ @for (issue of importErrors(); track $index) {
2415
+ <li><code>{{ issue.path }}</code> {{ issue.message }}</li>
2416
+ }
2417
+ </ul>
2418
+ <button matButton type="button" (click)="importErrors.set([])">Fechar</button>
2419
+ </div>
2420
+ }
2421
+
2422
+ <div class="mkbd-body">
2423
+ <div class="mkbd-editors">
2424
+ <mat-tab-group stretchTabs="false" animationDuration="0ms">
2425
+ <mat-tab label="Pipeline"><mkb-pipeline-editor /></mat-tab>
2426
+ <mat-tab label="Cartão"><mkb-card-editor /></mat-tab>
2427
+ <mat-tab label="Dados"><mkb-data-editor /></mat-tab>
2428
+ <mat-tab label="Tema"><mkb-theme-editor /></mat-tab>
2429
+ <mat-tab label="JSON">
2430
+ <p class="mkbd-hint">
2431
+ BoardConfig ao vivo. Edite e aplique — inválido não substitui o design atual.
2432
+ </p>
2433
+ <textarea
2434
+ class="mkbd-json"
2435
+ rows="24"
2436
+ spellcheck="false"
2437
+ [value]="store.exportConfigJson()"
2438
+ #jsonArea
2439
+ ></textarea>
2440
+ <button matButton type="button" (click)="applyJson(jsonArea)">Aplicar JSON</button>
2441
+ </mat-tab>
2442
+ </mat-tab-group>
2443
+ </div>
2444
+
2445
+ <div class="mkbd-preview">
2446
+ <div class="mkbd-preview-head">
2447
+ <span>Preview ao vivo</span>
2448
+ @if (store.errors().length) {
2449
+ <span class="mkbd-hint">mostrando a última config válida</span>
2450
+ }
2451
+ </div>
2452
+ @if (store.errors().length) {
2453
+ <ul class="mkbd-issues">
2454
+ @for (issue of store.errors(); track $index) {
2455
+ <li><code>{{ issue.path }}</code> {{ issue.message }}</li>
2456
+ }
2457
+ </ul>
2458
+ }
2459
+ <div class="mkbd-preview-board" [style]="store.themeStyle()">
2460
+ <mos-kanban-board [board]="store.previewBoard()" />
2461
+ </div>
2462
+ </div>
2463
+ </div>
2464
+ `, isInline: true, styles: [":host{display:flex;flex-direction:column;height:100%;min-height:0;font-family:var(--mkb-font, inherit)}.mkbd-toolbar{display:flex;align-items:center;gap:8px;padding:8px 12px;border-bottom:1px solid var(--mkb-border, #e3e0ea);flex-wrap:wrap}.mkbd-title{font-size:14px;font-weight:600}.mkbd-chip{font-size:11px;padding:2px 10px;border-radius:999px}.mkbd-chip-ok{background:color-mix(in srgb,var(--mkb-success, #1b6e3c) 12%,transparent);color:var(--mkb-success, #1b6e3c)}.mkbd-chip-error{background:color-mix(in srgb,var(--mkb-danger, #b3261e) 12%,transparent);color:var(--mkb-danger, #b3261e)}.mkbd-spacer{flex:1}.mkbd-import-errors{margin:8px 12px;padding:8px 12px;border:1px solid var(--mkb-danger, #b3261e);border-radius:8px;font-size:12px}.mkbd-import-errors ul{margin:4px 0;padding-left:18px}.mkbd-body{display:flex;flex:1;min-height:0}.mkbd-editors{flex:1 1 46%;min-width:340px;overflow:auto;padding:8px 12px 24px;border-right:1px solid var(--mkb-border, #e3e0ea)}.mkbd-preview{flex:1 1 54%;min-width:0;display:flex;flex-direction:column}.mkbd-preview-head{display:flex;align-items:baseline;gap:10px;padding:8px 12px 4px;font-size:12px;font-weight:600}.mkbd-preview-board{flex:1;min-height:0;overflow:auto}.mkbd-issues{margin:0 12px 6px;padding:6px 12px 6px 26px;font-size:12px;color:var(--mkb-danger, #b3261e);border:1px dashed var(--mkb-danger, #b3261e);border-radius:8px}.mkbd-hint{font-size:11.5px;color:var(--mkb-muted, #6f6a7c)}.mkbd-json{width:100%;font-family:ui-monospace,monospace;font-size:12px;border:1px solid var(--mkb-border, #e3e0ea);border-radius:8px;padding:8px;box-sizing:border-box}@media(max-width:900px){.mkbd-body{flex-direction:column}.mkbd-editors{border-right:none;border-bottom:1px solid var(--mkb-border, #e3e0ea)}}\n"], dependencies: [{ kind: "ngmodule", type: MatTabsModule }, { kind: "component", type: i1$1.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "component", type: i1$1.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "mat-align-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i7.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: MosKanbanBoard, selector: "mos-kanban-board", inputs: ["board", "loading", "error", "showSearch", "itemActions"], outputs: ["itemClick", "itemMove", "moveRejected", "itemAction", "entryAction", "dragStarted", "dragEnded", "dragCanceled"] }, { kind: "component", type: MkbPipelineEditor, selector: "mkb-pipeline-editor" }, { kind: "component", type: MkbCardEditor, selector: "mkb-card-editor" }, { kind: "component", type: MkbDataEditor, selector: "mkb-data-editor" }, { kind: "component", type: MkbThemeEditor, selector: "mkb-theme-editor" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2465
+ }
2466
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: MosKanbanDesigner, decorators: [{
2467
+ type: Component,
2468
+ args: [{ selector: 'mos-kanban-designer', changeDetection: ChangeDetectionStrategy.OnPush, providers: [DesignerStore], imports: [
2469
+ MatTabsModule,
2470
+ MatButtonModule,
2471
+ MosKanbanBoard,
2472
+ MkbPipelineEditor,
2473
+ MkbCardEditor,
2474
+ MkbDataEditor,
2475
+ MkbThemeEditor,
2476
+ ], template: `
2477
+ <div class="mkbd-toolbar">
2478
+ <span class="mkbd-title">Designer do Board</span>
2479
+ @if (store.errors().length; as count) {
2480
+ <span class="mkbd-chip mkbd-chip-error" role="status">{{ count }} erro(s)</span>
2481
+ } @else {
2482
+ <span class="mkbd-chip mkbd-chip-ok" role="status">Config válida</span>
2483
+ }
2484
+ <span class="mkbd-spacer"></span>
2485
+ <button matButton type="button" (click)="fileInput.click()">Importar</button>
2486
+ <input
2487
+ #fileInput
2488
+ type="file"
2489
+ accept=".json,application/json"
2490
+ hidden
2491
+ (change)="onImportFile($event)"
2492
+ />
2493
+ <button matButton type="button" (click)="exportConfig()">Exportar config</button>
2494
+ <button matButton type="button" (click)="exportDesign()">Exportar design</button>
2495
+ <button matButton type="button" (click)="exportCss()" [disabled]="!hasTheme()">Exportar CSS</button>
2496
+ @if (storageKey()) {
2497
+ <button matButton type="button" (click)="clearDraft()">Limpar rascunho</button>
2498
+ }
2499
+ </div>
2500
+
2501
+ @if (importErrors().length) {
2502
+ <div class="mkbd-import-errors" role="alert">
2503
+ <strong>Importação rejeitada:</strong>
2504
+ <ul>
2505
+ @for (issue of importErrors(); track $index) {
2506
+ <li><code>{{ issue.path }}</code> {{ issue.message }}</li>
2507
+ }
2508
+ </ul>
2509
+ <button matButton type="button" (click)="importErrors.set([])">Fechar</button>
2510
+ </div>
2511
+ }
2512
+
2513
+ <div class="mkbd-body">
2514
+ <div class="mkbd-editors">
2515
+ <mat-tab-group stretchTabs="false" animationDuration="0ms">
2516
+ <mat-tab label="Pipeline"><mkb-pipeline-editor /></mat-tab>
2517
+ <mat-tab label="Cartão"><mkb-card-editor /></mat-tab>
2518
+ <mat-tab label="Dados"><mkb-data-editor /></mat-tab>
2519
+ <mat-tab label="Tema"><mkb-theme-editor /></mat-tab>
2520
+ <mat-tab label="JSON">
2521
+ <p class="mkbd-hint">
2522
+ BoardConfig ao vivo. Edite e aplique — inválido não substitui o design atual.
2523
+ </p>
2524
+ <textarea
2525
+ class="mkbd-json"
2526
+ rows="24"
2527
+ spellcheck="false"
2528
+ [value]="store.exportConfigJson()"
2529
+ #jsonArea
2530
+ ></textarea>
2531
+ <button matButton type="button" (click)="applyJson(jsonArea)">Aplicar JSON</button>
2532
+ </mat-tab>
2533
+ </mat-tab-group>
2534
+ </div>
2535
+
2536
+ <div class="mkbd-preview">
2537
+ <div class="mkbd-preview-head">
2538
+ <span>Preview ao vivo</span>
2539
+ @if (store.errors().length) {
2540
+ <span class="mkbd-hint">mostrando a última config válida</span>
2541
+ }
2542
+ </div>
2543
+ @if (store.errors().length) {
2544
+ <ul class="mkbd-issues">
2545
+ @for (issue of store.errors(); track $index) {
2546
+ <li><code>{{ issue.path }}</code> {{ issue.message }}</li>
2547
+ }
2548
+ </ul>
2549
+ }
2550
+ <div class="mkbd-preview-board" [style]="store.themeStyle()">
2551
+ <mos-kanban-board [board]="store.previewBoard()" />
2552
+ </div>
2553
+ </div>
2554
+ </div>
2555
+ `, styles: [":host{display:flex;flex-direction:column;height:100%;min-height:0;font-family:var(--mkb-font, inherit)}.mkbd-toolbar{display:flex;align-items:center;gap:8px;padding:8px 12px;border-bottom:1px solid var(--mkb-border, #e3e0ea);flex-wrap:wrap}.mkbd-title{font-size:14px;font-weight:600}.mkbd-chip{font-size:11px;padding:2px 10px;border-radius:999px}.mkbd-chip-ok{background:color-mix(in srgb,var(--mkb-success, #1b6e3c) 12%,transparent);color:var(--mkb-success, #1b6e3c)}.mkbd-chip-error{background:color-mix(in srgb,var(--mkb-danger, #b3261e) 12%,transparent);color:var(--mkb-danger, #b3261e)}.mkbd-spacer{flex:1}.mkbd-import-errors{margin:8px 12px;padding:8px 12px;border:1px solid var(--mkb-danger, #b3261e);border-radius:8px;font-size:12px}.mkbd-import-errors ul{margin:4px 0;padding-left:18px}.mkbd-body{display:flex;flex:1;min-height:0}.mkbd-editors{flex:1 1 46%;min-width:340px;overflow:auto;padding:8px 12px 24px;border-right:1px solid var(--mkb-border, #e3e0ea)}.mkbd-preview{flex:1 1 54%;min-width:0;display:flex;flex-direction:column}.mkbd-preview-head{display:flex;align-items:baseline;gap:10px;padding:8px 12px 4px;font-size:12px;font-weight:600}.mkbd-preview-board{flex:1;min-height:0;overflow:auto}.mkbd-issues{margin:0 12px 6px;padding:6px 12px 6px 26px;font-size:12px;color:var(--mkb-danger, #b3261e);border:1px dashed var(--mkb-danger, #b3261e);border-radius:8px}.mkbd-hint{font-size:11.5px;color:var(--mkb-muted, #6f6a7c)}.mkbd-json{width:100%;font-family:ui-monospace,monospace;font-size:12px;border:1px solid var(--mkb-border, #e3e0ea);border-radius:8px;padding:8px;box-sizing:border-box}@media(max-width:900px){.mkbd-body{flex-direction:column}.mkbd-editors{border-right:none;border-bottom:1px solid var(--mkb-border, #e3e0ea)}}\n"] }]
2556
+ }], ctorParameters: () => [], propDecorators: { config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: false }] }], theme: [{ type: i0.Input, args: [{ isSignal: true, alias: "theme", required: false }] }], registry: [{ type: i0.Input, args: [{ isSignal: true, alias: "registry", required: false }] }], storageKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "storageKey", required: false }] }], configChange: [{ type: i0.Output, args: ["configChange"] }], themeChange: [{ type: i0.Output, args: ["themeChange"] }] } });
2557
+
2558
+ /**
2559
+ * @mosaicoo/kanban/designer — tela OPT-IN de design do board.
2560
+ *
2561
+ * Entry point ISOLADO: só quem importa daqui paga por @angular/material e
2562
+ * @angular/forms (peers opcionais). O runtime (ui/adapters/core) não depende
2563
+ * de nada deste entry point.
2564
+ */
2565
+
2566
+ /**
2567
+ * Generated bundle index. Do not edit.
2568
+ */
2569
+
2570
+ export { BLOCK_FIELDS, BLOCK_TYPE_LABELS, DesignerStore, MosKanbanDesigner, TOKEN_GROUPS };