@sapphire-caitlyn/schema-kit-ng 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +2 -0
  2. package/docs/inputs/date/input-date/README.md +37 -0
  3. package/docs/inputs/date/input-date-span/README.md +36 -0
  4. package/docs/inputs/date/input-date-time/README.md +32 -0
  5. package/docs/inputs/date/input-date-time-span/README.md +32 -0
  6. package/docs/inputs/date/input-time/README.md +28 -0
  7. package/docs/inputs/date/input-time-span/README.md +29 -0
  8. package/docs/inputs/input-base/README.md +42 -0
  9. package/docs/inputs/input-bool/README.md +30 -0
  10. package/docs/inputs/input-color/README.md +24 -0
  11. package/docs/inputs/input-color/color-picker/README.md +25 -0
  12. package/docs/inputs/input-cpf-cnpj/README.md +34 -0
  13. package/docs/inputs/input-file/README.md +33 -0
  14. package/docs/inputs/input-icon/README.md +28 -0
  15. package/docs/inputs/input-json/README.md +36 -0
  16. package/docs/inputs/input-mask/README.md +39 -0
  17. package/docs/inputs/input-number/README.md +44 -0
  18. package/docs/inputs/input-phone/README.md +25 -0
  19. package/docs/inputs/input-select/README.md +42 -0
  20. package/docs/inputs/input-string/README.md +32 -0
  21. package/docs/inputs/input-text-area/README.md +27 -0
  22. package/docs/schema-table/README.md +444 -0
  23. package/docs/schema-view/README.md +439 -0
  24. package/docs/ui/blurry-text-reveal/README.md +49 -0
  25. package/docs/ui/breadcrumbs/README.md +62 -0
  26. package/docs/ui/file-preview/README.md +60 -0
  27. package/docs/ui/infinte-scroll/README.md +10 -0
  28. package/docs/ui/loading/README.md +62 -0
  29. package/docs/ui/save-button/README.md +68 -0
  30. package/docs/ui/schema-dialog/README.md +154 -0
  31. package/docs/ui/suspended-slider/README.md +95 -0
  32. package/docs/ui/tree-drag-drop/README.md +202 -0
  33. package/fesm2022/sapphire-caitlyn-schema-kit-ng-helm.mjs.map +1 -1
  34. package/fesm2022/sapphire-caitlyn-schema-kit-ng.mjs +106 -103
  35. package/fesm2022/sapphire-caitlyn-schema-kit-ng.mjs.map +1 -1
  36. package/package.json +1 -1
  37. package/types/sapphire-caitlyn-schema-kit-ng.d.ts +93 -17
@@ -0,0 +1,439 @@
1
+ # schema-view
2
+
3
+ Tela de criar/editar gerada a partir de um schema: um input por campo, carregamento do registro pela rota, validação e
4
+ salvamento. Na criação envia todos os valores (`POST`) e depois abre a edição do registro criado; na edição envia só os
5
+ campos alterados (`PATCH`).
6
+
7
+ Feita para trabalhar junto com o [schema-table](../schema-table/README.md) (a listagem).
8
+
9
+ ## Uso
10
+
11
+ ```ts
12
+ import { SchemaViewComponent, SchemaViewContentDirective } from '@schema-kit';
13
+ ```
14
+
15
+ ### 1. O service
16
+
17
+ O jeito mais comum é passar um `SchemaService`: ele dá os campos, carrega o registro e salva. Os exemplos deste README
18
+ usam este service:
19
+
20
+ ```ts
21
+ import { inject, Injectable } from '@angular/core';
22
+ import { min, required } from '@angular/forms/signals';
23
+ import { FieldType, IView, SchemaService } from '@schema-kit';
24
+
25
+ export interface IProduto extends IView {
26
+ nome: string;
27
+ preco: number;
28
+ ativo: boolean;
29
+ uuidCategoria: string | null;
30
+ foto: string | File | null;
31
+ descricao: string | null;
32
+ }
33
+
34
+ @Injectable({ providedIn: 'root' })
35
+ export class ProdutoService extends SchemaService<IProduto> {
36
+ constructor() {
37
+ // `this` não existe antes do super(): injete as dependências em variáveis locais
38
+ const categoriaService = inject(CategoriaService);
39
+
40
+ super({
41
+ endpoint: '/produtos',
42
+ title: 'Produtos',
43
+ bindLabel: 'nome',
44
+ bindValue: 'uuid',
45
+ schema: {
46
+ uuid: { type: FieldType.UUID, label: 'UUID', hidden: true },
47
+ nome: { type: FieldType.String, label: 'Nome', validators: required },
48
+ preco: {
49
+ type: FieldType.Currency,
50
+ label: 'Preço',
51
+ validators: [required, (path) => min(path, 0, { message: 'O preço não pode ser negativo' })],
52
+ },
53
+ ativo: { type: FieldType.Bool, label: 'Ativo', defaultValue: true },
54
+ uuidCategoria: {
55
+ type: FieldType.Select,
56
+ label: 'Categoria',
57
+ tooltipInfo: 'Define em qual seção da loja o produto aparece',
58
+ options: { service: () => categoriaService, clearable: true },
59
+ },
60
+ foto: { type: FieldType.File, label: 'Foto', options: { clearable: true } },
61
+ descricao: { type: FieldType.TextArea, label: 'Descrição' },
62
+ },
63
+ });
64
+ }
65
+ }
66
+ ```
67
+
68
+ ### 2. As rotas
69
+
70
+ O componente lê o uuid do parâmetro da rota. Sem uuid, é criação; com uuid, é edição. Use a mesma estrutura que o
71
+ schema-table navega (`./view` e `./view/:uuid`):
72
+
73
+ ```ts
74
+ const routes: Routes = [
75
+ { path: '', component: ProdutoListComponent },
76
+ { path: 'view', component: ProdutoViewComponent }, // criar
77
+ { path: 'view/:uuid', component: ProdutoViewComponent }, // editar
78
+ ];
79
+ ```
80
+
81
+ Se o parâmetro tiver outro nome (ex.: `view/:id`), informe em `uuidIdentifier="id"`.
82
+
83
+ #### Sem rota: input `uuid`
84
+
85
+ Fora dessa estrutura (ex.: um dialog), informe o registro pelo input `uuid`. Ele tem prioridade sobre a rota, e cada
86
+ troca de valor carrega o novo registro. Sem valor (`null`), volta para a criação com os valores iniciais.
87
+
88
+ ```html
89
+ <schema-view
90
+ [schemaService]="produtoService"
91
+ [uuid]="uuidProduto()"
92
+ [redirectOnCreate]="false"
93
+ [noBack]="true"
94
+ (onDidSave)="fechar()"
95
+ />
96
+ ```
97
+
98
+ ### 3. O componente
99
+
100
+ ```ts
101
+ @Component({
102
+ selector: 'produto-view',
103
+ imports: [SchemaViewComponent],
104
+ template: `<schema-view [schemaService]="produtoService" />`,
105
+ })
106
+ export class ProdutoViewComponent {
107
+ protected produtoService = inject(ProdutoService);
108
+ }
109
+ ```
110
+
111
+ Só com isso a tela tem: breadcrumb (`Produtos > Novo` ou `Produtos > Edição`), botões **Voltar** e **Novo**, um input
112
+ por campo e o botão **Salvar**.
113
+
114
+ ## Inputs
115
+
116
+ | Input | Tipo | Padrão | Descrição |
117
+ | ---------------- | ------------------------------------------ | -------- | ---------------------------------------------------------------------- |
118
+ | `schemaService` | `SchemaService<T, IP> \| null` | `null` | Campos, breadcrumb, carregamento (`show`) e salvamento (`create`/`patch`) |
119
+ | `schema` | `SchemaFieldOptionsRecord<T> \| null` | `null` | Campos da tela (inputs e form), no lugar do schema do service. Ver [Personalizando os campos](#personalizando-os-campos) |
120
+ | `saveFn` | `(values: Partial<T>) => Promise<R>` | `null` | Substitui o `create`/`patch` do service. Ver [Salvando sem service](#salvando-sem-service) |
121
+ | `redirectOnCreate` | `string \| false` | `'../view'` | Para onde ir depois de **criar**: `[redirectOnCreate, uuid]`, relativo à rota atual. `false`: fica na tela e vira edição do registro criado |
122
+ | `uuid` | `string \| null` | — | Registro a editar, no lugar do parâmetro da rota. Ver [Sem rota](#sem-rota-input-uuid) |
123
+ | `uuidIdentifier` | `string` | `'uuid'` | Nome do parâmetro da rota com o uuid |
124
+ | `lstBreadCrumbs` | `IBreadcrumbItem[]` | `[]` | Substitui o breadcrumb |
125
+ | `noBack` | `boolean` | `false` | Esconde Voltar |
126
+ | `noNew` | `boolean` | `false` | Esconde Novo |
127
+ | `noSave` | `boolean` | `false` | Esconde Salvar (tela só de leitura) |
128
+
129
+ É preciso informar `schemaService` ou `schema`.
130
+
131
+ ## Outputs
132
+
133
+ | Output | Tipo | Descrição |
134
+ | ----------- | ------- | ------------------------------------------------------------------------------------------------------ |
135
+ | `onDidSave` | `R` | Depois de salvar (criação ou edição), com o que o salvamento devolveu. Vem vazio se a resposta for vazia (ex.: `204`). Não emite sem alterações, em erro nem com form inválido |
136
+ | `novo` | `Event` | Clique em Novo, antes de navegar. `event.preventDefault()` cancela |
137
+
138
+ `R` é o tipo que o salvamento devolve: o retorno do `saveFn`, ou `T` quando quem salva é o service.
139
+
140
+ Voltar para a listagem depois de salvar (desligue o redirect para a navegação ficar só com você):
141
+
142
+ ```html
143
+ <schema-view [schemaService]="produtoService" [redirectOnCreate]="false" (onDidSave)="aoSalvar($event)" />
144
+ ```
145
+
146
+ ```ts
147
+ private router = inject(Router);
148
+
149
+ protected aoSalvar(produto: IProduto) {
150
+ console.log('Salvo:', produto.uuid);
151
+ this.router.navigate(['/produtos']);
152
+ }
153
+ ```
154
+
155
+ ## Membros públicos
156
+
157
+ Acessíveis por uma referência de template (`#view`) ou `viewChild(SchemaViewComponent)`:
158
+
159
+ | Membro | Tipo | Descrição |
160
+ | ----------- | --------------------- | ----------------------------------------------- |
161
+ | `form()` | `FieldTree<T>` | O form (signal forms). Recriado quando o `schema` ou o `schemaService` muda |
162
+ | `uuid` | `WritableSignal<string>` | uuid em edição (input `uuid` ou rota). Vazio na criação. `uuid.set()` troca o registro sem navegar |
163
+ | `loading()` | `boolean` | Carregando ou salvando (inputs em skeleton) |
164
+ | `saving()` | `boolean` | Salvando |
165
+ | `save()` | `Promise<void>` | O mesmo que clicar em Salvar |
166
+ | `load(uuid?)` | `Promise<void>` | Recarrega o registro (padrão: o `uuid()` atual). Precisa de `schemaService` |
167
+
168
+ ## Template: `schemaViewContent`
169
+
170
+ Conteúdo extra dentro do card. Não tem contexto: para ler o form, use a referência do componente.
171
+
172
+ | Input | Tipo | Padrão | Descrição |
173
+ | ---------- | ------------------ | ------- | ------------------------------------------------------------------------- |
174
+ | `position` | `'start' \| 'end'` | `'end'` | `start`: entre os botões do topo e os campos. `end`: depois dos campos, antes de Salvar |
175
+
176
+ ```html
177
+ <schema-view #view [schemaService]="produtoService">
178
+ <ng-template schemaViewContent position="start">
179
+ @if (!view.form().ativo().value()) {
180
+ <div class="rounded-lg bg-muted p-2 text-sm">
181
+ Produto inativo: não aparece na loja.
182
+ </div>
183
+ }
184
+ </ng-template>
185
+ </schema-view>
186
+ ```
187
+
188
+ ## Campos
189
+
190
+ Cada campo do schema vira um input, na ordem em que foi declarado:
191
+
192
+ | Tipo | Input |
193
+ | ---------------------------------------- | --------------------------------------------------------------------------------------- |
194
+ | `String`, `UUID` | [input-string](../inputs/input-string/README.md). `options.type` define o `type` do `<input>` (ex.: `'email'`) |
195
+ | `TextArea` | [input-text-area](../inputs/input-text-area/README.md) |
196
+ | `Number` / `Decimal` / `Currency` | [input-number / input-decimal / input-currency](../inputs/input-number/README.md) |
197
+ | `Bool` | [input-bool](../inputs/input-bool/README.md) |
198
+ | `Date` / `DateTime` / `Time` | [input-date](../inputs/date/input-date/README.md) / [input-date-time](../inputs/date/input-date-time/README.md) / [input-time](../inputs/date/input-time/README.md) |
199
+ | `DateSpan` / `DateTimeSpan` / `TimeSpan` | [input-date-span](../inputs/date/input-date-span/README.md) / [input-date-time-span](../inputs/date/input-date-time-span/README.md) / [input-time-span](../inputs/date/input-time-span/README.md) |
200
+ | `Phone` | [input-phone](../inputs/input-phone/README.md) |
201
+ | `Cpf` / `Cnpj` / `CpfCnpj` | [input-cpf / input-cnpj / input-cpf-cnpj](../inputs/input-cpf-cnpj/README.md) |
202
+ | `Mask` | [input-mask](../inputs/input-mask/README.md), com `options.maskitoOptions` e `options.unmaskHandler` |
203
+ | `Select` | [input-select](../inputs/input-select/README.md), com as `options` abaixo |
204
+ | `Icon` | [input-icon](../inputs/input-icon/README.md) |
205
+ | `File` | [input-file](../inputs/input-file/README.md), com `options.clearable` |
206
+ | `JSON` | [input-json](../inputs/input-json/README.md) |
207
+
208
+ Do schema, cada input recebe `label`, `placeholder`, `tooltipInfo` e `class`.
209
+
210
+ ### Select
211
+
212
+ Duas formas de `options`:
213
+
214
+ ```ts
215
+ // Busca na API de outro service (paginada, com busca no servidor)
216
+ uuidCategoria: {
217
+ type: FieldType.Select,
218
+ label: 'Categoria',
219
+ options: {
220
+ service: () => categoriaService,
221
+ lstFilter: [{ index: 'ativa', value: true }], // filtros enviados no index do outro service
222
+ clearable: true,
223
+ },
224
+ },
225
+
226
+ // Lista fixa: bindValue precisa produzir o tipo do campo
227
+ unidade: {
228
+ type: FieldType.Select,
229
+ label: 'Unidade',
230
+ options: {
231
+ lstOptions: [
232
+ { id: 'UN', nome: 'Unidade' },
233
+ { id: 'KG', nome: 'Quilo' },
234
+ ],
235
+ bindValue: 'id',
236
+ bindLabel: 'nome',
237
+ },
238
+ },
239
+ ```
240
+
241
+ ### Layout
242
+
243
+ Os campos ficam num grid de 12 colunas. O `class` do campo define a largura:
244
+
245
+ | Tipo | `class` padrão | Resultado |
246
+ | ----------- | ----------------------------------------- | ----------------------------------------- |
247
+ | `TextArea` | `col-span-full` | Linha inteira |
248
+ | os demais | `xl:col-span-3 md:col-span-6 col-span-full` | 4 por linha no `xl`, 2 no `md`, 1 no celular |
249
+
250
+ ```ts
251
+ nome: { type: FieldType.String, label: 'Nome', class: 'md:col-span-8 col-span-full' },
252
+ preco: { type: FieldType.Currency, label: 'Preço', class: 'md:col-span-4 col-span-full' },
253
+ ```
254
+
255
+ `hidden: true` ou `hidden: { hiddenForm: true }` esconde o input, mas o campo continua no form (o valor é enviado na
256
+ criação).
257
+
258
+ ### Valores iniciais
259
+
260
+ Na criação, cada campo começa com o `defaultValue` do schema ou, sem ele, com o padrão do tipo:
261
+
262
+ | Tipo | Padrão |
263
+ | --------------------------------- | ------- |
264
+ | `Number`, `Decimal`, `Currency` | `0` |
265
+ | `Bool` | `false` |
266
+ | os demais | `null` |
267
+
268
+ ### Validação e somente leitura
269
+
270
+ `validators` recebe uma regra ou uma lista de regras do signal forms (`(path) => void`):
271
+
272
+ ```ts
273
+ import { email, maxLength, required } from '@angular/forms/signals';
274
+ import { cpfValidator, urlValidator } from '@schema-kit';
275
+
276
+ email: { type: FieldType.String, label: 'E-mail', options: { type: 'email' }, validators: [required, email] },
277
+ sku: { type: FieldType.String, label: 'SKU', validators: (path) => maxLength(path, 20) },
278
+ cpf: { type: FieldType.Cpf, label: 'CPF', validators: cpfValidator },
279
+ site: { type: FieldType.String, label: 'Site', validators: urlValidator },
280
+ ```
281
+
282
+ Validators prontos do `@schema-kit`: `cpfValidator`, `cnpjValidator`, `cpfCnpjValidator`, `urlValidator`,
283
+ `partialUrlValidator` e `passwordValidator`.
284
+
285
+ Ao salvar com o form inválido, todos os campos são marcados como tocados (as mensagens aparecem embaixo de cada input) e
286
+ um toast lista os labels dos campos com erro.
287
+
288
+ Para um campo só de leitura, use `readonly: true` (aplica a regra `readonly` do signal forms):
289
+
290
+ ```ts
291
+ criadoPor: { type: FieldType.String, label: 'Criado por', readonly: true },
292
+ ```
293
+
294
+ O valor continua no form: vai no `POST` da criação e, como não pode ser alterado, nunca entra no `PATCH`. Para uma tela
295
+ inteira só de leitura, combine `readonly` nos campos com `[noSave]="true"`.
296
+
297
+ ## Salvando
298
+
299
+ | Modo | Ao abrir | Ao salvar | Depois |
300
+ | ---------------------- | ------------------------------ | ----------------------------------------------------------- | ------------------------------ |
301
+ | Criação (sem uuid) | Form com os valores iniciais | `POST ${endpoint}` com **todos** os valores | Vai para `../view/<uuid>` |
302
+ | Edição (com uuid) | `GET ${endpoint}/${uuid}` | `PATCH ${endpoint}/${uuid}` só com os campos **alterados** | Fica na tela |
303
+
304
+ ```jsonc
305
+ // Na edição de /produtos/view/3f2a…, mudando só o preço:
306
+ // PATCH /produtos/3f2a…
307
+ { "preco": 19.9 }
308
+ ```
309
+
310
+ - Sem alterações, nada é enviado e aparece o toast "Nenhum dado foi alterado".
311
+ - Se algum valor for arquivo (`File`, `File[]`, `FileList`), o corpo vai como `FormData`: o arquivo na chave do campo,
312
+ listas em `campo[]` e os demais valores serializados em JSON.
313
+ - Sucesso e erro mostram um toast. Os inputs ficam em skeleton enquanto salva.
314
+ - Depois de criar, a tela navega para `[redirectOnCreate, uuid]` (padrão `../view/<uuid>`), substituindo a URL: ela passa
315
+ para edição, recarrega o registro, e o próximo Salvar envia um `PATCH`. Voltar no navegador não retorna para o form de
316
+ criação.
317
+ - Com `[redirectOnCreate]="false"`, a tela não navega: o uuid do registro criado vira o `uuid()`, o form é recarregado
318
+ (sem `schemaService`, fica com os valores digitados) e o próximo Salvar envia um `PATCH`.
319
+ - Nos dois casos, é preciso que a resposta da criação tenha `uuid`. Sem ele (ex.: um `saveFn` que devolve outro tipo,
320
+ ou resposta vazia), não há registro para editar: o form volta para os valores iniciais, pronto para outra criação.
321
+
322
+ ### Salvando sem service
323
+
324
+ Com `saveFn`, o componente chama a função no lugar do `create`/`patch`. Ela recebe os mesmos valores (todos na criação,
325
+ só os alterados na edição) e devolve uma `Promise` com o resultado:
326
+
327
+ - o valor resolvido é o `$event` do `onDidSave` (e define o tipo `R`; pode ser diferente do tipo do form);
328
+ - se o valor tiver `uuid`, a criação faz o redirect de `redirectOnCreate` (ou, com `false`, vira edição na mesma tela);
329
+ - se vier vazio (ex.: resposta `204`), o toast de sucesso aparece e o `onDidSave` emite vazio;
330
+ - se rejeitar, aparece o toast de erro.
331
+
332
+ Junto com `schema`, dá para montar um formulário sem service:
333
+
334
+ ```ts
335
+ import { email, required } from '@angular/forms/signals';
336
+ import { FieldType, IView, SchemaFieldOptionsRecord, SchemaViewComponent } from '@schema-kit';
337
+
338
+ // O componente exige `T extends IView`
339
+ interface IContato extends IView {
340
+ nome: string;
341
+ email: string;
342
+ mensagem: string;
343
+ }
344
+
345
+ @Component({
346
+ selector: 'contato',
347
+ imports: [SchemaViewComponent],
348
+ template: `
349
+ <schema-view
350
+ [schema]="schema"
351
+ [saveFn]="enviar"
352
+ [redirectOnCreate]="false"
353
+ [lstBreadCrumbs]="[{ label: 'Contato' }]"
354
+ [noBack]="true"
355
+ [noNew]="true"
356
+ (onDidSave)="enviado($event)"
357
+ />
358
+ `,
359
+ })
360
+ export class ContatoComponent {
361
+ private http = inject(HttpClient);
362
+ private router = inject(Router);
363
+
364
+ protected schema: SchemaFieldOptionsRecord<IContato> = {
365
+ nome: { type: FieldType.String, label: 'Nome', validators: required },
366
+ email: { type: FieldType.String, label: 'E-mail', options: { type: 'email' }, validators: [required, email] },
367
+ mensagem: { type: FieldType.TextArea, label: 'Mensagem', validators: required },
368
+ };
369
+
370
+ // Arrow function: mantém o `this` quando o schema-view chama a função.
371
+ // A API devolve um protocolo, não um IContato: `R` vira IProtocolo
372
+ protected enviar = (values: Partial<IContato>) =>
373
+ lastValueFrom(this.http.post<IProtocolo>('/api/contato', values));
374
+
375
+ protected enviado(protocolo: IProtocolo) {
376
+ this.router.navigate(['/contato/enviado', protocolo.numero]);
377
+ }
378
+ }
379
+
380
+ interface IProtocolo {
381
+ numero: string;
382
+ }
383
+ ```
384
+
385
+ Sem service não há como carregar o registro: com um uuid (rota ou input), o form não é preenchido. Na edição, o
386
+ `saveFn` recebe só os campos alterados; o uuid do registro fica em `uuid()` (via referência `#view`). Fora da estrutura
387
+ `view`/`view/:uuid` (rota própria, dialog), passe `[redirectOnCreate]="false"`.
388
+
389
+ ## Personalizando os campos
390
+
391
+ Com `schemaService` **e** `schema`, o `schema` substitui o do service na tela: define os inputs e o form (valores
392
+ iniciais, `validators` e `readonly`). O service continua carregando e salvando. Útil para ajustar uma tela sem mexer no
393
+ service.
394
+
395
+ O `schema` **substitui**, não completa: um campo do service que não estiver nele não aparece nem vai no form. Para
396
+ manter os outros campos, parta do schema do service:
397
+
398
+ ```ts
399
+ export class ProdutoViewComponent {
400
+ protected produtoService = inject(ProdutoService);
401
+ uuidCategoria = input<string>(); // parâmetro da rota, com withComponentInputBinding()
402
+
403
+ // Parte do schema do service: esconde a foto e já cria o produto na categoria da rota
404
+ protected schema = computed<SchemaFieldOptionsRecord<IProduto>>(() => ({
405
+ ...this.produtoService.schema,
406
+ foto: { ...this.produtoService.schema.foto!, hidden: { hiddenForm: true } },
407
+ uuidCategoria: { ...this.produtoService.schema.uuidCategoria!, defaultValue: this.uuidCategoria() ?? null },
408
+ }));
409
+ }
410
+ ```
411
+
412
+ ```html
413
+ <schema-view [schemaService]="produtoService" [schema]="schema()" />
414
+ ```
415
+
416
+ O `schema` é reativo (pode ser um `computed`). Mudar o valor recria o form:
417
+
418
+ - o que o usuário já alterou continua, nos campos que ainda existem;
419
+ - os outros campos voltam ao valor inicial do schema novo (ex.: um `defaultValue` que mudou);
420
+ - na edição, o registro é recarregado, e o que foi alterado se perde.
421
+
422
+ ## Navegação e breadcrumb
423
+
424
+ | Botão | Aparece | Vai para |
425
+ | ------ | --------------------------------- | ----------------------------------------------------------- |
426
+ | Voltar | Sempre (exceto com `noBack`) | A listagem: `../` em `view`, `../../` em `view/:uuid`. Depende só da rota, não do input `uuid` |
427
+ | Novo | Só na edição (exceto com `noNew`) | `../view` (criação). Se o uuid não veio da rota, volta para a criação na mesma tela. Cancelável com `(novo)` |
428
+ | Salvar | Sempre (exceto com `noSave`) | Na criação, `[redirectOnCreate, uuid]` (padrão `../view/<uuid>`); com `false`, fica e vira edição. Na edição, não navega |
429
+
430
+ O breadcrumb vem, nesta ordem, de: `lstBreadCrumbs`; `breadcrumbs` das opções do service; ou o padrão
431
+ `[{ label: title, routerLink: endpoint }, { label: 'Novo' | 'Edição' }]`.
432
+
433
+ ```ts
434
+ interface IBreadcrumbItem {
435
+ label: string;
436
+ routerLink?: string;
437
+ icon?: string;
438
+ }
439
+ ```
@@ -0,0 +1,49 @@
1
+ # blurry-text-reveal
2
+
3
+ Exibe um texto com animação de entrada letra a letra: cada caractere surge desfocado, transparente e um pouco abaixo da
4
+ posição final, e assenta no lugar 30 ms depois do anterior.
5
+
6
+ Exemplo real de uso: [login](../../../../../../../src/app/pages/auth/login/login.component.html) (título "Backoffice").
7
+
8
+ ## Uso
9
+
10
+ ```ts
11
+ import { BlurryTextRevealComponent } from '@schema-kit';
12
+
13
+ @Component({
14
+ imports: [BlurryTextRevealComponent],
15
+ // ...
16
+ })
17
+ export class ExemploComponent {}
18
+ ```
19
+
20
+ ```html
21
+ <blurry-text-reveal class="font-garamond text-5xl font-semibold text-primary" text="Backoffice" />
22
+ ```
23
+
24
+ Coloque as classes de tipografia e cor no próprio `<blurry-text-reveal>`: as letras herdam fonte, tamanho e cor do host.
25
+
26
+ ## API
27
+
28
+ ### Inputs
29
+
30
+ | Input | Tipo | Padrão | Descrição |
31
+ | ------ | -------- | -------- | ------------------------------------------------------------------- |
32
+ | `text` | `string` | `'Text'` | Texto animado. É dividido em caracteres, com um `<span>` para cada um |
33
+
34
+ ## Comportamentos a saber
35
+
36
+ - **Estilos:** a animação vem do utilitário `letter-reveal`
37
+ ([letter-reveal.css](../../../styles/directives/letter-reveal.css)), carregado pelo `styles.scss` da lib. O `.scss` do
38
+ componente é vazio, então o app precisa importar os estilos da lib.
39
+ - **Duração:** cada letra anima por 500 ms, com atraso de `(posição - 1) × 30 ms`. Um texto de 10 letras termina em
40
+ cerca de 770 ms.
41
+ - **`sibling-index()`:** o atraso é calculado com essa função CSS, que é recente. Em navegadores sem suporte, a
42
+ declaração do `animation-delay` é descartada e todas as letras animam ao mesmo tempo.
43
+ - **Espaços:** cada caractere vira um `<span>` `inline-block`, e um `inline-block` que contém só um espaço fica com
44
+ largura zero. Em textos com mais de uma palavra, os espaços somem ("Novo Backoffice" aparece como "NovoBackoffice").
45
+ Os usos atuais são de uma palavra só.
46
+ - **Troca de texto:** os `<span>` são rastreados pela posição (`track $index`). Ao mudar `text`, as letras nas posições
47
+ que já existiam só trocam de conteúdo, sem animar de novo. Só as posições novas (quando o texto fica mais longo)
48
+ animam.
49
+ - **Movimento reduzido:** a animação não respeita `prefers-reduced-motion`.
@@ -0,0 +1,62 @@
1
+ # breadcrumbs
2
+
3
+ Trilha de navegação (breadcrumb) montada a partir de uma lista de itens, com os primitivos `hlmBreadcrumb*` do Spartan.
4
+
5
+ Usado no topo do [schema-table](../../schema-table/schema-table.component.html) e do
6
+ [schema-view](../../schema-view/schema-view.component.html).
7
+
8
+ ## Uso
9
+
10
+ ```ts
11
+ import { BreadcrumbsComponent, IBreadcrumbItem } from '@schema-kit';
12
+
13
+ @Component({
14
+ imports: [BreadcrumbsComponent],
15
+ // ...
16
+ })
17
+ export class ExemploComponent {
18
+ items: IBreadcrumbItem[] = [
19
+ { label: 'Módulos', routerLink: '/modulo' },
20
+ { label: 'Edição' },
21
+ ];
22
+ }
23
+ ```
24
+
25
+ ```html
26
+ <breadcrumbs [items]="items" />
27
+ ```
28
+
29
+ ### Nas telas de schema
30
+
31
+ Com `schema-table` e `schema-view`, o componente não é usado diretamente: os itens vêm do `SchemaService`.
32
+
33
+ - Se as opções do service têm `breadcrumbs`, essa lista é usada nas duas telas.
34
+ - Senão, a listagem mostra só o `title`, e a edição mostra o `title` (com link para o `endpoint` do service) seguido de
35
+ "Edição" ou "Novo".
36
+ - O `schema-view` também aceita o input `lstBreadCrumbs`, que tem prioridade sobre o service.
37
+
38
+ ## API
39
+
40
+ ### `IBreadcrumbItem`
41
+
42
+ | Campo | Tipo | Descrição |
43
+ | ------------ | --------- | --------------------------------------------------------------- |
44
+ | `label` | `string` | Texto exibido |
45
+ | `routerLink` | `string?` | Rota do item, passada para o `routerLink` do Angular |
46
+ | `icon` | `string?` | Ainda não é exibido pelo componente |
47
+
48
+ ### Inputs
49
+
50
+ | Input | Tipo | Padrão | Descrição |
51
+ | ------- | ------------------- | ------ | ----------------------------------------- |
52
+ | `items` | `IBreadcrumbItem[]` | `[]` | Itens, do mais geral até a página atual |
53
+
54
+ ## Comportamentos a saber
55
+
56
+ - **Último item:** é sempre a página atual e aparece como texto (`hlmBreadcrumbPage`), mesmo que tenha `routerLink`.
57
+ - **Itens sem link:** itens intermediários sem `routerLink` também usam `hlmBreadcrumbPage`, que marca o elemento com
58
+ `aria-current="page"`. Com mais de um item assim, leitores de tela anunciam mais de uma "página atual".
59
+ - **Rotas relativas:** um `routerLink` sem `/` no início é resolvido a partir da rota em que o componente está. Os
60
+ `endpoint`s dos services começam com `/`, então os links gerados por eles são absolutos.
61
+ - **Separador:** o ícone (`lucideChevronRight`) já é registrado pelo `hlmBreadcrumbSeparator`.
62
+ - **Trilhas longas:** não há colapso com reticências (`hlmBreadcrumbEllipsis`): todos os itens são exibidos.
@@ -0,0 +1,60 @@
1
+ # file-preview
2
+
3
+ Botão pequeno (`icon-xs`) de um arquivo. Imagens abrem uma pré-visualização num popover, com o [loading](../loading/README.md)
4
+ por cima enquanto a imagem carrega; os demais arquivos abrem/baixam pelo link.
5
+
6
+ Exemplos reais de uso: [input-file](../../inputs/input-file/input-file.component.html) (arquivo selecionado ou já salvo) e
7
+ [schema-table](../../schema-table/schema-table.component.html) (template das colunas `FieldType.File`).
8
+
9
+ ## Uso
10
+
11
+ ```ts
12
+ import { FilePreviewComponent, fileItemsFromValue } from '@schema-kit';
13
+
14
+ @Component({
15
+ imports: [FilePreviewComponent],
16
+ // ...
17
+ })
18
+ export class ExemploComponent {
19
+ arquivos = computed(() => fileItemsFromValue(this.registro().arquivo));
20
+ }
21
+ ```
22
+
23
+ ```html
24
+ @for (arquivo of arquivos(); track arquivo.url) {
25
+ <file-preview [item]="arquivo" />
26
+ }
27
+ ```
28
+
29
+ ## API
30
+
31
+ ### Inputs
32
+
33
+ | Input | Tipo | Padrão | Descrição |
34
+ | ------ | ---------- | ------------- | ------------------ |
35
+ | `item` | `FileItem` | (obrigatório) | Arquivo a exibir |
36
+
37
+ ### `FileItem`
38
+
39
+ | Campo | Tipo | Descrição |
40
+ | ------- | --------- | ------------------------------------------------------------------------- |
41
+ | `name` | `string` | Nome exibido nos rótulos de acessibilidade e usado no download de `file` |
42
+ | `url` | `string` | URL da imagem ou do link |
43
+ | `image` | `boolean` | Abre a pré-visualização em vez do link |
44
+ | `file` | `File?` | Arquivo local (a `url` é uma object URL); o link baixa com o nome `name` |
45
+
46
+ ### Funções
47
+
48
+ - **`fileItemFromUrl(url)`:** item de um arquivo já existente. O nome é o último segmento do caminho da URL (o nome
49
+ original, nas URLs assinadas do bucket) e `image` vem da extensão.
50
+ - **`fileItemsFromValue(value)`:** itens de um valor vindo da API (`string`, `string[]` ou vazio). Ignora o que não for
51
+ string.
52
+
53
+ ## Comportamentos a saber
54
+
55
+ - **Loading só na primeira abertura:** o popover recria a imagem a cada abertura, mas o navegador reaproveita a já
56
+ carregada. O loading volta a aparecer quando a `url` muda (ex.: nova URL assinada após recarregar o registro).
57
+ - **Erro ao carregar:** o loading some e fica o texto alternativo da imagem.
58
+ - **Cliques:** o componente não para a propagação do clique. Dentro de algo clicável (ex.: linha da tabela), envolva com
59
+ `(click)="$event.stopPropagation()"`.
60
+ - **Object URLs:** quem cria a object URL de um `File` é responsável por revogá-la (o `input-file` já faz isso).
@@ -0,0 +1,10 @@
1
+ # infinte-scroll
2
+
3
+ > **Não implementado.** A pasta contém só o esqueleto gerado pelo Angular CLI.
4
+
5
+ - O template só exibe `infinte-scroll works!`. Não há inputs, outputs nem lógica.
6
+ - O componente (`InfinteScrollComponent`, seletor `lib-infinte-scroll`) **não é exportado** no
7
+ [index.ts](../../../index.ts), então não está disponível via `@schema-kit`.
8
+ - O nome da pasta, dos arquivos e da classe tem um erro de digitação ("infinte" em vez de "infinite"). Vale corrigir
9
+ ao implementar, antes que alguém passe a usar.
10
+ - O seletor usa o prefixo `lib-`, diferente dos outros componentes de `ui/`, que não têm prefixo.