@trismegisto/spine 3.3.0 → 3.3.1

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 (92) hide show
  1. package/README.md +19 -11
  2. package/fesm2022/trismegisto-spine-audit.mjs +16 -12
  3. package/fesm2022/trismegisto-spine-audit.mjs.map +1 -1
  4. package/fesm2022/{trismegisto-spine-auto-service-auto-service-audit.component-tfGcVR3i.mjs → trismegisto-spine-auto-service-auto-service-audit.component-D8HUpZtA.mjs} +4 -4
  5. package/fesm2022/{trismegisto-spine-auto-service-auto-service-audit.component-tfGcVR3i.mjs.map → trismegisto-spine-auto-service-auto-service-audit.component-D8HUpZtA.mjs.map} +1 -1
  6. package/fesm2022/trismegisto-spine-auto-service-auto-service-client-picker.component-NawbfdLW.mjs +65 -0
  7. package/fesm2022/trismegisto-spine-auto-service-auto-service-client-picker.component-NawbfdLW.mjs.map +1 -0
  8. package/fesm2022/{trismegisto-spine-auto-service-auto-service-client.component-IATeedbr.mjs → trismegisto-spine-auto-service-auto-service-client.component-BAfeLyTB.mjs} +27 -19
  9. package/fesm2022/trismegisto-spine-auto-service-auto-service-client.component-BAfeLyTB.mjs.map +1 -0
  10. package/fesm2022/trismegisto-spine-auto-service-auto-service-groups.component-BYjIQ9ID.mjs +635 -0
  11. package/fesm2022/trismegisto-spine-auto-service-auto-service-groups.component-BYjIQ9ID.mjs.map +1 -0
  12. package/fesm2022/{trismegisto-spine-auto-service-auto-service-login-attempts.component-CRvxfSWD.mjs → trismegisto-spine-auto-service-auto-service-login-attempts.component-DoyhkSYc.mjs} +4 -4
  13. package/fesm2022/{trismegisto-spine-auto-service-auto-service-login-attempts.component-CRvxfSWD.mjs.map → trismegisto-spine-auto-service-auto-service-login-attempts.component-DoyhkSYc.mjs.map} +1 -1
  14. package/fesm2022/trismegisto-spine-auto-service-auto-service-news-page.component-CBemH4hr.mjs +432 -0
  15. package/fesm2022/trismegisto-spine-auto-service-auto-service-news-page.component-CBemH4hr.mjs.map +1 -0
  16. package/fesm2022/trismegisto-spine-auto-service-auto-service-ratings-page.component-C_THGTkw.mjs +769 -0
  17. package/fesm2022/trismegisto-spine-auto-service-auto-service-ratings-page.component-C_THGTkw.mjs.map +1 -0
  18. package/fesm2022/trismegisto-spine-auto-service-auto-service-suggestions-page.component-DIeunpQ9.mjs +2123 -0
  19. package/fesm2022/trismegisto-spine-auto-service-auto-service-suggestions-page.component-DIeunpQ9.mjs.map +1 -0
  20. package/fesm2022/{trismegisto-spine-auto-service-auto-service.component-DGrs_Jvw.mjs → trismegisto-spine-auto-service-auto-service.component-QsIQ61w2.mjs} +31 -23
  21. package/fesm2022/trismegisto-spine-auto-service-auto-service.component-QsIQ61w2.mjs.map +1 -0
  22. package/fesm2022/{trismegisto-spine-auto-service-client.usage-C_2zBCRN.mjs → trismegisto-spine-auto-service-client.usage-CJxn9Ysz.mjs} +5 -3
  23. package/fesm2022/trismegisto-spine-auto-service-client.usage-CJxn9Ysz.mjs.map +1 -0
  24. package/fesm2022/{trismegisto-spine-auto-service-groups.technical-9aPPulhy.mjs → trismegisto-spine-auto-service-groups.technical-_jQrg1Qh.mjs} +7 -5
  25. package/fesm2022/trismegisto-spine-auto-service-groups.technical-_jQrg1Qh.mjs.map +1 -0
  26. package/fesm2022/{trismegisto-spine-auto-service-groups.usage-CYh61LFL.mjs → trismegisto-spine-auto-service-groups.usage-OvWjNeeT.mjs} +7 -6
  27. package/fesm2022/{trismegisto-spine-auto-service-groups.usage-CYh61LFL.mjs.map → trismegisto-spine-auto-service-groups.usage-OvWjNeeT.mjs.map} +1 -1
  28. package/fesm2022/trismegisto-spine-auto-service-home.component-rQ3dIGIE.mjs +315 -0
  29. package/fesm2022/trismegisto-spine-auto-service-home.component-rQ3dIGIE.mjs.map +1 -0
  30. package/fesm2022/trismegisto-spine-auto-service-home.technical-Ce7oYRdg.mjs +46 -0
  31. package/fesm2022/trismegisto-spine-auto-service-home.technical-Ce7oYRdg.mjs.map +1 -0
  32. package/fesm2022/trismegisto-spine-auto-service-home.usage-D9R_ZMuU.mjs +48 -0
  33. package/fesm2022/trismegisto-spine-auto-service-home.usage-D9R_ZMuU.mjs.map +1 -0
  34. package/fesm2022/trismegisto-spine-auto-service-news.technical-DV9UpobG.mjs +38 -0
  35. package/fesm2022/trismegisto-spine-auto-service-news.technical-DV9UpobG.mjs.map +1 -0
  36. package/fesm2022/trismegisto-spine-auto-service-news.usage-CeuZnG0V.mjs +48 -0
  37. package/fesm2022/trismegisto-spine-auto-service-news.usage-CeuZnG0V.mjs.map +1 -0
  38. package/fesm2022/trismegisto-spine-auto-service-ratings.technical-QIVj6HBU.mjs +52 -0
  39. package/fesm2022/trismegisto-spine-auto-service-ratings.technical-QIVj6HBU.mjs.map +1 -0
  40. package/fesm2022/trismegisto-spine-auto-service-ratings.usage-BkteY8lz.mjs +45 -0
  41. package/fesm2022/trismegisto-spine-auto-service-ratings.usage-BkteY8lz.mjs.map +1 -0
  42. package/fesm2022/trismegisto-spine-auto-service-suggestions.technical-BtUK6sKD.mjs +69 -0
  43. package/fesm2022/trismegisto-spine-auto-service-suggestions.technical-BtUK6sKD.mjs.map +1 -0
  44. package/fesm2022/trismegisto-spine-auto-service-suggestions.usage-BwO9Hjsx.mjs +72 -0
  45. package/fesm2022/trismegisto-spine-auto-service-suggestions.usage-BwO9Hjsx.mjs.map +1 -0
  46. package/fesm2022/trismegisto-spine-auto-service.mjs +49 -19
  47. package/fesm2022/trismegisto-spine-auto-service.mjs.map +1 -1
  48. package/fesm2022/trismegisto-spine-graphql.mjs +11 -2
  49. package/fesm2022/trismegisto-spine-graphql.mjs.map +1 -1
  50. package/fesm2022/trismegisto-spine-layout.mjs +3160 -1279
  51. package/fesm2022/trismegisto-spine-layout.mjs.map +1 -1
  52. package/fesm2022/trismegisto-spine-modal.mjs +7 -5
  53. package/fesm2022/trismegisto-spine-modal.mjs.map +1 -1
  54. package/fesm2022/trismegisto-spine-routing.mjs +12 -20
  55. package/fesm2022/trismegisto-spine-routing.mjs.map +1 -1
  56. package/fesm2022/trismegisto-spine-shell.mjs +15 -39
  57. package/fesm2022/trismegisto-spine-shell.mjs.map +1 -1
  58. package/fesm2022/trismegisto-spine-side-panel.mjs +7 -5
  59. package/fesm2022/trismegisto-spine-side-panel.mjs.map +1 -1
  60. package/fesm2022/trismegisto-spine.mjs +1969 -279
  61. package/fesm2022/trismegisto-spine.mjs.map +1 -1
  62. package/package.json +6 -6
  63. package/types/trismegisto-spine-audit.d.ts +2 -12
  64. package/types/trismegisto-spine-audit.d.ts.map +1 -1
  65. package/types/trismegisto-spine-auto-service.d.ts +22 -5
  66. package/types/trismegisto-spine-auto-service.d.ts.map +1 -1
  67. package/types/trismegisto-spine-layout.d.ts +1086 -283
  68. package/types/trismegisto-spine-layout.d.ts.map +1 -1
  69. package/types/trismegisto-spine-modal.d.ts +1 -0
  70. package/types/trismegisto-spine-modal.d.ts.map +1 -1
  71. package/types/trismegisto-spine-routing.d.ts +3 -12
  72. package/types/trismegisto-spine-routing.d.ts.map +1 -1
  73. package/types/trismegisto-spine-shell.d.ts +2 -15
  74. package/types/trismegisto-spine-shell.d.ts.map +1 -1
  75. package/types/trismegisto-spine-side-panel.d.ts +1 -0
  76. package/types/trismegisto-spine-side-panel.d.ts.map +1 -1
  77. package/types/trismegisto-spine.d.ts +739 -165
  78. package/types/trismegisto-spine.d.ts.map +1 -1
  79. package/fesm2022/trismegisto-spine-auto-service-auto-service-client-picker.component-Cl7ahisu.mjs +0 -65
  80. package/fesm2022/trismegisto-spine-auto-service-auto-service-client-picker.component-Cl7ahisu.mjs.map +0 -1
  81. package/fesm2022/trismegisto-spine-auto-service-auto-service-client.component-IATeedbr.mjs.map +0 -1
  82. package/fesm2022/trismegisto-spine-auto-service-auto-service-groups.component-D6L0gTmd.mjs +0 -605
  83. package/fesm2022/trismegisto-spine-auto-service-auto-service-groups.component-D6L0gTmd.mjs.map +0 -1
  84. package/fesm2022/trismegisto-spine-auto-service-auto-service.component-DGrs_Jvw.mjs.map +0 -1
  85. package/fesm2022/trismegisto-spine-auto-service-client.usage-C_2zBCRN.mjs.map +0 -1
  86. package/fesm2022/trismegisto-spine-auto-service-groups.technical-9aPPulhy.mjs.map +0 -1
  87. package/fesm2022/trismegisto-spine-auto-service-home.component-5_aji1hf.mjs +0 -239
  88. package/fesm2022/trismegisto-spine-auto-service-home.component-5_aji1hf.mjs.map +0 -1
  89. package/fesm2022/trismegisto-spine-auto-service-home.technical-BrVhcgDO.mjs +0 -35
  90. package/fesm2022/trismegisto-spine-auto-service-home.technical-BrVhcgDO.mjs.map +0 -1
  91. package/fesm2022/trismegisto-spine-auto-service-home.usage-DWYGtngX.mjs +0 -43
  92. package/fesm2022/trismegisto-spine-auto-service-home.usage-DWYGtngX.mjs.map +0 -1
@@ -0,0 +1,2123 @@
1
+ import * as i0 from '@angular/core';
2
+ import { inject, Injectable, input, output, computed, ChangeDetectionStrategy, Component, signal, effect, untracked, viewChild } from '@angular/core';
3
+ import { A as AutoServiceClientPickerComponent } from './trismegisto-spine-auto-service-auto-service-client-picker.component-NawbfdLW.mjs';
4
+ import { Location } from '@angular/common';
5
+ import { Router } from '@angular/router';
6
+ import { AuthorizationService, AUTH_CONFIG } from '@orfeu/angular/core';
7
+ import { ButtonComponent, IconComponent, AccountAvatarComponent, SearchFieldComponent, CheckboxComponent, HermesToastService, HermesDialogService, AlertComponent, DatePickerComponent, IconButtonComponent, InputComponent, ModalComponent, PopoverComponent, PopoverTriggerDirective, RichTextComponent, SelectComponent, SwitchComponent, TabComponent, TabsComponent, TagComponent, TextareaComponent, EmptyComponent, KanbanCardDirective, KanbanColumnActionsDirective, KanbanColumnEmptyDirective, KanbanColumnFooterDirective, KanbanColumnSummaryDirective, KanbanComponent, MenuButtonComponent, PaginatorComponent, SegmentedControlComponent } from '@trismegisto/angular';
8
+ import { urlPath, formatMessage, pluralMessage } from '@trismegisto/primitives';
9
+ import { SPINE_MESSAGES, SpinePreferencesService, SpineUserStorage } from '@trismegisto/spine';
10
+ import { SPINE_SUGGESTION_STATUSES, SPINE_SUGGESTION_STATUS_COLORS, SPINE_SUGGESTION_KINDS, SPINE_SUGGESTION_KIND_COLORS, SPINE_SUGGESTION_LIMITS, SPINE_SUGGESTION_STATUS_ICONS, SPINE_SUGGESTION_KIND_ICONS } from '@trismegisto/spine/layout';
11
+ import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
12
+ import { firstValueFrom } from 'rxjs';
13
+ import { AUTO_SERVICE_CONFIG } from './trismegisto-spine-auto-service.mjs';
14
+
15
+ /** Janelas quando o servidor não diz (as mesmas constantes do argos-admin). */
16
+ const AUTO_SERVICE_SUGGESTION_DONE_DAYS = 7;
17
+ const AUTO_SERVICE_SUGGESTION_DUE_SOON_DAYS = 1;
18
+ /**
19
+ * O quadro de trabalho das sugestões do client — `argos-admin /auto-service/:clientId/suggestions` (atrás do
20
+ * clientAdminGuard, como o resto do auto-serviço). Serviço próprio da tela, para não engordar o `AutoServiceService`.
21
+ * Toda mudança na tarefa responde a tarefa inteira (com o checklist completo); mover responde a tarefa leve.
22
+ */
23
+ class AutoServiceSuggestionsService {
24
+ constructor() {
25
+ this.http = inject(HttpClient);
26
+ this.auth = inject(AuthorizationService);
27
+ this.config = inject(AUTO_SERVICE_CONFIG);
28
+ }
29
+ url(strings, ...ids) {
30
+ return `${this.config.adminApi}/auto-service${urlPath(strings, ...ids)}`;
31
+ }
32
+ get headers() {
33
+ return new HttpHeaders({ 'x-access-token': this.auth.getToken() ?? '' });
34
+ }
35
+ read(url, params) {
36
+ return firstValueFrom(this.http.get(url, { headers: this.headers, params }));
37
+ }
38
+ /**
39
+ * O quadro (`board=true`): sem arquivadas, recusadas e concluídas fora da janela — o corte é do servidor. A tela
40
+ * filtra o resto sobre a lista inteira: a posição de cada cartão é a da coluna inteira.
41
+ */
42
+ async board(clientId) {
43
+ const res = await this.read(this.url `/${clientId}/suggestions`, new HttpParams().set('board', 'true'));
44
+ return {
45
+ suggestions: res.suggestions ?? [],
46
+ doneDays: res.done_days ?? AUTO_SERVICE_SUGGESTION_DONE_DAYS,
47
+ dueSoonDays: res.due_soon_days ?? AUTO_SERVICE_SUGGESTION_DUE_SOON_DAYS,
48
+ };
49
+ }
50
+ /** O arquivo (`archive=true`): arquivadas, recusadas e concluídas antigas, paginado e filtrado no servidor. */
51
+ async archive(clientId, q) {
52
+ let params = new HttpParams().set('archive', 'true').set('limit', q.limit).set('offset', q.offset);
53
+ if (q.search)
54
+ params = params.set('search', q.search);
55
+ if (q.kind)
56
+ params = params.set('kind', q.kind);
57
+ if (q.status)
58
+ params = params.set('status', q.status);
59
+ const res = await this.read(this.url `/${clientId}/suggestions`, params);
60
+ return { suggestions: res.suggestions ?? [], total: res.total ?? 0 };
61
+ }
62
+ /** A tarefa inteira (checklist completo, pessoas, rótulos, última atividade). */
63
+ task(clientId, id) {
64
+ return this.read(this.url `/${clientId}/suggestions/${id}`);
65
+ }
66
+ /** Pela chave ("SUG-12"): o link copiado abre a tarefa. */
67
+ lookup(clientId, key) {
68
+ return this.read(this.url `/${clientId}/suggestions/lookup`, new HttpParams().set('key', key));
69
+ }
70
+ create(clientId, draft) {
71
+ return firstValueFrom(this.http.post(this.url `/${clientId}/suggestions`, draft, { headers: this.headers }));
72
+ }
73
+ update(clientId, id, edit) {
74
+ return firstValueFrom(this.http.patch(this.url `/${clientId}/suggestions/${id}`, edit, { headers: this.headers }));
75
+ }
76
+ /**
77
+ * Arrastar ou mudar pelo detalhe: status (coluna) e/ou posição (índice na coluna de destino, contando a coluna
78
+ * inteira); `comment` vai junto com a mudança de status (`internal` = só a equipe vê).
79
+ */
80
+ move(clientId, id, change) {
81
+ return firstValueFrom(this.http.patch(this.url `/${clientId}/suggestions/${id}`, change, { headers: this.headers }));
82
+ }
83
+ remove(clientId, id) {
84
+ return firstValueFrom(this.http.delete(this.url `/${clientId}/suggestions/${id}`, { headers: this.headers }));
85
+ }
86
+ setArchived(clientId, id, archived) {
87
+ const url = archived ? this.url `/${clientId}/suggestions/${id}/archive` : this.url `/${clientId}/suggestions/${id}/restore`;
88
+ return firstValueFrom(this.http.post(url, {}, { headers: this.headers }));
89
+ }
90
+ /** Prévia do agrupamento em `into`: quantos relatores e apoios passariam (nada é gravado). */
91
+ mergePreview(clientId, id, into) {
92
+ return this.read(this.url `/${clientId}/suggestions/${id}/merge`, new HttpParams().set('into', into));
93
+ }
94
+ /** Agrupa esta em `into`: responde o destino inteiro (com os relatores e as agrupadas). */
95
+ merge(clientId, id, into) {
96
+ return firstValueFrom(this.http.post(this.url `/${clientId}/suggestions/${id}/merge`, { into }, { headers: this.headers }));
97
+ }
98
+ /** Desagrupa esta (a origem): ela volta ao quadro; responde ela inteira. */
99
+ unmerge(clientId, id) {
100
+ return firstValueFrom(this.http.post(this.url `/${clientId}/suggestions/${id}/unmerge`, {}, { headers: this.headers }));
101
+ }
102
+ /** Comentários ou atividades, do mais novo, paginado. */
103
+ events(clientId, id, q) {
104
+ const params = new HttpParams().set('type', q.type).set('limit', q.limit).set('offset', q.offset);
105
+ return this.read(this.url `/${clientId}/suggestions/${id}/events`, params);
106
+ }
107
+ comment(clientId, id, comment, internal = false) {
108
+ return firstValueFrom(this.http.post(this.url `/${clientId}/suggestions/${id}/comments`, { comment, internal }, { headers: this.headers }));
109
+ }
110
+ /** Responsável/envolvido: `userId` ou "me" (atribuir a mim). */
111
+ addPerson(clientId, id, userId, role) {
112
+ return firstValueFrom(this.http.post(this.url `/${clientId}/suggestions/${id}/people`, { user_id: userId, role }, { headers: this.headers }));
113
+ }
114
+ removePerson(clientId, id, userId, role) {
115
+ return firstValueFrom(this.http.delete(this.url `/${clientId}/suggestions/${id}/people/${userId}`, {
116
+ headers: this.headers, params: new HttpParams().set('role', role),
117
+ }));
118
+ }
119
+ addChecklistItem(clientId, id, text) {
120
+ return firstValueFrom(this.http.post(this.url `/${clientId}/suggestions/${id}/checklist`, { text }, { headers: this.headers }));
121
+ }
122
+ updateChecklistItem(clientId, id, itemId, change) {
123
+ return firstValueFrom(this.http.patch(this.url `/${clientId}/suggestions/${id}/checklist/${itemId}`, change, { headers: this.headers }));
124
+ }
125
+ removeChecklistItem(clientId, id, itemId) {
126
+ return firstValueFrom(this.http.delete(this.url `/${clientId}/suggestions/${id}/checklist/${itemId}`, { headers: this.headers }));
127
+ }
128
+ // --- rótulos ---
129
+ async labels(clientId) {
130
+ const res = await this.read(this.url `/${clientId}/suggestions/labels`);
131
+ return res.labels ?? [];
132
+ }
133
+ createLabel(clientId, name, color) {
134
+ return firstValueFrom(this.http.post(this.url `/${clientId}/suggestions/labels`, { name, color }, { headers: this.headers }));
135
+ }
136
+ setLabel(clientId, id, labelId, linked) {
137
+ const url = this.url `/${clientId}/suggestions/${id}/labels/${labelId}`;
138
+ return firstValueFrom(linked
139
+ ? this.http.put(url, {}, { headers: this.headers })
140
+ : this.http.delete(url, { headers: this.headers }));
141
+ }
142
+ // --- pessoas do realm do app (a mesma busca da aba Grupos, com a foto) ---
143
+ async searchPeople(clientId, search) {
144
+ let params = new HttpParams().set('limit', 10);
145
+ if (search.trim())
146
+ params = params.set('search', search.trim());
147
+ const res = await this.read(this.url `/${clientId}/users`, params);
148
+ return res.users ?? [];
149
+ }
150
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AutoServiceSuggestionsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
151
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AutoServiceSuggestionsService, providedIn: 'root' }); }
152
+ }
153
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AutoServiceSuggestionsService, decorators: [{
154
+ type: Injectable,
155
+ args: [{ providedIn: 'root' }]
156
+ }] });
157
+
158
+ // O que a equipe vê de uma sugestão no quadro de trabalho (auto-serviço): a sugestão de quem pediu mais o que é da
159
+ // tarefa -- chave, prioridade, datas, capa, descrição rica, pessoas, rótulos e checklist. Constantes e contas puras
160
+ // (prazo, capa) ficam aqui, fora dos componentes, para o cartão, o detalhe e os testes usarem as mesmas.
161
+ const TASK_PRIORITIES = ['urgente', 'alta', 'media', 'baixa'];
162
+ /** Ícone e intenção de cada prioridade (o texto vem do catálogo; a cor nunca é a única pista). */
163
+ const TASK_PRIORITY_ICONS = {
164
+ urgente: 'exclamation-triangle', alta: 'arrow-up', media: 'minus', baixa: 'arrow-bottom',
165
+ };
166
+ /** Capas: tokens da paleta do tema (o servidor só aceita estes). Sólidas e gradientes. */
167
+ const TASK_SOLID_COVERS = ['cat-1', 'cat-2', 'cat-3', 'cat-4', 'cat-5', 'cat-6', 'cat-7', 'cat-8'];
168
+ const TASK_GRADIENT_COVERS = ['grad-1', 'grad-2', 'grad-3', 'grad-4', 'grad-5', 'grad-6'];
169
+ const TASK_LABEL_COLORS = TASK_SOLID_COVERS;
170
+ /** Hoje como AAAA-MM-DD no fuso dado (o do perfil da pessoa). */
171
+ function todayIso(timeZone, now = new Date()) {
172
+ try {
173
+ return new Intl.DateTimeFormat('en-CA', { timeZone, year: 'numeric', month: '2-digit', day: '2-digit' }).format(now);
174
+ }
175
+ catch {
176
+ return now.toISOString().slice(0, 10);
177
+ }
178
+ }
179
+ /** AAAA-MM-DD mais `days` dias (negativo volta). */
180
+ function addDays(iso, days) {
181
+ const d = new Date(`${iso}T00:00:00Z`);
182
+ d.setUTCDate(d.getUTCDate() + days);
183
+ return d.toISOString().slice(0, 10);
184
+ }
185
+ /** O prazo da tarefa contra hoje. Concluída/recusada não vence. */
186
+ function dueState(task, today) {
187
+ if (!task.due_date || task.status === 'concluida' || task.status === 'recusada')
188
+ return null;
189
+ if (task.due_date < today)
190
+ return 'overdue';
191
+ if (task.due_date === today)
192
+ return 'today';
193
+ if (task.due_date === addDays(today, 1))
194
+ return 'tomorrow';
195
+ return 'later';
196
+ }
197
+ /** Dias inteiros entre duas datas AAAA-MM-DD (`to` - `from`). */
198
+ function daysBetween(from, to) {
199
+ return Math.round((Date.parse(`${to}T00:00:00Z`) - Date.parse(`${from}T00:00:00Z`)) / 86_400_000);
200
+ }
201
+ /** Duração em dias, horas e minutos inteiros (o "4h 12min" do quadro). Negativa vira zero. */
202
+ function splitDuration(ms) {
203
+ const minutes = Math.max(0, Math.floor(ms / 60_000));
204
+ return { d: Math.floor(minutes / 1440), h: Math.floor((minutes % 1440) / 60), m: minutes % 60 };
205
+ }
206
+ /** Responsável por mim? (o `sub` da sessão) */
207
+ const assignedTo = (task, userId) => !!userId && task.assignees.some((p) => p.user_id === userId);
208
+
209
+ // Os pequenos seletores do quadro de sugestões, para ir dentro de um `hm-popover` (o cartão e o detalhe da tarefa):
210
+ // capa (cores e gradientes da paleta do tema), pessoas do realm do app (com foto) e rótulos do catálogo do app.
211
+ /**
212
+ * Capa: amostras sólidas e em gradiente (tokens da paleta, o nome falado em cada uma) e "Remover capa". A escolhida
213
+ * fica `aria-pressed`. Emite o token (ou null).
214
+ */
215
+ class SuggestionCoverPickerComponent {
216
+ constructor() {
217
+ this.text = inject(SPINE_MESSAGES).suggestions;
218
+ this.value = input(null, ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
219
+ this.picked = output();
220
+ this.groups = computed(() => [
221
+ { heading: this.text.coverSolid, items: TASK_SOLID_COVERS.map((token, i) => ({ token, name: formatMessage(this.text.coverName, { n: i + 1 }) })) },
222
+ { heading: this.text.coverGradient, items: TASK_GRADIENT_COVERS.map((token, i) => ({ token, name: formatMessage(this.text.gradientName, { n: i + 1 }) })) },
223
+ ], ...(ngDevMode ? [{ debugName: "groups" }] : /* istanbul ignore next */ []));
224
+ }
225
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionCoverPickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
226
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: SuggestionCoverPickerComponent, isStandalone: true, selector: "spine-suggestion-cover-picker", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { picked: "picked" }, ngImport: i0, template: `
227
+ <div class="as-picker">
228
+ @for (group of groups(); track group.heading) {
229
+ <span class="as-picker__heading">{{ group.heading }}</span>
230
+ <div class="as-picker__swatches">
231
+ @for (c of group.items; track c.token) {
232
+ <button type="button" class="as-picker__swatch" [class]="'as-tok--' + c.token" [attr.aria-label]="c.name"
233
+ [attr.title]="c.name" [attr.aria-pressed]="value() === c.token" (click)="picked.emit(c.token)">
234
+ @if (value() === c.token) { <hm-icon shape="check" /> }
235
+ </button>
236
+ }
237
+ </div>
238
+ }
239
+ <hm-button size="small" emphasis="minimal" icon="times" [disabled]="!value()" (clicked)="picked.emit(null)">{{ text.coverRemove }}</hm-button>
240
+ </div>
241
+ `, isInline: true, styles: [".as-tok--cat-1{--as-tok: var(--hm-data-categorical-1);background:var(--as-tok)}.as-tok--cat-2{--as-tok: var(--hm-data-categorical-2);background:var(--as-tok)}.as-tok--cat-3{--as-tok: var(--hm-data-categorical-3);background:var(--as-tok)}.as-tok--cat-4{--as-tok: var(--hm-data-categorical-4);background:var(--as-tok)}.as-tok--cat-5{--as-tok: var(--hm-data-categorical-5);background:var(--as-tok)}.as-tok--cat-6{--as-tok: var(--hm-data-categorical-6);background:var(--as-tok)}.as-tok--cat-7{--as-tok: var(--hm-data-categorical-7);background:var(--as-tok)}.as-tok--cat-8{--as-tok: var(--hm-data-categorical-8);background:var(--as-tok)}.as-tok--grad-1{--as-tok: var(--hm-data-categorical-1);background:linear-gradient(135deg,var(--hm-data-categorical-1),var(--hm-data-categorical-2))}.as-tok--grad-2{--as-tok: var(--hm-data-categorical-3);background:linear-gradient(135deg,var(--hm-data-categorical-3),var(--hm-data-categorical-4))}.as-tok--grad-3{--as-tok: var(--hm-data-categorical-5);background:linear-gradient(135deg,var(--hm-data-categorical-5),var(--hm-data-categorical-6))}.as-tok--grad-4{--as-tok: var(--hm-data-categorical-7);background:linear-gradient(135deg,var(--hm-data-categorical-7),var(--hm-data-categorical-8))}.as-tok--grad-5{--as-tok: var(--hm-data-categorical-1);background:linear-gradient(135deg,var(--hm-data-categorical-1),var(--hm-data-categorical-5))}.as-tok--grad-6{--as-tok: var(--hm-data-categorical-2);background:linear-gradient(135deg,var(--hm-data-categorical-2),var(--hm-data-categorical-6))}.as-label{display:inline-flex;align-items:center;gap:var(--hm-space-micro);max-inline-size:100%;padding:0 var(--hm-space-xxxs);border-radius:var(--hm-radius-pill);background:color-mix(in srgb,var(--as-tok, var(--hm-border)) 18%,var(--hm-surface-raised));color:var(--hm-text);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold);line-height:1.6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.as-label:before{flex:none;inline-size:.5rem;block-size:.5rem;border-radius:50%;background:var(--as-tok, var(--hm-border));content:\"\"}@media(forced-colors:active){.as-label{border:var(--hm-border-hairline) solid CanvasText}}.as-picker{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-inline-size:15rem;max-inline-size:20rem}.as-picker__heading{color:var(--hm-muted);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold)}.as-picker__muted{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.as-picker__swatches{display:grid;grid-template-columns:repeat(4,1fr);gap:var(--hm-space-xxxs)}.as-picker__swatch{display:inline-flex;align-items:center;justify-content:center;block-size:2rem;padding:0;border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);color:var(--hm-surface-raised);cursor:pointer}.as-picker__swatch[aria-pressed=true]{outline:var(--hm-border-thin) solid var(--hm-text);outline-offset:1px}.as-picker__swatch:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color);outline-offset:1px}.as-picker__swatch--small{block-size:1.5rem}.as-picker__list{display:flex;flex-direction:column;gap:var(--hm-space-micro);max-block-size:16rem;margin:0;padding:0;overflow-y:auto;list-style:none}.as-picker__person{display:flex;align-items:center;gap:var(--hm-space-xs);inline-size:100%;padding:var(--hm-space-micro) var(--hm-space-xxxs);border:0;border-radius:var(--hm-radius-control);background:none;color:var(--hm-text);font:inherit;text-align:start;cursor:pointer}.as-picker__person:hover{background:var(--hm-hover)}.as-picker__person:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color)}.as-picker__person-text{display:flex;flex-direction:column;min-inline-size:0}.as-picker__person-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.as-picker__label-row{display:flex;align-items:center;gap:var(--hm-space-xs)}.as-picker__dot{flex:none;inline-size:.625rem;block-size:.625rem;border-radius:50%}\n"], dependencies: [{ kind: "component", type: ButtonComponent, selector: "hm-button", inputs: ["emphasis", "intent", "size", "type", "icon", "iconDirection", "iconEnd", "disabled", "loading", "loadingState", "block", "link", "newTab", "caret", "inMenu", "toggle", "pressed"], outputs: ["loadingStateChange", "pressedChange", "clicked"] }, { kind: "component", type: IconComponent, selector: "hm-icon", inputs: ["shape", "direction", "size", "solid", "badge", "status", "flip", "inverse", "label"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
242
+ }
243
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionCoverPickerComponent, decorators: [{
244
+ type: Component,
245
+ args: [{ selector: 'spine-suggestion-cover-picker', imports: [ButtonComponent, IconComponent], template: `
246
+ <div class="as-picker">
247
+ @for (group of groups(); track group.heading) {
248
+ <span class="as-picker__heading">{{ group.heading }}</span>
249
+ <div class="as-picker__swatches">
250
+ @for (c of group.items; track c.token) {
251
+ <button type="button" class="as-picker__swatch" [class]="'as-tok--' + c.token" [attr.aria-label]="c.name"
252
+ [attr.title]="c.name" [attr.aria-pressed]="value() === c.token" (click)="picked.emit(c.token)">
253
+ @if (value() === c.token) { <hm-icon shape="check" /> }
254
+ </button>
255
+ }
256
+ </div>
257
+ }
258
+ <hm-button size="small" emphasis="minimal" icon="times" [disabled]="!value()" (clicked)="picked.emit(null)">{{ text.coverRemove }}</hm-button>
259
+ </div>
260
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".as-tok--cat-1{--as-tok: var(--hm-data-categorical-1);background:var(--as-tok)}.as-tok--cat-2{--as-tok: var(--hm-data-categorical-2);background:var(--as-tok)}.as-tok--cat-3{--as-tok: var(--hm-data-categorical-3);background:var(--as-tok)}.as-tok--cat-4{--as-tok: var(--hm-data-categorical-4);background:var(--as-tok)}.as-tok--cat-5{--as-tok: var(--hm-data-categorical-5);background:var(--as-tok)}.as-tok--cat-6{--as-tok: var(--hm-data-categorical-6);background:var(--as-tok)}.as-tok--cat-7{--as-tok: var(--hm-data-categorical-7);background:var(--as-tok)}.as-tok--cat-8{--as-tok: var(--hm-data-categorical-8);background:var(--as-tok)}.as-tok--grad-1{--as-tok: var(--hm-data-categorical-1);background:linear-gradient(135deg,var(--hm-data-categorical-1),var(--hm-data-categorical-2))}.as-tok--grad-2{--as-tok: var(--hm-data-categorical-3);background:linear-gradient(135deg,var(--hm-data-categorical-3),var(--hm-data-categorical-4))}.as-tok--grad-3{--as-tok: var(--hm-data-categorical-5);background:linear-gradient(135deg,var(--hm-data-categorical-5),var(--hm-data-categorical-6))}.as-tok--grad-4{--as-tok: var(--hm-data-categorical-7);background:linear-gradient(135deg,var(--hm-data-categorical-7),var(--hm-data-categorical-8))}.as-tok--grad-5{--as-tok: var(--hm-data-categorical-1);background:linear-gradient(135deg,var(--hm-data-categorical-1),var(--hm-data-categorical-5))}.as-tok--grad-6{--as-tok: var(--hm-data-categorical-2);background:linear-gradient(135deg,var(--hm-data-categorical-2),var(--hm-data-categorical-6))}.as-label{display:inline-flex;align-items:center;gap:var(--hm-space-micro);max-inline-size:100%;padding:0 var(--hm-space-xxxs);border-radius:var(--hm-radius-pill);background:color-mix(in srgb,var(--as-tok, var(--hm-border)) 18%,var(--hm-surface-raised));color:var(--hm-text);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold);line-height:1.6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.as-label:before{flex:none;inline-size:.5rem;block-size:.5rem;border-radius:50%;background:var(--as-tok, var(--hm-border));content:\"\"}@media(forced-colors:active){.as-label{border:var(--hm-border-hairline) solid CanvasText}}.as-picker{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-inline-size:15rem;max-inline-size:20rem}.as-picker__heading{color:var(--hm-muted);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold)}.as-picker__muted{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.as-picker__swatches{display:grid;grid-template-columns:repeat(4,1fr);gap:var(--hm-space-xxxs)}.as-picker__swatch{display:inline-flex;align-items:center;justify-content:center;block-size:2rem;padding:0;border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);color:var(--hm-surface-raised);cursor:pointer}.as-picker__swatch[aria-pressed=true]{outline:var(--hm-border-thin) solid var(--hm-text);outline-offset:1px}.as-picker__swatch:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color);outline-offset:1px}.as-picker__swatch--small{block-size:1.5rem}.as-picker__list{display:flex;flex-direction:column;gap:var(--hm-space-micro);max-block-size:16rem;margin:0;padding:0;overflow-y:auto;list-style:none}.as-picker__person{display:flex;align-items:center;gap:var(--hm-space-xs);inline-size:100%;padding:var(--hm-space-micro) var(--hm-space-xxxs);border:0;border-radius:var(--hm-radius-control);background:none;color:var(--hm-text);font:inherit;text-align:start;cursor:pointer}.as-picker__person:hover{background:var(--hm-hover)}.as-picker__person:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color)}.as-picker__person-text{display:flex;flex-direction:column;min-inline-size:0}.as-picker__person-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.as-picker__label-row{display:flex;align-items:center;gap:var(--hm-space-xs)}.as-picker__dot{flex:none;inline-size:.625rem;block-size:.625rem;border-radius:50%}\n"] }]
261
+ }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], picked: [{ type: i0.Output, args: ["picked"] }] } });
262
+ /**
263
+ * Pessoa do realm do app: busca (a mesma do auto-serviço, com a foto), as já escolhidas de fora. Lê na abertura (as
264
+ * primeiras) e a cada termo, com espera.
265
+ */
266
+ class SuggestionPeoplePickerComponent {
267
+ constructor() {
268
+ this.text = inject(SPINE_MESSAGES).suggestions;
269
+ this.service = inject(AutoServiceSuggestionsService);
270
+ this.clientId = input.required(...(ngDevMode ? [{ debugName: "clientId" }] : /* istanbul ignore next */ []));
271
+ /** Quem já está no papel (não aparece de novo). */
272
+ this.exclude = input([], ...(ngDevMode ? [{ debugName: "exclude" }] : /* istanbul ignore next */ []));
273
+ this.picked = output();
274
+ this.found = signal([], ...(ngDevMode ? [{ debugName: "found" }] : /* istanbul ignore next */ []));
275
+ this.loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
276
+ this.options = computed(() => this.found().filter((p) => !this.exclude().includes(p.id)), ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
277
+ this.seq = 0;
278
+ effect(() => {
279
+ this.clientId();
280
+ untracked(() => void this.load(''));
281
+ });
282
+ }
283
+ async load(term) {
284
+ const mine = ++this.seq;
285
+ this.loading.set(true);
286
+ try {
287
+ const list = await this.service.searchPeople(this.clientId(), term);
288
+ if (mine === this.seq)
289
+ this.found.set(list);
290
+ }
291
+ catch {
292
+ if (mine === this.seq)
293
+ this.found.set([]);
294
+ }
295
+ finally {
296
+ if (mine === this.seq)
297
+ this.loading.set(false);
298
+ }
299
+ }
300
+ nameOf(p) {
301
+ return p.name || p.username || p.email || '';
302
+ }
303
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionPeoplePickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
304
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: SuggestionPeoplePickerComponent, isStandalone: true, selector: "spine-suggestion-people-picker", inputs: { clientId: { classPropertyName: "clientId", publicName: "clientId", isSignal: true, isRequired: true, transformFunction: null }, exclude: { classPropertyName: "exclude", publicName: "exclude", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { picked: "picked" }, ngImport: i0, template: `
305
+ <div class="as-picker">
306
+ <hm-search-field [label]="text.peopleSearch" labelHidden [placeholder]="text.peopleSearch" [landmark]="false"
307
+ [debounce]="250" [loading]="loading()" (search)="load($event)" />
308
+ <ul class="as-picker__list" [attr.aria-busy]="loading() ? 'true' : null">
309
+ @for (p of options(); track p.id) {
310
+ <li>
311
+ <button type="button" class="as-picker__person" (click)="picked.emit(p)">
312
+ <hm-account-avatar size="small" [name]="nameOf(p)" [imageUrl]="p.picture" />
313
+ <span class="as-picker__person-text">
314
+ <span class="as-picker__person-name">{{ nameOf(p) }}</span>
315
+ @if (p.username && p.name) { <span class="as-picker__muted">{{ p.username }}</span> }
316
+ </span>
317
+ </button>
318
+ </li>
319
+ } @empty {
320
+ @if (!loading()) { <li class="as-picker__muted">{{ text.peopleEmpty }}</li> }
321
+ }
322
+ </ul>
323
+ </div>
324
+ `, isInline: true, styles: [".as-tok--cat-1{--as-tok: var(--hm-data-categorical-1);background:var(--as-tok)}.as-tok--cat-2{--as-tok: var(--hm-data-categorical-2);background:var(--as-tok)}.as-tok--cat-3{--as-tok: var(--hm-data-categorical-3);background:var(--as-tok)}.as-tok--cat-4{--as-tok: var(--hm-data-categorical-4);background:var(--as-tok)}.as-tok--cat-5{--as-tok: var(--hm-data-categorical-5);background:var(--as-tok)}.as-tok--cat-6{--as-tok: var(--hm-data-categorical-6);background:var(--as-tok)}.as-tok--cat-7{--as-tok: var(--hm-data-categorical-7);background:var(--as-tok)}.as-tok--cat-8{--as-tok: var(--hm-data-categorical-8);background:var(--as-tok)}.as-tok--grad-1{--as-tok: var(--hm-data-categorical-1);background:linear-gradient(135deg,var(--hm-data-categorical-1),var(--hm-data-categorical-2))}.as-tok--grad-2{--as-tok: var(--hm-data-categorical-3);background:linear-gradient(135deg,var(--hm-data-categorical-3),var(--hm-data-categorical-4))}.as-tok--grad-3{--as-tok: var(--hm-data-categorical-5);background:linear-gradient(135deg,var(--hm-data-categorical-5),var(--hm-data-categorical-6))}.as-tok--grad-4{--as-tok: var(--hm-data-categorical-7);background:linear-gradient(135deg,var(--hm-data-categorical-7),var(--hm-data-categorical-8))}.as-tok--grad-5{--as-tok: var(--hm-data-categorical-1);background:linear-gradient(135deg,var(--hm-data-categorical-1),var(--hm-data-categorical-5))}.as-tok--grad-6{--as-tok: var(--hm-data-categorical-2);background:linear-gradient(135deg,var(--hm-data-categorical-2),var(--hm-data-categorical-6))}.as-label{display:inline-flex;align-items:center;gap:var(--hm-space-micro);max-inline-size:100%;padding:0 var(--hm-space-xxxs);border-radius:var(--hm-radius-pill);background:color-mix(in srgb,var(--as-tok, var(--hm-border)) 18%,var(--hm-surface-raised));color:var(--hm-text);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold);line-height:1.6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.as-label:before{flex:none;inline-size:.5rem;block-size:.5rem;border-radius:50%;background:var(--as-tok, var(--hm-border));content:\"\"}@media(forced-colors:active){.as-label{border:var(--hm-border-hairline) solid CanvasText}}.as-picker{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-inline-size:15rem;max-inline-size:20rem}.as-picker__heading{color:var(--hm-muted);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold)}.as-picker__muted{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.as-picker__swatches{display:grid;grid-template-columns:repeat(4,1fr);gap:var(--hm-space-xxxs)}.as-picker__swatch{display:inline-flex;align-items:center;justify-content:center;block-size:2rem;padding:0;border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);color:var(--hm-surface-raised);cursor:pointer}.as-picker__swatch[aria-pressed=true]{outline:var(--hm-border-thin) solid var(--hm-text);outline-offset:1px}.as-picker__swatch:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color);outline-offset:1px}.as-picker__swatch--small{block-size:1.5rem}.as-picker__list{display:flex;flex-direction:column;gap:var(--hm-space-micro);max-block-size:16rem;margin:0;padding:0;overflow-y:auto;list-style:none}.as-picker__person{display:flex;align-items:center;gap:var(--hm-space-xs);inline-size:100%;padding:var(--hm-space-micro) var(--hm-space-xxxs);border:0;border-radius:var(--hm-radius-control);background:none;color:var(--hm-text);font:inherit;text-align:start;cursor:pointer}.as-picker__person:hover{background:var(--hm-hover)}.as-picker__person:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color)}.as-picker__person-text{display:flex;flex-direction:column;min-inline-size:0}.as-picker__person-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.as-picker__label-row{display:flex;align-items:center;gap:var(--hm-space-xs)}.as-picker__dot{flex:none;inline-size:.625rem;block-size:.625rem;border-radius:50%}\n"], dependencies: [{ kind: "component", type: AccountAvatarComponent, selector: "hm-account-avatar", inputs: ["name", "imageUrl", "size", "loading"] }, { kind: "component", type: SearchFieldComponent, selector: "hm-search-field", inputs: ["value", "label", "labelHidden", "placeholder", "hint", "size", "debounce", "landmark", "loading"], outputs: ["valueChange", "search", "cleared"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
325
+ }
326
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionPeoplePickerComponent, decorators: [{
327
+ type: Component,
328
+ args: [{ selector: 'spine-suggestion-people-picker', imports: [AccountAvatarComponent, SearchFieldComponent], template: `
329
+ <div class="as-picker">
330
+ <hm-search-field [label]="text.peopleSearch" labelHidden [placeholder]="text.peopleSearch" [landmark]="false"
331
+ [debounce]="250" [loading]="loading()" (search)="load($event)" />
332
+ <ul class="as-picker__list" [attr.aria-busy]="loading() ? 'true' : null">
333
+ @for (p of options(); track p.id) {
334
+ <li>
335
+ <button type="button" class="as-picker__person" (click)="picked.emit(p)">
336
+ <hm-account-avatar size="small" [name]="nameOf(p)" [imageUrl]="p.picture" />
337
+ <span class="as-picker__person-text">
338
+ <span class="as-picker__person-name">{{ nameOf(p) }}</span>
339
+ @if (p.username && p.name) { <span class="as-picker__muted">{{ p.username }}</span> }
340
+ </span>
341
+ </button>
342
+ </li>
343
+ } @empty {
344
+ @if (!loading()) { <li class="as-picker__muted">{{ text.peopleEmpty }}</li> }
345
+ }
346
+ </ul>
347
+ </div>
348
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".as-tok--cat-1{--as-tok: var(--hm-data-categorical-1);background:var(--as-tok)}.as-tok--cat-2{--as-tok: var(--hm-data-categorical-2);background:var(--as-tok)}.as-tok--cat-3{--as-tok: var(--hm-data-categorical-3);background:var(--as-tok)}.as-tok--cat-4{--as-tok: var(--hm-data-categorical-4);background:var(--as-tok)}.as-tok--cat-5{--as-tok: var(--hm-data-categorical-5);background:var(--as-tok)}.as-tok--cat-6{--as-tok: var(--hm-data-categorical-6);background:var(--as-tok)}.as-tok--cat-7{--as-tok: var(--hm-data-categorical-7);background:var(--as-tok)}.as-tok--cat-8{--as-tok: var(--hm-data-categorical-8);background:var(--as-tok)}.as-tok--grad-1{--as-tok: var(--hm-data-categorical-1);background:linear-gradient(135deg,var(--hm-data-categorical-1),var(--hm-data-categorical-2))}.as-tok--grad-2{--as-tok: var(--hm-data-categorical-3);background:linear-gradient(135deg,var(--hm-data-categorical-3),var(--hm-data-categorical-4))}.as-tok--grad-3{--as-tok: var(--hm-data-categorical-5);background:linear-gradient(135deg,var(--hm-data-categorical-5),var(--hm-data-categorical-6))}.as-tok--grad-4{--as-tok: var(--hm-data-categorical-7);background:linear-gradient(135deg,var(--hm-data-categorical-7),var(--hm-data-categorical-8))}.as-tok--grad-5{--as-tok: var(--hm-data-categorical-1);background:linear-gradient(135deg,var(--hm-data-categorical-1),var(--hm-data-categorical-5))}.as-tok--grad-6{--as-tok: var(--hm-data-categorical-2);background:linear-gradient(135deg,var(--hm-data-categorical-2),var(--hm-data-categorical-6))}.as-label{display:inline-flex;align-items:center;gap:var(--hm-space-micro);max-inline-size:100%;padding:0 var(--hm-space-xxxs);border-radius:var(--hm-radius-pill);background:color-mix(in srgb,var(--as-tok, var(--hm-border)) 18%,var(--hm-surface-raised));color:var(--hm-text);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold);line-height:1.6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.as-label:before{flex:none;inline-size:.5rem;block-size:.5rem;border-radius:50%;background:var(--as-tok, var(--hm-border));content:\"\"}@media(forced-colors:active){.as-label{border:var(--hm-border-hairline) solid CanvasText}}.as-picker{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-inline-size:15rem;max-inline-size:20rem}.as-picker__heading{color:var(--hm-muted);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold)}.as-picker__muted{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.as-picker__swatches{display:grid;grid-template-columns:repeat(4,1fr);gap:var(--hm-space-xxxs)}.as-picker__swatch{display:inline-flex;align-items:center;justify-content:center;block-size:2rem;padding:0;border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);color:var(--hm-surface-raised);cursor:pointer}.as-picker__swatch[aria-pressed=true]{outline:var(--hm-border-thin) solid var(--hm-text);outline-offset:1px}.as-picker__swatch:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color);outline-offset:1px}.as-picker__swatch--small{block-size:1.5rem}.as-picker__list{display:flex;flex-direction:column;gap:var(--hm-space-micro);max-block-size:16rem;margin:0;padding:0;overflow-y:auto;list-style:none}.as-picker__person{display:flex;align-items:center;gap:var(--hm-space-xs);inline-size:100%;padding:var(--hm-space-micro) var(--hm-space-xxxs);border:0;border-radius:var(--hm-radius-control);background:none;color:var(--hm-text);font:inherit;text-align:start;cursor:pointer}.as-picker__person:hover{background:var(--hm-hover)}.as-picker__person:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color)}.as-picker__person-text{display:flex;flex-direction:column;min-inline-size:0}.as-picker__person-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.as-picker__label-row{display:flex;align-items:center;gap:var(--hm-space-xs)}.as-picker__dot{flex:none;inline-size:.625rem;block-size:.625rem;border-radius:50%}\n"] }]
349
+ }], ctorParameters: () => [], propDecorators: { clientId: [{ type: i0.Input, args: [{ isSignal: true, alias: "clientId", required: true }] }], exclude: [{ type: i0.Input, args: [{ isSignal: true, alias: "exclude", required: false }] }], picked: [{ type: i0.Output, args: ["picked"] }] } });
350
+ /**
351
+ * Rótulos do catálogo do app: marcar/desmarcar (caixa por rótulo, o ponto na cor dele) e, quando o termo digitado não
352
+ * existe, "Criar o rótulo" com a cor escolhida.
353
+ */
354
+ class SuggestionLabelPickerComponent {
355
+ constructor() {
356
+ this.text = inject(SPINE_MESSAGES).suggestions;
357
+ this.labels = input([], ...(ngDevMode ? [{ debugName: "labels" }] : /* istanbul ignore next */ []));
358
+ this.selected = input([], ...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
359
+ this.toggled = output();
360
+ this.created = output();
361
+ this.colors = TASK_LABEL_COLORS;
362
+ this.term = signal('', ...(ngDevMode ? [{ debugName: "term" }] : /* istanbul ignore next */ []));
363
+ this.color = signal('cat-1', ...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
364
+ this.shown = computed(() => {
365
+ const t = this.term().trim().toLocaleLowerCase();
366
+ return this.labels().filter((l) => !t || l.name.toLocaleLowerCase().includes(t));
367
+ }, ...(ngDevMode ? [{ debugName: "shown" }] : /* istanbul ignore next */ []));
368
+ /** Termo que ainda não é rótulo: dá para criar. */
369
+ this.creatable = computed(() => {
370
+ const t = this.term().trim().toLocaleLowerCase();
371
+ return !!t && t.length <= 40 && !this.labels().some((l) => l.name.toLocaleLowerCase() === t);
372
+ }, ...(ngDevMode ? [{ debugName: "creatable" }] : /* istanbul ignore next */ []));
373
+ this.createText = computed(() => formatMessage(this.text.createLabel, { name: this.term().trim() }), ...(ngDevMode ? [{ debugName: "createText" }] : /* istanbul ignore next */ []));
374
+ }
375
+ colorName(i) {
376
+ return formatMessage(this.text.coverName, { n: i + 1 });
377
+ }
378
+ create() {
379
+ const name = this.term().trim();
380
+ if (!name)
381
+ return;
382
+ this.created.emit({ name, color: this.color() });
383
+ this.term.set('');
384
+ }
385
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionLabelPickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
386
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: SuggestionLabelPickerComponent, isStandalone: true, selector: "spine-suggestion-label-picker", inputs: { labels: { classPropertyName: "labels", publicName: "labels", isSignal: true, isRequired: false, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { toggled: "toggled", created: "created" }, ngImport: i0, template: `
387
+ <div class="as-picker">
388
+ <hm-search-field [label]="text.labelFilter" labelHidden [placeholder]="text.labelFilter" [landmark]="false"
389
+ [value]="term()" (valueChange)="term.set($event)" />
390
+ <ul class="as-picker__list">
391
+ @for (l of shown(); track l.id) {
392
+ <li class="as-picker__label-row">
393
+ <span class="as-picker__dot" [class]="'as-tok--' + l.color" aria-hidden="true"></span>
394
+ <hm-checkbox [label]="l.name" [checked]="selected().includes(l.id)" (checkedChange)="toggled.emit({ label: l, linked: $event })" />
395
+ </li>
396
+ } @empty {
397
+ @if (!creatable()) { <li class="as-picker__muted">{{ text.noLabels }}</li> }
398
+ }
399
+ </ul>
400
+ @if (creatable()) {
401
+ <span class="as-picker__heading">{{ text.labelColor }}</span>
402
+ <div class="as-picker__swatches">
403
+ @for (c of colors; track c; let i = $index) {
404
+ <button type="button" class="as-picker__swatch as-picker__swatch--small" [class]="'as-tok--' + c"
405
+ [attr.aria-label]="colorName(i)" [attr.aria-pressed]="color() === c" (click)="color.set(c)"></button>
406
+ }
407
+ </div>
408
+ <hm-button size="small" emphasis="medium" icon="plus" (clicked)="create()">{{ createText() }}</hm-button>
409
+ }
410
+ </div>
411
+ `, isInline: true, styles: [".as-tok--cat-1{--as-tok: var(--hm-data-categorical-1);background:var(--as-tok)}.as-tok--cat-2{--as-tok: var(--hm-data-categorical-2);background:var(--as-tok)}.as-tok--cat-3{--as-tok: var(--hm-data-categorical-3);background:var(--as-tok)}.as-tok--cat-4{--as-tok: var(--hm-data-categorical-4);background:var(--as-tok)}.as-tok--cat-5{--as-tok: var(--hm-data-categorical-5);background:var(--as-tok)}.as-tok--cat-6{--as-tok: var(--hm-data-categorical-6);background:var(--as-tok)}.as-tok--cat-7{--as-tok: var(--hm-data-categorical-7);background:var(--as-tok)}.as-tok--cat-8{--as-tok: var(--hm-data-categorical-8);background:var(--as-tok)}.as-tok--grad-1{--as-tok: var(--hm-data-categorical-1);background:linear-gradient(135deg,var(--hm-data-categorical-1),var(--hm-data-categorical-2))}.as-tok--grad-2{--as-tok: var(--hm-data-categorical-3);background:linear-gradient(135deg,var(--hm-data-categorical-3),var(--hm-data-categorical-4))}.as-tok--grad-3{--as-tok: var(--hm-data-categorical-5);background:linear-gradient(135deg,var(--hm-data-categorical-5),var(--hm-data-categorical-6))}.as-tok--grad-4{--as-tok: var(--hm-data-categorical-7);background:linear-gradient(135deg,var(--hm-data-categorical-7),var(--hm-data-categorical-8))}.as-tok--grad-5{--as-tok: var(--hm-data-categorical-1);background:linear-gradient(135deg,var(--hm-data-categorical-1),var(--hm-data-categorical-5))}.as-tok--grad-6{--as-tok: var(--hm-data-categorical-2);background:linear-gradient(135deg,var(--hm-data-categorical-2),var(--hm-data-categorical-6))}.as-label{display:inline-flex;align-items:center;gap:var(--hm-space-micro);max-inline-size:100%;padding:0 var(--hm-space-xxxs);border-radius:var(--hm-radius-pill);background:color-mix(in srgb,var(--as-tok, var(--hm-border)) 18%,var(--hm-surface-raised));color:var(--hm-text);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold);line-height:1.6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.as-label:before{flex:none;inline-size:.5rem;block-size:.5rem;border-radius:50%;background:var(--as-tok, var(--hm-border));content:\"\"}@media(forced-colors:active){.as-label{border:var(--hm-border-hairline) solid CanvasText}}.as-picker{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-inline-size:15rem;max-inline-size:20rem}.as-picker__heading{color:var(--hm-muted);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold)}.as-picker__muted{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.as-picker__swatches{display:grid;grid-template-columns:repeat(4,1fr);gap:var(--hm-space-xxxs)}.as-picker__swatch{display:inline-flex;align-items:center;justify-content:center;block-size:2rem;padding:0;border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);color:var(--hm-surface-raised);cursor:pointer}.as-picker__swatch[aria-pressed=true]{outline:var(--hm-border-thin) solid var(--hm-text);outline-offset:1px}.as-picker__swatch:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color);outline-offset:1px}.as-picker__swatch--small{block-size:1.5rem}.as-picker__list{display:flex;flex-direction:column;gap:var(--hm-space-micro);max-block-size:16rem;margin:0;padding:0;overflow-y:auto;list-style:none}.as-picker__person{display:flex;align-items:center;gap:var(--hm-space-xs);inline-size:100%;padding:var(--hm-space-micro) var(--hm-space-xxxs);border:0;border-radius:var(--hm-radius-control);background:none;color:var(--hm-text);font:inherit;text-align:start;cursor:pointer}.as-picker__person:hover{background:var(--hm-hover)}.as-picker__person:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color)}.as-picker__person-text{display:flex;flex-direction:column;min-inline-size:0}.as-picker__person-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.as-picker__label-row{display:flex;align-items:center;gap:var(--hm-space-xs)}.as-picker__dot{flex:none;inline-size:.625rem;block-size:.625rem;border-radius:50%}\n"], dependencies: [{ kind: "component", type: ButtonComponent, selector: "hm-button", inputs: ["emphasis", "intent", "size", "type", "icon", "iconDirection", "iconEnd", "disabled", "loading", "loadingState", "block", "link", "newTab", "caret", "inMenu", "toggle", "pressed"], outputs: ["loadingStateChange", "pressedChange", "clicked"] }, { kind: "component", type: CheckboxComponent, selector: "hm-checkbox", inputs: ["label", "labelHidden", "checked", "indeterminate"], outputs: ["checkedChange"] }, { kind: "component", type: SearchFieldComponent, selector: "hm-search-field", inputs: ["value", "label", "labelHidden", "placeholder", "hint", "size", "debounce", "landmark", "loading"], outputs: ["valueChange", "search", "cleared"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
412
+ }
413
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionLabelPickerComponent, decorators: [{
414
+ type: Component,
415
+ args: [{ selector: 'spine-suggestion-label-picker', imports: [ButtonComponent, CheckboxComponent, SearchFieldComponent], template: `
416
+ <div class="as-picker">
417
+ <hm-search-field [label]="text.labelFilter" labelHidden [placeholder]="text.labelFilter" [landmark]="false"
418
+ [value]="term()" (valueChange)="term.set($event)" />
419
+ <ul class="as-picker__list">
420
+ @for (l of shown(); track l.id) {
421
+ <li class="as-picker__label-row">
422
+ <span class="as-picker__dot" [class]="'as-tok--' + l.color" aria-hidden="true"></span>
423
+ <hm-checkbox [label]="l.name" [checked]="selected().includes(l.id)" (checkedChange)="toggled.emit({ label: l, linked: $event })" />
424
+ </li>
425
+ } @empty {
426
+ @if (!creatable()) { <li class="as-picker__muted">{{ text.noLabels }}</li> }
427
+ }
428
+ </ul>
429
+ @if (creatable()) {
430
+ <span class="as-picker__heading">{{ text.labelColor }}</span>
431
+ <div class="as-picker__swatches">
432
+ @for (c of colors; track c; let i = $index) {
433
+ <button type="button" class="as-picker__swatch as-picker__swatch--small" [class]="'as-tok--' + c"
434
+ [attr.aria-label]="colorName(i)" [attr.aria-pressed]="color() === c" (click)="color.set(c)"></button>
435
+ }
436
+ </div>
437
+ <hm-button size="small" emphasis="medium" icon="plus" (clicked)="create()">{{ createText() }}</hm-button>
438
+ }
439
+ </div>
440
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".as-tok--cat-1{--as-tok: var(--hm-data-categorical-1);background:var(--as-tok)}.as-tok--cat-2{--as-tok: var(--hm-data-categorical-2);background:var(--as-tok)}.as-tok--cat-3{--as-tok: var(--hm-data-categorical-3);background:var(--as-tok)}.as-tok--cat-4{--as-tok: var(--hm-data-categorical-4);background:var(--as-tok)}.as-tok--cat-5{--as-tok: var(--hm-data-categorical-5);background:var(--as-tok)}.as-tok--cat-6{--as-tok: var(--hm-data-categorical-6);background:var(--as-tok)}.as-tok--cat-7{--as-tok: var(--hm-data-categorical-7);background:var(--as-tok)}.as-tok--cat-8{--as-tok: var(--hm-data-categorical-8);background:var(--as-tok)}.as-tok--grad-1{--as-tok: var(--hm-data-categorical-1);background:linear-gradient(135deg,var(--hm-data-categorical-1),var(--hm-data-categorical-2))}.as-tok--grad-2{--as-tok: var(--hm-data-categorical-3);background:linear-gradient(135deg,var(--hm-data-categorical-3),var(--hm-data-categorical-4))}.as-tok--grad-3{--as-tok: var(--hm-data-categorical-5);background:linear-gradient(135deg,var(--hm-data-categorical-5),var(--hm-data-categorical-6))}.as-tok--grad-4{--as-tok: var(--hm-data-categorical-7);background:linear-gradient(135deg,var(--hm-data-categorical-7),var(--hm-data-categorical-8))}.as-tok--grad-5{--as-tok: var(--hm-data-categorical-1);background:linear-gradient(135deg,var(--hm-data-categorical-1),var(--hm-data-categorical-5))}.as-tok--grad-6{--as-tok: var(--hm-data-categorical-2);background:linear-gradient(135deg,var(--hm-data-categorical-2),var(--hm-data-categorical-6))}.as-label{display:inline-flex;align-items:center;gap:var(--hm-space-micro);max-inline-size:100%;padding:0 var(--hm-space-xxxs);border-radius:var(--hm-radius-pill);background:color-mix(in srgb,var(--as-tok, var(--hm-border)) 18%,var(--hm-surface-raised));color:var(--hm-text);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold);line-height:1.6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.as-label:before{flex:none;inline-size:.5rem;block-size:.5rem;border-radius:50%;background:var(--as-tok, var(--hm-border));content:\"\"}@media(forced-colors:active){.as-label{border:var(--hm-border-hairline) solid CanvasText}}.as-picker{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-inline-size:15rem;max-inline-size:20rem}.as-picker__heading{color:var(--hm-muted);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold)}.as-picker__muted{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.as-picker__swatches{display:grid;grid-template-columns:repeat(4,1fr);gap:var(--hm-space-xxxs)}.as-picker__swatch{display:inline-flex;align-items:center;justify-content:center;block-size:2rem;padding:0;border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);color:var(--hm-surface-raised);cursor:pointer}.as-picker__swatch[aria-pressed=true]{outline:var(--hm-border-thin) solid var(--hm-text);outline-offset:1px}.as-picker__swatch:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color);outline-offset:1px}.as-picker__swatch--small{block-size:1.5rem}.as-picker__list{display:flex;flex-direction:column;gap:var(--hm-space-micro);max-block-size:16rem;margin:0;padding:0;overflow-y:auto;list-style:none}.as-picker__person{display:flex;align-items:center;gap:var(--hm-space-xs);inline-size:100%;padding:var(--hm-space-micro) var(--hm-space-xxxs);border:0;border-radius:var(--hm-radius-control);background:none;color:var(--hm-text);font:inherit;text-align:start;cursor:pointer}.as-picker__person:hover{background:var(--hm-hover)}.as-picker__person:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color)}.as-picker__person-text{display:flex;flex-direction:column;min-inline-size:0}.as-picker__person-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.as-picker__label-row{display:flex;align-items:center;gap:var(--hm-space-xs)}.as-picker__dot{flex:none;inline-size:.625rem;block-size:.625rem;border-radius:50%}\n"] }]
441
+ }], propDecorators: { labels: [{ type: i0.Input, args: [{ isSignal: true, alias: "labels", required: false }] }], selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }], toggled: [{ type: i0.Output, args: ["toggled"] }], created: [{ type: i0.Output, args: ["created"] }] } });
442
+
443
+ const EVENTS_PAGE = 20;
444
+ /** Copia para a área de transferência; false se o navegador recusar. */
445
+ async function copyText(value) {
446
+ try {
447
+ await navigator.clipboard.writeText(value);
448
+ return true;
449
+ }
450
+ catch {
451
+ return false;
452
+ }
453
+ }
454
+ /**
455
+ * A tarefa aberta (modal grande, duas colunas, como os quadros de tarefas conhecidos), montada pelo quadro.
456
+ *
457
+ * - **Esquerda**: título editável no lugar (Enter salva, Esc desiste), descrição em `hm-rich-text` (ler; "Editar"
458
+ * abre o editor e "Salvar" grava o HTML, que o servidor saneia), checklist (marcar, renomear, subir/descer, remover,
459
+ * "Mostrar no cartão") e as abas Comentários e Atividades (do mais novo, "Carregar mais"). Comentário é texto simples
460
+ * e pode ser interno (só a equipe vê; quem pediu não lê em "Minhas sugestões").
461
+ * - **Direita**: status, tipo, responsáveis ("Atribuir a mim") e envolvidos (busca de pessoas do realm, com foto),
462
+ * relator, prioridade, início e conclusão, rótulos (catálogo do app; cria na hora), capa, tela de origem ("Abrir a
463
+ * tela" quando o app administrado é o desta sessão), apoios, criada/atualizada e "Última alteração há X por Fulano";
464
+ * a chave com copiar; arquivar/restaurar e excluir (com confirmação).
465
+ * - **Agrupar**: "Relatores" (o autor e quem veio das agrupadas, com foto) no lugar do relator; "Agrupadas aqui" (chave,
466
+ * título e autor de cada origem, com link e "Desagrupar"); "Agrupar em…" pede o seletor ao quadro (`mergeRequested`).
467
+ * Na agrupada, o aviso "Agrupada em SUG-12" no topo, com "Abrir SUG-12" (`openTask`) e "Desagrupar" (`unmerged`).
468
+ * - **Nova tarefa** (`task` nulo, `draft`): título, tipo, coluna, prioridade, datas, descrição, rótulos e "Atribuir a
469
+ * mim"; criar troca para a tarefa completa.
470
+ *
471
+ * Toda mudança vai direto ao servidor, que devolve a tarefa inteira: ela vira a nova verdade aqui (`taskChange`) e no
472
+ * quadro (`changed`).
473
+ */
474
+ class SuggestionTaskDetailComponent {
475
+ constructor() {
476
+ this.messages = inject(SPINE_MESSAGES);
477
+ this.text = this.messages.suggestions;
478
+ this.service = inject(AutoServiceSuggestionsService);
479
+ this.toast = inject(HermesToastService);
480
+ this.dialogs = inject(HermesDialogService);
481
+ this.preferences = inject(SpinePreferencesService);
482
+ this.userStorage = inject(SpineUserStorage, { optional: true });
483
+ this.router = inject(Router, { optional: true });
484
+ this.location = inject(Location, { optional: true });
485
+ this.clientId = input.required(...(ngDevMode ? [{ debugName: "clientId" }] : /* istanbul ignore next */ []));
486
+ /** A tarefa aberta (leve, do quadro: a completa chega do servidor). null + `draft` = nova tarefa. */
487
+ this.task = input(null, ...(ngDevMode ? [{ debugName: "task" }] : /* istanbul ignore next */ []));
488
+ this.draft = input(false, ...(ngDevMode ? [{ debugName: "draft" }] : /* istanbul ignore next */ []));
489
+ /** Coluna da nova tarefa. */
490
+ this.draftStatus = input('em_analise', ...(ngDevMode ? [{ debugName: "draftStatus" }] : /* istanbul ignore next */ []));
491
+ this.labels = input([], ...(ngDevMode ? [{ debugName: "labels" }] : /* istanbul ignore next */ []));
492
+ /** A tela da sugestão abre daqui (o app administrado é o desta sessão). */
493
+ this.canOpenScreen = input(false, ...(ngDevMode ? [{ debugName: "canOpenScreen" }] : /* istanbul ignore next */ []));
494
+ /** A tarefa mudou (o servidor devolveu a nova versão). */
495
+ this.changed = output();
496
+ this.created = output();
497
+ this.removed = output();
498
+ this.closed = output();
499
+ /** Um rótulo novo entrou no catálogo. */
500
+ this.labelCreated = output();
501
+ /** "Agrupar em…": o quadro fecha este modal e abre o seletor. */
502
+ this.mergeRequested = output();
503
+ /** Abrir outra tarefa pelo id (o destino de uma agrupada, uma das agrupadas). */
504
+ this.openTask = output();
505
+ /** Desagrupou (daqui): o quadro relê, a origem volta para ele. */
506
+ this.unmerged = output();
507
+ this.statuses = SPINE_SUGGESTION_STATUSES;
508
+ this.statusColors = SPINE_SUGGESTION_STATUS_COLORS;
509
+ this.kinds = SPINE_SUGGESTION_KINDS;
510
+ this.kindColors = SPINE_SUGGESTION_KIND_COLORS;
511
+ this.priorities = TASK_PRIORITIES;
512
+ this.priorityIcons = TASK_PRIORITY_ICONS;
513
+ this.limits = SPINE_SUGGESTION_LIMITS;
514
+ this.today = todayIso(this.preferences.effectiveTimeZone());
515
+ this.current = signal(null, ...(ngDevMode ? [{ debugName: "current" }] : /* istanbul ignore next */ []));
516
+ this.loadError = signal('', ...(ngDevMode ? [{ debugName: "loadError" }] : /* istanbul ignore next */ []));
517
+ this.busy = signal(false, ...(ngDevMode ? [{ debugName: "busy" }] : /* istanbul ignore next */ []));
518
+ this.open = computed(() => !!this.current() || this.draft(), ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
519
+ this.heading = computed(() => {
520
+ const t = this.current();
521
+ return t ? `${t.key} · ${t.title}` : this.text.newTaskHeading;
522
+ }, ...(ngDevMode ? [{ debugName: "heading" }] : /* istanbul ignore next */ []));
523
+ // título e descrição
524
+ this.editingTitle = signal(false, ...(ngDevMode ? [{ debugName: "editingTitle" }] : /* istanbul ignore next */ []));
525
+ this.titleDraft = signal('', ...(ngDevMode ? [{ debugName: "titleDraft" }] : /* istanbul ignore next */ []));
526
+ this.editingDescription = signal(false, ...(ngDevMode ? [{ debugName: "editingDescription" }] : /* istanbul ignore next */ []));
527
+ this.descriptionDraft = signal('', ...(ngDevMode ? [{ debugName: "descriptionDraft" }] : /* istanbul ignore next */ []));
528
+ // checklist
529
+ this.newItem = signal('', ...(ngDevMode ? [{ debugName: "newItem" }] : /* istanbul ignore next */ []));
530
+ this.renaming = signal(null, ...(ngDevMode ? [{ debugName: "renaming" }] : /* istanbul ignore next */ []));
531
+ this.renameDraft = signal('', ...(ngDevMode ? [{ debugName: "renameDraft" }] : /* istanbul ignore next */ []));
532
+ // comentários e atividades
533
+ this.tab = signal('comments', ...(ngDevMode ? [{ debugName: "tab" }] : /* istanbul ignore next */ []));
534
+ this.comments = signal([], ...(ngDevMode ? [{ debugName: "comments" }] : /* istanbul ignore next */ []));
535
+ this.commentsTotal = signal(0, ...(ngDevMode ? [{ debugName: "commentsTotal" }] : /* istanbul ignore next */ []));
536
+ this.activity = signal([], ...(ngDevMode ? [{ debugName: "activity" }] : /* istanbul ignore next */ []));
537
+ this.activityTotal = signal(0, ...(ngDevMode ? [{ debugName: "activityTotal" }] : /* istanbul ignore next */ []));
538
+ this.eventsLoading = signal(false, ...(ngDevMode ? [{ debugName: "eventsLoading" }] : /* istanbul ignore next */ []));
539
+ this.comment = signal('', ...(ngDevMode ? [{ debugName: "comment" }] : /* istanbul ignore next */ []));
540
+ this.commentInternal = signal(false, ...(ngDevMode ? [{ debugName: "commentInternal" }] : /* istanbul ignore next */ []));
541
+ this.commentError = signal('', ...(ngDevMode ? [{ debugName: "commentError" }] : /* istanbul ignore next */ []));
542
+ this.commenting = signal(false, ...(ngDevMode ? [{ debugName: "commenting" }] : /* istanbul ignore next */ []));
543
+ // nova tarefa
544
+ this.draftTitle = signal('', ...(ngDevMode ? [{ debugName: "draftTitle" }] : /* istanbul ignore next */ []));
545
+ this.draftTitleError = signal('', ...(ngDevMode ? [{ debugName: "draftTitleError" }] : /* istanbul ignore next */ []));
546
+ this.draftKind = signal('melhoria', ...(ngDevMode ? [{ debugName: "draftKind" }] : /* istanbul ignore next */ []));
547
+ this.draftColumn = signal('em_analise', ...(ngDevMode ? [{ debugName: "draftColumn" }] : /* istanbul ignore next */ []));
548
+ this.draftPriority = signal('media', ...(ngDevMode ? [{ debugName: "draftPriority" }] : /* istanbul ignore next */ []));
549
+ this.draftStart = signal(null, ...(ngDevMode ? [{ debugName: "draftStart" }] : /* istanbul ignore next */ []));
550
+ this.draftDue = signal(null, ...(ngDevMode ? [{ debugName: "draftDue" }] : /* istanbul ignore next */ []));
551
+ this.draftDescription = signal('', ...(ngDevMode ? [{ debugName: "draftDescription" }] : /* istanbul ignore next */ []));
552
+ this.draftLabels = signal([], ...(ngDevMode ? [{ debugName: "draftLabels" }] : /* istanbul ignore next */ []));
553
+ this.draftAssignMe = signal(true, ...(ngDevMode ? [{ debugName: "draftAssignMe" }] : /* istanbul ignore next */ []));
554
+ this.selectedLabels = computed(() => this.current()?.labels.map((l) => l.id) ?? this.draftLabels(), ...(ngDevMode ? [{ debugName: "selectedLabels" }] : /* istanbul ignore next */ []));
555
+ this.assigneeIds = computed(() => this.current()?.assignees.map((p) => p.user_id) ?? [], ...(ngDevMode ? [{ debugName: "assigneeIds" }] : /* istanbul ignore next */ []));
556
+ this.watcherIds = computed(() => this.current()?.watchers.map((p) => p.user_id) ?? [], ...(ngDevMode ? [{ debugName: "watcherIds" }] : /* istanbul ignore next */ []));
557
+ this.checklistItems = computed(() => [...(this.current()?.checklist.items ?? [])].sort((a, b) => a.position - b.position), ...(ngDevMode ? [{ debugName: "checklistItems" }] : /* istanbul ignore next */ []));
558
+ this.dueSituation = computed(() => {
559
+ const t = this.current();
560
+ return t ? dueState(t, this.today) : null;
561
+ }, ...(ngDevMode ? [{ debugName: "dueSituation" }] : /* istanbul ignore next */ []));
562
+ this.isMine = computed(() => {
563
+ const me = this.me();
564
+ return !!me && this.assigneeIds().includes(me);
565
+ }, ...(ngDevMode ? [{ debugName: "isMine" }] : /* istanbul ignore next */ []));
566
+ effect(() => {
567
+ const task = this.task();
568
+ const draft = this.draft();
569
+ untracked(() => {
570
+ this.reset();
571
+ if (task)
572
+ void this.load(task);
573
+ else if (draft)
574
+ this.startDraft();
575
+ });
576
+ });
577
+ }
578
+ me() {
579
+ return this.userStorage?.userId() ?? '';
580
+ }
581
+ reset() {
582
+ this.current.set(null);
583
+ this.loadError.set('');
584
+ this.editingTitle.set(false);
585
+ this.editingDescription.set(false);
586
+ this.renaming.set(null);
587
+ this.newItem.set('');
588
+ this.comments.set([]);
589
+ this.activity.set([]);
590
+ this.commentsTotal.set(0);
591
+ this.activityTotal.set(0);
592
+ this.comment.set('');
593
+ this.commentInternal.set(false);
594
+ this.commentError.set('');
595
+ this.tab.set('comments');
596
+ }
597
+ startDraft() {
598
+ this.draftTitle.set('');
599
+ this.draftTitleError.set('');
600
+ this.draftKind.set('melhoria');
601
+ this.draftColumn.set(this.draftStatus());
602
+ this.draftPriority.set('media');
603
+ this.draftStart.set(null);
604
+ this.draftDue.set(null);
605
+ this.draftDescription.set('');
606
+ this.draftLabels.set([]);
607
+ this.draftAssignMe.set(true);
608
+ }
609
+ async load(light) {
610
+ this.current.set(light);
611
+ try {
612
+ const full = await this.service.task(this.clientId(), light.id);
613
+ if (this.task()?.id !== light.id)
614
+ return;
615
+ this.current.set({ ...full, author: full.author ?? light.author });
616
+ void this.loadEvents('comments', true);
617
+ }
618
+ catch {
619
+ this.loadError.set(this.text.loadError);
620
+ }
621
+ }
622
+ setOpen(open) {
623
+ if (!open)
624
+ this.closed.emit();
625
+ }
626
+ /** Aplica a versão do servidor aqui e avisa o quadro. */
627
+ apply(task) {
628
+ const prev = this.current();
629
+ const next = { ...task, author: task.author ?? prev?.author ?? null };
630
+ this.current.set(next);
631
+ this.changed.emit(next);
632
+ }
633
+ /** Roda uma mudança no servidor; erro = aviso no canto. */
634
+ async run(call, success) {
635
+ if (this.busy())
636
+ return false;
637
+ this.busy.set(true);
638
+ try {
639
+ this.apply(await call());
640
+ if (success)
641
+ this.toast.success(success);
642
+ if (this.tab() === 'activity')
643
+ void this.loadEvents('activity', true);
644
+ return true;
645
+ }
646
+ catch {
647
+ this.toast.error(this.text.saveError);
648
+ return false;
649
+ }
650
+ finally {
651
+ this.busy.set(false);
652
+ }
653
+ }
654
+ edit(patch) {
655
+ const t = this.current();
656
+ return t ? this.run(() => this.service.update(this.clientId(), t.id, patch)) : Promise.resolve(false);
657
+ }
658
+ // --- título e descrição ---
659
+ startTitle() {
660
+ this.titleDraft.set(this.current()?.title ?? '');
661
+ this.editingTitle.set(true);
662
+ }
663
+ async saveTitle() {
664
+ const title = this.titleDraft().trim();
665
+ if (!title || title === this.current()?.title) {
666
+ this.editingTitle.set(false);
667
+ return;
668
+ }
669
+ if (await this.edit({ title }))
670
+ this.editingTitle.set(false);
671
+ }
672
+ onTitleKey(event) {
673
+ if (event.key === 'Enter') {
674
+ event.preventDefault();
675
+ void this.saveTitle();
676
+ }
677
+ else if (event.key === 'Escape') {
678
+ event.stopPropagation();
679
+ this.editingTitle.set(false);
680
+ }
681
+ }
682
+ startDescription() {
683
+ this.descriptionDraft.set(this.current()?.description_html ?? '');
684
+ this.editingDescription.set(true);
685
+ }
686
+ async saveDescription() {
687
+ if (await this.edit({ description_html: this.descriptionDraft() }))
688
+ this.editingDescription.set(false);
689
+ }
690
+ // --- campos da direita ---
691
+ changeStatus(status) {
692
+ const t = this.current();
693
+ if (!t || status === t.status)
694
+ return;
695
+ void this.run(() => this.service.move(this.clientId(), t.id, { status: status }), this.text.statusSaved);
696
+ }
697
+ changeKind(kind) {
698
+ if (kind !== this.current()?.kind)
699
+ void this.edit({ kind: kind });
700
+ }
701
+ changePriority(priority) {
702
+ if (priority !== this.current()?.priority)
703
+ void this.edit({ priority: priority });
704
+ }
705
+ changeDate(field, value) {
706
+ const t = this.current();
707
+ if (!t || (value || null) === t[field])
708
+ return;
709
+ void this.edit({ [field]: value || null });
710
+ }
711
+ changeCover(cover, popover) {
712
+ popover?.close();
713
+ if (cover !== this.current()?.cover)
714
+ void this.edit({ cover });
715
+ }
716
+ toggleChecklistOnCard(on) {
717
+ if (on !== this.current()?.checklist_on_card)
718
+ void this.edit({ checklist_on_card: on });
719
+ }
720
+ assignMe() {
721
+ const t = this.current();
722
+ if (t)
723
+ void this.run(() => this.service.addPerson(this.clientId(), t.id, 'me', 'responsavel'), this.text.assignedToMe);
724
+ }
725
+ addPerson(person, role, popover) {
726
+ popover.close();
727
+ const t = this.current();
728
+ if (t)
729
+ void this.run(() => this.service.addPerson(this.clientId(), t.id, person.id, role));
730
+ }
731
+ removePerson(person, role) {
732
+ const t = this.current();
733
+ if (t)
734
+ void this.run(() => this.service.removePerson(this.clientId(), t.id, person.user_id, role));
735
+ }
736
+ toggleLabel(label, linked) {
737
+ const t = this.current();
738
+ if (!t) {
739
+ this.draftLabels.update((ids) => (linked ? [...new Set([...ids, label.id])] : ids.filter((id) => id !== label.id)));
740
+ return;
741
+ }
742
+ void this.run(() => this.service.setLabel(this.clientId(), t.id, label.id, linked));
743
+ }
744
+ async createLabel(name, color) {
745
+ try {
746
+ const label = await this.service.createLabel(this.clientId(), name, color);
747
+ const entry = { ...label, uses: 0 };
748
+ this.labelCreated.emit(entry);
749
+ this.toggleLabel(entry, true);
750
+ }
751
+ catch {
752
+ this.toast.error(this.text.saveError);
753
+ }
754
+ }
755
+ async copyKey() {
756
+ const t = this.current();
757
+ if (!t)
758
+ return;
759
+ if (await copyText(t.key))
760
+ this.toast.success(this.text.keyCopied);
761
+ else
762
+ this.toast.error(this.text.copyError);
763
+ }
764
+ screenHref(path) {
765
+ return this.location?.prepareExternalUrl(path) ?? path;
766
+ }
767
+ /** Clique simples navega no app (e fecha); com Ctrl/Cmd/Shift ou botão do meio, o navegador abre a aba. */
768
+ openScreen(event, path) {
769
+ if (!this.router || event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey)
770
+ return;
771
+ event.preventDefault();
772
+ this.closed.emit();
773
+ void this.router.navigateByUrl(path);
774
+ }
775
+ toggleArchived() {
776
+ const t = this.current();
777
+ if (!t)
778
+ return;
779
+ const archive = !t.archived_at;
780
+ void this.run(() => this.service.setArchived(this.clientId(), t.id, archive), archive ? this.text.archivedToast : this.text.restoredToast);
781
+ }
782
+ async remove() {
783
+ const t = this.current();
784
+ if (!t)
785
+ return;
786
+ const ok = await this.dialogs.confirm({
787
+ heading: formatMessage(this.text.deleteHeading, { key: t.key }), message: this.text.deleteMessage,
788
+ confirmLabel: this.text.deleteConfirm, danger: true,
789
+ });
790
+ if (!ok)
791
+ return;
792
+ try {
793
+ await this.service.remove(this.clientId(), t.id);
794
+ this.toast.success(this.text.deleted);
795
+ this.removed.emit(t);
796
+ }
797
+ catch {
798
+ this.toast.error(this.text.saveError);
799
+ }
800
+ }
801
+ // --- agrupar ---
802
+ /** Desagrupa esta (a agrupada): ela volta ao quadro. */
803
+ async unmergeSelf() {
804
+ const t = this.current();
805
+ if (!t?.merged_into || this.busy())
806
+ return;
807
+ if (await this.run(() => this.service.unmerge(this.clientId(), t.id), formatMessage(this.text.unmergedToast, { key: t.key }))) {
808
+ this.unmerged.emit();
809
+ }
810
+ }
811
+ /** Desagrupa uma das agrupadas nesta: relê esta (perde os relatores e apoios que vieram dela). */
812
+ async unmergeOrigin(origin) {
813
+ const t = this.current();
814
+ if (!t)
815
+ return;
816
+ const ok = await this.run(async () => {
817
+ await this.service.unmerge(this.clientId(), origin.id);
818
+ return this.service.task(this.clientId(), t.id);
819
+ }, formatMessage(this.text.unmergedToast, { key: origin.key }));
820
+ if (ok)
821
+ this.unmerged.emit();
822
+ }
823
+ /** Quem pediu: o autor e os relatores extras (do detalhe); sem a lista ainda, só o autor. */
824
+ reportersOf(t) {
825
+ if (t.reporters?.length)
826
+ return t.reporters;
827
+ return [{ ...(t.author ?? { user_id: '', username: null, name: null, picture: null }), role: 'author', from_suggestion_id: null }];
828
+ }
829
+ /** "Autor" ou "via SUG-30" (a agrupada de onde o relator veio). */
830
+ reporterHint(t, r) {
831
+ if (r.role === 'author')
832
+ return this.text.reporterAuthor;
833
+ const origin = t.merged?.find((m) => m.id === r.from_suggestion_id);
834
+ return origin ? formatMessage(this.text.reporterVia, { key: origin.key }) : '';
835
+ }
836
+ mergedIntoText(key) {
837
+ return formatMessage(this.text.mergedInto, { key });
838
+ }
839
+ openMergedText(key) {
840
+ return formatMessage(this.text.openMerged, { key });
841
+ }
842
+ unmergeLabel(key) {
843
+ return formatMessage(this.text.unmergeLabel, { key });
844
+ }
845
+ // --- checklist ---
846
+ addItem() {
847
+ const t = this.current();
848
+ const text = this.newItem().trim();
849
+ if (!t || !text)
850
+ return;
851
+ void this.run(() => this.service.addChecklistItem(this.clientId(), t.id, text)).then((ok) => ok && this.newItem.set(''));
852
+ }
853
+ onNewItemKey(event) {
854
+ if (event.key === 'Enter') {
855
+ event.preventDefault();
856
+ this.addItem();
857
+ }
858
+ }
859
+ toggleItem(item, done) {
860
+ const t = this.current();
861
+ if (t && done !== item.done)
862
+ void this.run(() => this.service.updateChecklistItem(this.clientId(), t.id, item.id, { done }));
863
+ }
864
+ startRename(item) {
865
+ this.renaming.set(item.id);
866
+ this.renameDraft.set(item.text);
867
+ }
868
+ saveRename(item) {
869
+ const text = this.renameDraft().trim();
870
+ this.renaming.set(null);
871
+ const t = this.current();
872
+ if (t && text && text !== item.text)
873
+ void this.run(() => this.service.updateChecklistItem(this.clientId(), t.id, item.id, { text }));
874
+ }
875
+ onRenameKey(event, item) {
876
+ if (event.key === 'Enter') {
877
+ event.preventDefault();
878
+ this.saveRename(item);
879
+ }
880
+ else if (event.key === 'Escape') {
881
+ event.stopPropagation();
882
+ this.renaming.set(null);
883
+ }
884
+ }
885
+ moveItem(item, delta) {
886
+ const t = this.current();
887
+ const index = this.checklistItems().findIndex((i) => i.id === item.id);
888
+ const target = index + delta;
889
+ if (!t || index < 0 || target < 0 || target >= this.checklistItems().length)
890
+ return;
891
+ void this.run(() => this.service.updateChecklistItem(this.clientId(), t.id, item.id, { position: target }));
892
+ }
893
+ removeItem(item) {
894
+ const t = this.current();
895
+ if (t)
896
+ void this.run(() => this.service.removeChecklistItem(this.clientId(), t.id, item.id));
897
+ }
898
+ // --- comentários e atividades ---
899
+ selectTab(tab) {
900
+ const next = tab === 'activity' ? 'activity' : 'comments';
901
+ this.tab.set(next);
902
+ if ((next === 'activity' ? this.activity() : this.comments()).length === 0)
903
+ void this.loadEvents(next, true);
904
+ }
905
+ async loadEvents(type, reset) {
906
+ const t = this.current();
907
+ if (!t)
908
+ return;
909
+ const list = type === 'comments' ? this.comments : this.activity;
910
+ const total = type === 'comments' ? this.commentsTotal : this.activityTotal;
911
+ this.eventsLoading.set(true);
912
+ try {
913
+ const res = await this.service.events(this.clientId(), t.id, { type, limit: EVENTS_PAGE, offset: reset ? 0 : list().length });
914
+ if (this.current()?.id !== t.id)
915
+ return;
916
+ list.set(reset ? res.events : [...list(), ...res.events]);
917
+ total.set(res.total);
918
+ }
919
+ catch {
920
+ this.loadError.set(this.text.loadError);
921
+ }
922
+ finally {
923
+ this.eventsLoading.set(false);
924
+ }
925
+ }
926
+ async sendComment() {
927
+ const t = this.current();
928
+ const comment = this.comment().trim();
929
+ if (!t || this.commenting())
930
+ return;
931
+ this.commentError.set(comment ? '' : this.text.commentRequired);
932
+ if (!comment)
933
+ return;
934
+ this.commenting.set(true);
935
+ try {
936
+ const event = await this.service.comment(this.clientId(), t.id, comment, this.commentInternal());
937
+ this.comments.update((list) => [event, ...list]);
938
+ this.commentsTotal.update((n) => n + 1);
939
+ this.comment.set('');
940
+ this.commentInternal.set(false);
941
+ const updated = {
942
+ ...t, comments_count: t.comments_count + 1,
943
+ last_activity: { at: event.created_at, actor: event.actor },
944
+ ...(event.internal ? {} : { last_comment: { comment: event.comment ?? comment, created_at: event.created_at } }),
945
+ };
946
+ this.current.set(updated);
947
+ this.changed.emit(updated);
948
+ this.toast.success(this.text.commentSent);
949
+ }
950
+ catch {
951
+ this.commentError.set(this.text.commentError);
952
+ }
953
+ finally {
954
+ this.commenting.set(false);
955
+ }
956
+ }
957
+ // --- nova tarefa ---
958
+ async create() {
959
+ const title = this.draftTitle().trim();
960
+ this.draftTitleError.set(title ? '' : this.text.titleRequired);
961
+ if (!title || this.busy())
962
+ return;
963
+ this.busy.set(true);
964
+ try {
965
+ const task = await this.service.create(this.clientId(), {
966
+ title, kind: this.draftKind(), status: this.draftColumn(),
967
+ priority: this.draftPriority(), start_date: this.draftStart(), due_date: this.draftDue(),
968
+ description_html: this.draftDescription(), label_ids: this.draftLabels(),
969
+ people: this.draftAssignMe() ? [{ user_id: 'me', role: 'responsavel' }] : [],
970
+ });
971
+ this.toast.success(this.text.taskCreated);
972
+ this.created.emit(task);
973
+ }
974
+ catch {
975
+ this.toast.error(this.text.createError);
976
+ }
977
+ finally {
978
+ this.busy.set(false);
979
+ }
980
+ }
981
+ // --- textos ---
982
+ personName(p) {
983
+ return p?.name || p?.username || this.text.removedAuthor;
984
+ }
985
+ statusLabel(status) {
986
+ if (!status)
987
+ return '—';
988
+ return this.text.statuses[status] ?? this.text.legacyStatuses[status] ?? status;
989
+ }
990
+ /** O valor de um evento 'change' legível: prioridade e tipo pelo catálogo, data no formato local. */
991
+ valueText(field, value) {
992
+ if (!value)
993
+ return '—';
994
+ if (field === 'priority')
995
+ return this.text.priorities[value] ?? value;
996
+ if (field === 'kind')
997
+ return this.text.kinds[value] ?? value;
998
+ if (field === 'start_date' || field === 'due_date')
999
+ return this.day(value);
1000
+ return value;
1001
+ }
1002
+ eventText(e) {
1003
+ const actor = e.actor ? this.personName(e.actor) : this.text.someone;
1004
+ if (e.kind === 'comment')
1005
+ return formatMessage(this.text.eventComment, { actor });
1006
+ if (e.kind === 'status')
1007
+ return formatMessage(this.text.eventStatus, { actor, from: this.statusLabel(e.from_status), to: this.statusLabel(e.to_status) });
1008
+ const field = e.field ?? 'other';
1009
+ const removed = ['start_date', 'due_date', 'cover'].includes(field) && !e.to_value ? `${field}_removed` : field;
1010
+ const template = this.text.activity[removed] ?? this.text.activity.other;
1011
+ return formatMessage(template, { actor, from: this.valueText(e.field, e.from_value), to: this.valueText(e.field, e.to_value) });
1012
+ }
1013
+ lastChange(t) {
1014
+ if (!t.last_activity)
1015
+ return '';
1016
+ const when = this.age(t.last_activity.at);
1017
+ return t.last_activity.actor
1018
+ ? formatMessage(this.text.lastChange, { when, actor: this.personName(t.last_activity.actor) })
1019
+ : formatMessage(this.text.lastChangeNoActor, { when });
1020
+ }
1021
+ dueText(t) {
1022
+ if (!t.due_date)
1023
+ return '';
1024
+ const s = this.dueSituation();
1025
+ if (s === 'overdue')
1026
+ return formatMessage(this.text.dueLate, { date: this.day(t.due_date) });
1027
+ if (s === 'today')
1028
+ return this.text.dueToday;
1029
+ if (s === 'tomorrow')
1030
+ return this.text.dueTomorrow;
1031
+ return formatMessage(this.text.dueOn, { date: this.day(t.due_date) });
1032
+ }
1033
+ removePersonLabel(p) {
1034
+ return formatMessage(this.text.removePerson, { name: this.personName(p) });
1035
+ }
1036
+ itemLabel(template, item) {
1037
+ return formatMessage(template, { text: item.text });
1038
+ }
1039
+ age(iso) {
1040
+ const seconds = Math.round((Date.now() - new Date(iso).getTime()) / 1000);
1041
+ const units = [['year', 31_536_000], ['month', 2_592_000], ['week', 604_800], ['day', 86_400], ['hour', 3_600], ['minute', 60]];
1042
+ try {
1043
+ const rtf = new Intl.RelativeTimeFormat(this.messages.meta.locale, { numeric: 'auto' });
1044
+ for (const [unit, size] of units)
1045
+ if (Math.abs(seconds) >= size)
1046
+ return rtf.format(-Math.floor(seconds / size), unit);
1047
+ return rtf.format(0, 'minute');
1048
+ }
1049
+ catch {
1050
+ return this.date(iso);
1051
+ }
1052
+ }
1053
+ date(iso, withTime = false) {
1054
+ try {
1055
+ return new Intl.DateTimeFormat(this.messages.meta.locale, {
1056
+ dateStyle: 'short', ...(withTime ? { timeStyle: 'short' } : {}), timeZone: this.preferences.effectiveTimeZone(),
1057
+ }).format(new Date(iso));
1058
+ }
1059
+ catch {
1060
+ return iso.slice(0, 10);
1061
+ }
1062
+ }
1063
+ /** Data sem hora (AAAA-MM-DD), sem fuso: o dia é o dia. */
1064
+ day(iso) {
1065
+ try {
1066
+ return new Intl.DateTimeFormat(this.messages.meta.locale, { dateStyle: 'short', timeZone: 'UTC' }).format(new Date(`${iso}T00:00:00Z`));
1067
+ }
1068
+ catch {
1069
+ return iso;
1070
+ }
1071
+ }
1072
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionTaskDetailComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1073
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: SuggestionTaskDetailComponent, isStandalone: true, selector: "spine-suggestion-task-detail", inputs: { clientId: { classPropertyName: "clientId", publicName: "clientId", isSignal: true, isRequired: true, transformFunction: null }, task: { classPropertyName: "task", publicName: "task", isSignal: true, isRequired: false, transformFunction: null }, draft: { classPropertyName: "draft", publicName: "draft", isSignal: true, isRequired: false, transformFunction: null }, draftStatus: { classPropertyName: "draftStatus", publicName: "draftStatus", isSignal: true, isRequired: false, transformFunction: null }, labels: { classPropertyName: "labels", publicName: "labels", isSignal: true, isRequired: false, transformFunction: null }, canOpenScreen: { classPropertyName: "canOpenScreen", publicName: "canOpenScreen", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { changed: "changed", created: "created", removed: "removed", closed: "closed", labelCreated: "labelCreated", mergeRequested: "mergeRequested", openTask: "openTask", unmerged: "unmerged" }, ngImport: i0, template: "<hm-modal [heading]=\"heading()\" [open]=\"open()\" (openChange)=\"setOpen($event)\" size=\"xlarge\">\n @if (current(); as t) {\n @if (t.cover) {\n <div class=\"as-task__cover\" [class]=\"'as-tok--' + t.cover\" aria-hidden=\"true\"></div>\n }\n @if (loadError()) {\n <hm-alert variant=\"error\">{{ loadError() }}</hm-alert>\n }\n <div class=\"as-task\">\n <!-- Esquerda: o trabalho (t\u00EDtulo, descri\u00E7\u00E3o, checklist, conversa). -->\n <div class=\"as-task__main\">\n <div class=\"as-task__key-row\">\n <span class=\"as-task__key\">{{ t.key }}</span>\n <hm-icon-button size=\"small\" emphasis=\"subtle\" icon=\"copy\" [label]=\"text.copyKey\" (clicked)=\"copyKey()\" />\n <hm-tag [name]=\"text.kinds[t.kind]\" [color]=\"kindColors[t.kind]\" />\n @if (t.archived_at) { <hm-tag [name]=\"text.archivedTag\" /> }\n @if (t.merged_into) { <hm-tag [name]=\"text.mergedTag\" color=\"info\" /> }\n </div>\n\n <!-- Agrupada: o destino tem os relatores e os apoios; os coment\u00E1rios ficam aqui. -->\n @if (t.merged_into; as into) {\n <div class=\"as-task__merged\">\n <hm-icon class=\"as-task__merged-icon\" shape=\"layers\" />\n <div class=\"as-task__merged-text\">\n <strong>{{ mergedIntoText(into.key) }}</strong>\n <span>{{ into.title }}</span>\n <span class=\"as-field__hint\">{{ text.mergedIntoHint }}</span>\n </div>\n <div class=\"as-actions\">\n <hm-button size=\"small\" emphasis=\"medium\" (clicked)=\"openTask.emit(into.id)\">{{ openMergedText(into.key) }}</hm-button>\n <hm-button size=\"small\" emphasis=\"minimal\" icon=\"unlink\" [disabled]=\"busy()\" (clicked)=\"unmergeSelf()\">{{ text.unmerge }}</hm-button>\n </div>\n </div>\n }\n\n @if (editingTitle()) {\n <hm-input [label]=\"text.title\" labelHidden [maxLength]=\"limits.title\" [value]=\"titleDraft()\"\n (valueChange)=\"titleDraft.set($event)\" (keydown)=\"onTitleKey($event)\" (focusout)=\"saveTitle()\" />\n } @else {\n <h2 class=\"as-task__title\">\n <button type=\"button\" class=\"as-task__title-button\" [attr.aria-label]=\"text.editTitle + ': ' + t.title\" (click)=\"startTitle()\">\n {{ t.title }}<hm-icon class=\"as-task__title-pen\" shape=\"pencil\" />\n </button>\n </h2>\n }\n\n <section class=\"as-task__section\" aria-labelledby=\"as-task-description\">\n <div class=\"as-task__section-head\">\n <h3 id=\"as-task-description\" class=\"as-task__heading\">{{ text.description }}</h3>\n @if (!editingDescription()) {\n <hm-button size=\"small\" emphasis=\"minimal\" icon=\"pencil\" (clicked)=\"startDescription()\">{{ text.editDescription }}</hm-button>\n }\n </div>\n @if (editingDescription()) {\n <hm-rich-text [label]=\"text.description\" labelHidden [rows]=\"6\" [maxRows]=\"20\" [value]=\"descriptionDraft()\"\n (valueChange)=\"descriptionDraft.set($event)\" />\n <div class=\"as-actions\">\n <hm-button emphasis=\"medium\" (clicked)=\"editingDescription.set(false)\">{{ text.cancel }}</hm-button>\n <hm-button [loading]=\"busy()\" (clicked)=\"saveDescription()\">{{ text.save }}</hm-button>\n </div>\n } @else if (t.description_html) {\n <hm-rich-text class=\"as-task__description\" [label]=\"text.description\" labelHidden readonly hideToolbar [value]=\"t.description_html\" />\n } @else {\n <p class=\"muted\">{{ text.noDescription }}</p>\n }\n </section>\n\n <section class=\"as-task__section\" aria-labelledby=\"as-task-checklist\">\n <div class=\"as-task__section-head\">\n <h3 id=\"as-task-checklist\" class=\"as-task__heading\">\n {{ text.checklist }}\n @if (t.checklist.total) { <span class=\"as-task__count\">{{ t.checklist.done }}/{{ t.checklist.total }}</span> }\n </h3>\n <hm-switch [label]=\"text.checklistOnCard\" [value]=\"t.checklist_on_card\" (valueChange)=\"toggleChecklistOnCard($event)\" />\n </div>\n @if (t.checklist.total) {\n <div class=\"as-task__progress\" role=\"progressbar\" aria-valuemin=\"0\" [attr.aria-valuemax]=\"t.checklist.total\"\n [attr.aria-valuenow]=\"t.checklist.done\" [attr.aria-label]=\"text.checklist\">\n <span [style.inline-size.%]=\"(t.checklist.done / t.checklist.total) * 100\"></span>\n </div>\n }\n <ul class=\"as-checklist\">\n @for (item of checklistItems(); track item.id; let first = $first; let last = $last) {\n <li class=\"as-checklist__item\" [class.as-checklist__item--done]=\"item.done\">\n @if (renaming() === item.id) {\n <hm-input class=\"as-checklist__rename\" [label]=\"text.checklist\" labelHidden [maxLength]=\"500\" [value]=\"renameDraft()\"\n (valueChange)=\"renameDraft.set($event)\" (keydown)=\"onRenameKey($event, item)\" (focusout)=\"saveRename(item)\" />\n } @else {\n <hm-checkbox class=\"as-checklist__check\" [label]=\"item.text\" [checked]=\"item.done\" (checkedChange)=\"toggleItem(item, $event)\" />\n <span class=\"as-checklist__tools\">\n <hm-icon-button size=\"small\" emphasis=\"subtle\" icon=\"pencil\" [label]=\"text.editTitle + ': ' + item.text\" (clicked)=\"startRename(item)\" />\n <hm-icon-button size=\"small\" emphasis=\"subtle\" icon=\"angle-up\" [label]=\"itemLabel(text.checklistUp, item)\" [disabled]=\"first\" (clicked)=\"moveItem(item, -1)\" />\n <hm-icon-button size=\"small\" emphasis=\"subtle\" icon=\"angle-down\" [label]=\"itemLabel(text.checklistDown, item)\" [disabled]=\"last\" (clicked)=\"moveItem(item, 1)\" />\n <hm-icon-button size=\"small\" emphasis=\"subtle\" icon=\"trash\" [label]=\"itemLabel(text.checklistRemove, item)\" (clicked)=\"removeItem(item)\" />\n </span>\n }\n </li>\n } @empty {\n <li class=\"muted\">{{ text.checklistEmpty }}</li>\n }\n </ul>\n <div class=\"as-checklist__add\">\n <hm-input class=\"as-checklist__new\" [label]=\"text.checklistAdd\" labelHidden [placeholder]=\"text.checklistPlaceholder\"\n [maxLength]=\"500\" [value]=\"newItem()\" (valueChange)=\"newItem.set($event)\" (keydown)=\"onNewItemKey($event)\" />\n <hm-button emphasis=\"medium\" icon=\"plus\" [disabled]=\"!newItem().trim()\" (clicked)=\"addItem()\">{{ text.checklistAdd }}</hm-button>\n </div>\n </section>\n\n <hm-tabs [label]=\"text.taskLabel\" [active]=\"tab()\" (activeChange)=\"selectTab($event)\">\n <hm-tab id=\"comments\" [label]=\"text.tabComments\" [count]=\"commentsTotal() || null\">\n <div class=\"as-task__reply\">\n <hm-textarea [label]=\"text.comment\" [hint]=\"commentInternal() ? '' : text.commentHint\" [maxLength]=\"limits.description\"\n [rows]=\"2\" autoGrow [maxRows]=\"8\" [error]=\"commentError()\" [value]=\"comment()\" (valueChange)=\"comment.set($event)\" />\n <div class=\"as-task__reply-row\">\n <hm-checkbox [label]=\"text.commentInternal\" [checked]=\"commentInternal()\" (checkedChange)=\"commentInternal.set($event)\" />\n <hm-button icon=\"comment-message\" [loading]=\"commenting()\" (clicked)=\"sendComment()\">{{ text.commentSend }}</hm-button>\n </div>\n </div>\n <ol class=\"as-timeline\">\n @for (e of comments(); track e.id) {\n <li class=\"as-timeline__item\">\n <hm-account-avatar class=\"as-timeline__face\" size=\"small\" [name]=\"e.actor ? personName(e.actor) : text.someone\" [imageUrl]=\"e.actor?.picture\" />\n <div class=\"as-timeline__body\">\n <span class=\"as-timeline__line\">\n <strong>{{ e.actor ? personName(e.actor) : text.someone }}</strong> \u00B7\n <time class=\"muted\" [attr.datetime]=\"e.created_at\" [attr.title]=\"date(e.created_at, true)\">{{ age(e.created_at) }}</time>\n @if (e.kind === 'status') { \u00B7 <span class=\"muted\">{{ eventText(e) }}</span> }\n @if (e.internal) { <hm-tag [name]=\"text.internalTag\" color=\"warning\" /> }\n </span>\n <p class=\"as-timeline__comment\" [class.as-timeline__comment--internal]=\"e.internal\">{{ e.comment }}</p>\n </div>\n </li>\n } @empty {\n @if (!eventsLoading()) { <li class=\"muted\">{{ text.commentsEmpty }}</li> }\n }\n </ol>\n @if (comments().length < commentsTotal()) {\n <hm-button emphasis=\"medium\" [loading]=\"eventsLoading()\" (clicked)=\"loadEvents('comments', false)\">{{ text.loadMore }}</hm-button>\n }\n </hm-tab>\n <hm-tab id=\"activity\" [label]=\"text.tabActivity\">\n <ol class=\"as-timeline\">\n @for (e of activity(); track e.id) {\n <li class=\"as-timeline__item\">\n <hm-account-avatar class=\"as-timeline__face\" size=\"small\" [name]=\"e.actor ? personName(e.actor) : text.someone\" [imageUrl]=\"e.actor?.picture\" />\n <div class=\"as-timeline__body\">\n <span class=\"as-timeline__line\">{{ eventText(e) }} \u00B7\n <time class=\"muted\" [attr.datetime]=\"e.created_at\" [attr.title]=\"date(e.created_at, true)\">{{ age(e.created_at) }}</time>\n </span>\n @if (e.comment) {\n <p class=\"as-timeline__comment\" [class.as-timeline__comment--internal]=\"e.internal\">{{ e.comment }}</p>\n }\n </div>\n </li>\n } @empty {\n @if (!eventsLoading()) { <li class=\"muted\">{{ text.activityEmpty }}</li> }\n }\n </ol>\n @if (activity().length < activityTotal()) {\n <hm-button emphasis=\"medium\" [loading]=\"eventsLoading()\" (clicked)=\"loadEvents('activity', false)\">{{ text.loadMore }}</hm-button>\n }\n </hm-tab>\n </hm-tabs>\n </div>\n\n <!-- Direita: os campos da tarefa (cada mudan\u00E7a grava na hora). -->\n <aside class=\"as-task__side\" [attr.aria-label]=\"text.taskLabel\">\n <hm-select [label]=\"text.status\" [value]=\"t.status\" [disabled]=\"busy()\" (valueChange)=\"changeStatus($any($event))\">\n @for (st of statuses; track st) { <option [value]=\"st\">{{ text.statuses[st] }}</option> }\n </hm-select>\n\n <div class=\"as-field\">\n <div class=\"as-field__head\">\n <span class=\"as-field__label\">{{ text.assignees }}</span>\n @if (!isMine()) {\n <hm-button size=\"small\" emphasis=\"minimal\" icon=\"user-plus\" (clicked)=\"assignMe()\">{{ text.assignMe }}</hm-button>\n }\n </div>\n <ul class=\"as-people\">\n @for (p of t.assignees; track p.user_id) {\n <li class=\"as-people__item\">\n <hm-account-avatar size=\"small\" [name]=\"personName(p)\" [imageUrl]=\"p.picture\" />\n <span class=\"as-people__name\">{{ personName(p) }}</span>\n <hm-icon-button size=\"small\" emphasis=\"subtle\" icon=\"trash\" [label]=\"removePersonLabel(p)\" (clicked)=\"removePerson(p, 'responsavel')\" />\n </li>\n } @empty {\n <li class=\"muted\">{{ text.nobody }}</li>\n }\n </ul>\n <hm-button size=\"small\" emphasis=\"minimal\" icon=\"plus\" [hmPopoverTrigger]=\"assigneePop\">{{ text.addAssignee }}</hm-button>\n <hm-popover #assigneePop [label]=\"text.addAssignee\">\n @if (assigneePop.open()) {\n <spine-suggestion-people-picker [clientId]=\"clientId()\" [exclude]=\"assigneeIds()\" (picked)=\"addPerson($event, 'responsavel', assigneePop)\" />\n }\n </hm-popover>\n </div>\n\n <div class=\"as-field\">\n <span class=\"as-field__label\">{{ text.watchers }}</span>\n <ul class=\"as-people\">\n @for (p of t.watchers; track p.user_id) {\n <li class=\"as-people__item\">\n <hm-account-avatar size=\"small\" [name]=\"personName(p)\" [imageUrl]=\"p.picture\" />\n <span class=\"as-people__name\">{{ personName(p) }}</span>\n <hm-icon-button size=\"small\" emphasis=\"subtle\" icon=\"trash\" [label]=\"removePersonLabel(p)\" (clicked)=\"removePerson(p, 'envolvido')\" />\n </li>\n } @empty {\n <li class=\"muted\">{{ text.nobody }}</li>\n }\n </ul>\n <hm-button size=\"small\" emphasis=\"minimal\" icon=\"plus\" [hmPopoverTrigger]=\"watcherPop\">{{ text.addWatcher }}</hm-button>\n <hm-popover #watcherPop [label]=\"text.addWatcher\">\n @if (watcherPop.open()) {\n <spine-suggestion-people-picker [clientId]=\"clientId()\" [exclude]=\"watcherIds()\" (picked)=\"addPerson($event, 'envolvido', watcherPop)\" />\n }\n </hm-popover>\n </div>\n\n <!-- Quem pediu: o autor e, numa que recebeu agrupadas, os relatores que vieram delas. -->\n <div class=\"as-field\">\n <span class=\"as-field__label\">{{ (t.reporters?.length ?? 0) > 1 ? text.reporters : text.reporter }}</span>\n <ul class=\"as-people\">\n @for (p of reportersOf(t); track p.user_id || $index) {\n <li class=\"as-people__item\">\n <hm-account-avatar size=\"small\" [name]=\"personName(p.user_id ? p : null)\" [imageUrl]=\"p.picture\" />\n <span class=\"as-people__name\">{{ personName(p.user_id ? p : null) }}</span>\n @if ((t.reporters?.length ?? 0) > 1 && reporterHint(t, p); as hint) {\n <span class=\"as-field__hint\">{{ hint }}</span>\n }\n </li>\n }\n </ul>\n <span class=\"as-field__hint\">{{ t.origin === 'equipe' ? text.originTeam : text.originUser }}</span>\n </div>\n\n @if (t.merged?.length) {\n <div class=\"as-field\">\n <span class=\"as-field__label\">{{ text.mergedHere }}</span>\n <ul class=\"as-merged\">\n @for (m of t.merged; track m.id) {\n <li class=\"as-merged__item\">\n <button type=\"button\" class=\"as-merged__link\" (click)=\"openTask.emit(m.id)\">\n <span class=\"as-task__key\">{{ m.key }}</span> {{ m.title }}\n </button>\n <span class=\"as-merged__author\">\n <hm-account-avatar size=\"small\" [name]=\"personName(m.author)\" [imageUrl]=\"m.author?.picture\" />\n {{ personName(m.author) }}\n </span>\n <hm-icon-button size=\"small\" emphasis=\"subtle\" icon=\"unlink\" [label]=\"unmergeLabel(m.key)\"\n [disabled]=\"busy()\" (clicked)=\"unmergeOrigin(m)\" />\n </li>\n }\n </ul>\n </div>\n }\n\n <hm-select [label]=\"text.priority\" [value]=\"t.priority\" [disabled]=\"busy()\" (valueChange)=\"changePriority($any($event))\">\n @for (p of priorities; track p) { <option [value]=\"p\">{{ text.priorities[p] }}</option> }\n </hm-select>\n <hm-select [label]=\"text.kind\" [value]=\"t.kind\" [disabled]=\"busy()\" (valueChange)=\"changeKind($any($event))\">\n @for (k of kinds; track k) { <option [value]=\"k\">{{ text.kinds[k] }}</option> }\n </hm-select>\n <div class=\"as-field__dates\">\n <hm-date-picker [label]=\"text.startDate\" [value]=\"t.start_date\" [max]=\"$any(t.due_date)\" (valueChange)=\"changeDate('start_date', $event)\" />\n <hm-date-picker [label]=\"text.dueDate\" [value]=\"t.due_date\" [min]=\"$any(t.start_date)\" (valueChange)=\"changeDate('due_date', $event)\" />\n </div>\n @if (t.due_date) {\n <span class=\"as-due\" [attr.data-due]=\"dueSituation()\"><hm-icon shape=\"calendar\" />{{ dueText(t) }}</span>\n }\n\n <div class=\"as-field\">\n <div class=\"as-field__head\">\n <span class=\"as-field__label\">{{ text.labels }}</span>\n <hm-icon-button size=\"small\" emphasis=\"subtle\" icon=\"tag\" [label]=\"text.addLabel\" [hmPopoverTrigger]=\"labelPop\" />\n </div>\n <div class=\"as-field__chips\">\n @for (l of t.labels; track l.id) {\n <span class=\"as-label\" [class]=\"'as-tok--' + l.color\">{{ l.name }}</span>\n } @empty {\n <span class=\"muted\">{{ text.noLabels }}</span>\n }\n </div>\n <hm-popover #labelPop [label]=\"text.labels\">\n @if (labelPop.open()) {\n <spine-suggestion-label-picker [labels]=\"labels()\" [selected]=\"selectedLabels()\"\n (toggled)=\"toggleLabel($event.label, $event.linked)\" (created)=\"createLabel($event.name, $event.color)\" />\n }\n </hm-popover>\n </div>\n\n <div class=\"as-field\">\n <div class=\"as-field__head\">\n <span class=\"as-field__label\">{{ text.cover }}</span>\n <hm-icon-button size=\"small\" emphasis=\"subtle\" icon=\"color-palette\" [label]=\"text.menuCover\" [hmPopoverTrigger]=\"coverPop\" />\n </div>\n @if (t.cover) {\n <span class=\"as-field__cover\" [class]=\"'as-tok--' + t.cover\" aria-hidden=\"true\"></span>\n } @else {\n <span class=\"muted\">\u2014</span>\n }\n <hm-popover #coverPop [label]=\"text.cover\">\n <spine-suggestion-cover-picker [value]=\"t.cover\" (picked)=\"changeCover($event, coverPop)\" />\n </hm-popover>\n </div>\n\n <dl class=\"as-facts\">\n <dt>{{ text.screenOrigin }}</dt>\n <dd>\n {{ t.screen.label || text.noScreen }}\n @if (t.screen.path && canOpenScreen()) {\n <a class=\"as-facts__link\" [href]=\"screenHref(t.screen.path)\" (click)=\"openScreen($event, t.screen.path)\">\n <hm-icon shape=\"pop-out\" />{{ text.openScreen }}\n </a>\n }\n </dd>\n <dt>{{ text.votesField }}</dt>\n <dd>{{ t.votes }}</dd>\n <dt>{{ text.createdAt }}</dt>\n <dd>{{ date(t.created_at, true) }}</dd>\n <dt>{{ text.updatedAt }}</dt>\n <dd>{{ date(t.updated_at, true) }}</dd>\n </dl>\n @if (lastChange(t); as change) {\n <p class=\"as-field__hint\">{{ change }}</p>\n }\n\n <div class=\"as-task__danger\">\n <!-- Sem cadeia: nem a agrupada nem a que recebeu outras v\u00E3o para dentro de outra. -->\n @if (!t.merged_into && !t.merged_count) {\n <hm-button size=\"small\" emphasis=\"medium\" icon=\"layers\" (clicked)=\"mergeRequested.emit(t)\">{{ text.menuMerge }}</hm-button>\n }\n <hm-button size=\"small\" emphasis=\"medium\" [icon]=\"t.archived_at ? 'unarchive' : 'archive'\" (clicked)=\"toggleArchived()\">\n {{ t.archived_at ? text.menuRestore : text.menuArchive }}\n </hm-button>\n <hm-button size=\"small\" emphasis=\"medium\" intent=\"danger\" icon=\"trash\" (clicked)=\"remove()\">{{ text.menuDelete }}</hm-button>\n </div>\n </aside>\n </div>\n } @else if (draft()) {\n <!-- Nova tarefa: o essencial; o resto (pessoas, checklist, conversa) assim que existir. -->\n <div class=\"as-task\">\n <div class=\"as-task__main\">\n <hm-input [label]=\"text.title\" [placeholder]=\"text.addTaskPlaceholder\" [maxLength]=\"limits.title\" required\n [error]=\"draftTitleError()\" [value]=\"draftTitle()\" (valueChange)=\"draftTitle.set($event)\" />\n <hm-rich-text [label]=\"text.description\" [rows]=\"6\" [maxRows]=\"20\" [value]=\"draftDescription()\" (valueChange)=\"draftDescription.set($event)\" />\n <p class=\"as-field__hint\">{{ text.draftHint }}</p>\n </div>\n <aside class=\"as-task__side\" [attr.aria-label]=\"text.taskLabel\">\n <hm-select [label]=\"text.status\" [value]=\"draftColumn()\" (valueChange)=\"draftColumn.set($any($event))\">\n @for (st of statuses; track st) { <option [value]=\"st\">{{ text.statuses[st] }}</option> }\n </hm-select>\n <hm-select [label]=\"text.kind\" [value]=\"draftKind()\" (valueChange)=\"draftKind.set($any($event))\">\n @for (k of kinds; track k) { <option [value]=\"k\">{{ text.kinds[k] }}</option> }\n </hm-select>\n <hm-select [label]=\"text.priority\" [value]=\"draftPriority()\" (valueChange)=\"draftPriority.set($any($event))\">\n @for (p of priorities; track p) { <option [value]=\"p\">{{ text.priorities[p] }}</option> }\n </hm-select>\n <div class=\"as-field__dates\">\n <hm-date-picker [label]=\"text.startDate\" [value]=\"draftStart()\" [max]=\"$any(draftDue())\" (valueChange)=\"draftStart.set($event)\" />\n <hm-date-picker [label]=\"text.dueDate\" [value]=\"draftDue()\" [min]=\"$any(draftStart())\" (valueChange)=\"draftDue.set($event)\" />\n </div>\n <div class=\"as-field\">\n <span class=\"as-field__label\">{{ text.labels }}</span>\n <spine-suggestion-label-picker [labels]=\"labels()\" [selected]=\"draftLabels()\"\n (toggled)=\"toggleLabel($event.label, $event.linked)\" (created)=\"createLabel($event.name, $event.color)\" />\n </div>\n <hm-checkbox [label]=\"text.assignMeOnCreate\" [checked]=\"draftAssignMe()\" (checkedChange)=\"draftAssignMe.set($event)\" />\n </aside>\n </div>\n }\n\n <div footer class=\"as-task__footer\">\n @if (!current() && draft()) {\n <hm-button emphasis=\"medium\" (clicked)=\"closed.emit()\">{{ text.cancel }}</hm-button>\n <hm-button icon=\"plus\" [loading]=\"busy()\" (clicked)=\"create()\">{{ text.createTask }}</hm-button>\n } @else {\n <hm-button emphasis=\"medium\" (clicked)=\"closed.emit()\">{{ text.close }}</hm-button>\n }\n </div>\n</hm-modal>\n", styles: [".as-stack{display:flex;flex-direction:column;gap:var(--hm-space-md);min-inline-size:0}.as-actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--hm-space-xs)}.as-field-row{display:flex;flex-wrap:wrap;align-items:flex-end;gap:var(--hm-space-sm)}.as-field-row>hm-combobox{flex:1 1 16rem;min-inline-size:0}.as-summary{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-sm)}.as-summary__info{display:flex;flex:1 1 16rem;flex-direction:column;gap:var(--hm-space-micro);min-inline-size:0}.as-summary__title{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs);margin:0;color:var(--hm-text);font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-semibold)}.as-summary__detail{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.muted{color:var(--hm-muted)}.as-empty{margin:var(--hm-space-xs) 0 0;font-size:var(--hm-font-size-sm)}.as-secret{display:grid;gap:var(--hm-space-micro)}.as-secret__label{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.as-secret__row{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.as-secret__value{flex:1 1 240px;min-width:0;border:var(--hm-border-hairline) solid var(--hm-divider);border-radius:var(--hm-radius-control);padding:var(--hm-space-xs) var(--hm-space-sm);background:var(--hm-surface-muted);color:var(--hm-text);font-family:var(--hm-font-mono);font-size:var(--hm-font-size-sm);overflow-wrap:anywhere;-webkit-user-select:all;user-select:all}.as-tok--cat-1{--as-tok: var(--hm-data-categorical-1);background:var(--as-tok)}.as-tok--cat-2{--as-tok: var(--hm-data-categorical-2);background:var(--as-tok)}.as-tok--cat-3{--as-tok: var(--hm-data-categorical-3);background:var(--as-tok)}.as-tok--cat-4{--as-tok: var(--hm-data-categorical-4);background:var(--as-tok)}.as-tok--cat-5{--as-tok: var(--hm-data-categorical-5);background:var(--as-tok)}.as-tok--cat-6{--as-tok: var(--hm-data-categorical-6);background:var(--as-tok)}.as-tok--cat-7{--as-tok: var(--hm-data-categorical-7);background:var(--as-tok)}.as-tok--cat-8{--as-tok: var(--hm-data-categorical-8);background:var(--as-tok)}.as-tok--grad-1{--as-tok: var(--hm-data-categorical-1);background:linear-gradient(135deg,var(--hm-data-categorical-1),var(--hm-data-categorical-2))}.as-tok--grad-2{--as-tok: var(--hm-data-categorical-3);background:linear-gradient(135deg,var(--hm-data-categorical-3),var(--hm-data-categorical-4))}.as-tok--grad-3{--as-tok: var(--hm-data-categorical-5);background:linear-gradient(135deg,var(--hm-data-categorical-5),var(--hm-data-categorical-6))}.as-tok--grad-4{--as-tok: var(--hm-data-categorical-7);background:linear-gradient(135deg,var(--hm-data-categorical-7),var(--hm-data-categorical-8))}.as-tok--grad-5{--as-tok: var(--hm-data-categorical-1);background:linear-gradient(135deg,var(--hm-data-categorical-1),var(--hm-data-categorical-5))}.as-tok--grad-6{--as-tok: var(--hm-data-categorical-2);background:linear-gradient(135deg,var(--hm-data-categorical-2),var(--hm-data-categorical-6))}.as-label{display:inline-flex;align-items:center;gap:var(--hm-space-micro);max-inline-size:100%;padding:0 var(--hm-space-xxxs);border-radius:var(--hm-radius-pill);background:color-mix(in srgb,var(--as-tok, var(--hm-border)) 18%,var(--hm-surface-raised));color:var(--hm-text);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold);line-height:1.6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.as-label:before{flex:none;inline-size:.5rem;block-size:.5rem;border-radius:50%;background:var(--as-tok, var(--hm-border));content:\"\"}@media(forced-colors:active){.as-label{border:var(--hm-border-hairline) solid CanvasText}}.as-task{display:grid;grid-template-columns:minmax(0,1fr) minmax(15rem,20rem);gap:var(--hm-space-lg);align-items:start}@media(max-width:48rem){.as-task{grid-template-columns:minmax(0,1fr)}}.as-task__cover{block-size:3rem;margin-block-end:var(--hm-space-sm);border-radius:var(--hm-radius-control)}.as-task__main{display:flex;flex-direction:column;gap:var(--hm-space-md);min-inline-size:0}.as-task__key-row{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.as-task__key{color:var(--hm-muted);font-family:var(--hm-font-mono, monospace);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.as-task__title{margin:0;font-size:var(--hm-font-size-xl, 1.375rem)}.as-task__title-button{display:inline-flex;align-items:baseline;gap:var(--hm-space-xs);padding:0;border:0;background:none;color:var(--hm-text);font:inherit;font-weight:var(--hm-font-weight-bold);text-align:start;overflow-wrap:anywhere;cursor:text}.as-task__title-button:hover .as-task__title-pen,.as-task__title-button:focus-visible .as-task__title-pen{opacity:1}.as-task__title-button:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color);outline-offset:2px}.as-task__title-pen{color:var(--hm-muted);opacity:0;font-size:var(--hm-font-size-md)}.as-task__section{display:flex;flex-direction:column;gap:var(--hm-space-xs);padding:var(--hm-space-sm);border-radius:var(--hm-radius-control);background:var(--hm-surface-muted)}.as-task__section-head{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--hm-space-xs)}.as-task__heading{display:flex;align-items:center;gap:var(--hm-space-xs);margin:0;color:var(--hm-text);font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-semibold)}.as-task__count{color:var(--hm-muted);font-size:var(--hm-font-size-sm);font-variant-numeric:tabular-nums}.as-task__progress{block-size:.375rem;border-radius:var(--hm-radius-pill);background:var(--hm-border);overflow:hidden}.as-task__progress span{display:block;block-size:100%;background:var(--hm-success)}.as-task__reply{display:flex;flex-direction:column;gap:var(--hm-space-xs);margin-block:var(--hm-space-sm)}.as-task__reply-row{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--hm-space-xs)}.as-task__side{display:flex;flex-direction:column;gap:var(--hm-space-sm);padding:var(--hm-space-sm);border-radius:var(--hm-radius-control);background:var(--hm-surface-muted)}.as-task__danger{display:flex;flex-wrap:wrap;gap:var(--hm-space-xs);padding-block-start:var(--hm-space-xs);border-block-start:var(--hm-border-hairline) solid var(--hm-border)}.as-task__footer{display:flex;justify-content:flex-end;gap:var(--hm-space-xs)}.as-field{display:flex;flex-direction:column;gap:var(--hm-space-micro)}.as-field__head{display:flex;align-items:center;justify-content:space-between;gap:var(--hm-space-xs)}.as-field__label{color:var(--hm-muted);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.as-field__hint{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.as-field__chips{display:flex;flex-wrap:wrap;gap:var(--hm-space-micro)}.as-field__dates{display:grid;grid-template-columns:1fr 1fr;gap:var(--hm-space-xs)}.as-field__cover{display:block;block-size:1.5rem;border-radius:var(--hm-radius-control)}.as-people{display:flex;flex-direction:column;gap:var(--hm-space-micro);margin:0;padding:0;list-style:none}.as-people__item{display:flex;align-items:center;gap:var(--hm-space-xs);min-inline-size:0}.as-people__name{flex:1;min-inline-size:0;overflow:hidden;color:var(--hm-text);font-size:var(--hm-font-size-sm);text-overflow:ellipsis;white-space:nowrap}.as-task__merged{display:flex;flex-wrap:wrap;align-items:flex-start;gap:var(--hm-space-xs) var(--hm-space-sm);padding:var(--hm-space-xs) var(--hm-space-sm);border-inline-start:var(--hm-border-thin) solid var(--hm-info);border-radius:var(--hm-radius-control);background:var(--hm-info-bg);color:var(--hm-text)}.as-task__merged-icon{flex:none;color:var(--hm-info)}.as-task__merged-text{display:flex;flex:1 1 12rem;flex-direction:column;gap:var(--hm-space-micro);min-inline-size:0;overflow-wrap:anywhere}.as-merged{display:flex;flex-direction:column;gap:var(--hm-space-micro);margin:0;padding:0;list-style:none}.as-merged__item{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:var(--hm-space-micro) var(--hm-space-xs);padding:var(--hm-space-xxxs) var(--hm-space-xs);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface-raised)}.as-merged__item hm-icon-button{grid-row:1/span 2;grid-column:2}.as-merged__link{padding:0;border:0;background:none;color:var(--hm-accent);font:inherit;font-size:var(--hm-font-size-sm);text-align:start;overflow-wrap:anywhere;cursor:pointer}.as-merged__link:hover{text-decoration:underline}.as-merged__link:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color);outline-offset:2px}.as-merged__author{display:inline-flex;align-items:center;gap:var(--hm-space-micro);color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.as-due{display:inline-flex;align-items:center;gap:var(--hm-space-micro);color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.as-due[data-due=overdue]{color:var(--hm-danger);font-weight:var(--hm-font-weight-semibold)}.as-due[data-due=today],.as-due[data-due=tomorrow]{color:var(--hm-text);font-weight:var(--hm-font-weight-semibold)}.as-facts{display:grid;grid-template-columns:max-content 1fr;gap:var(--hm-space-xxxs) var(--hm-space-sm);margin:0;font-size:var(--hm-font-size-sm)}.as-facts dt{color:var(--hm-muted)}.as-facts dd{margin:0;color:var(--hm-text);overflow-wrap:anywhere}.as-facts__link{display:inline-flex;align-items:center;gap:var(--hm-space-micro);margin-inline-start:var(--hm-space-xxxs);color:var(--hm-accent)}.as-checklist{display:flex;flex-direction:column;gap:var(--hm-space-micro);margin:0;padding:0;list-style:none}.as-checklist__item{display:flex;align-items:center;gap:var(--hm-space-xs);min-block-size:var(--hm-control-height-dense)}.as-checklist__item--done .as-checklist__check{color:var(--hm-muted);text-decoration:line-through}.as-checklist__item:hover .as-checklist__tools,.as-checklist__item:focus-within .as-checklist__tools{opacity:1}.as-checklist__check{flex:1;min-inline-size:0}.as-checklist__rename{flex:1}.as-checklist__tools{display:inline-flex;opacity:0}.as-checklist__add{display:flex;gap:var(--hm-space-xs);align-items:flex-start}.as-checklist__new{flex:1}@media(hover:none){.as-checklist__tools{opacity:1}}.as-timeline{display:flex;flex-direction:column;gap:var(--hm-space-sm);margin:var(--hm-space-sm) 0 0;padding:0;list-style:none}.as-timeline__item{position:relative;display:flex;align-items:flex-start;gap:var(--hm-space-xs);font-size:var(--hm-font-size-sm)}.as-timeline__item:not(:last-child):before{position:absolute;inset-block:var(--hm-space-lg) calc(var(--hm-space-sm) * -1);inset-inline-start:calc(var(--hm-space-sm) - var(--hm-border-hairline));border-inline-start:var(--hm-border-thin) solid var(--hm-border);content:\"\"}.as-timeline__face{position:relative;flex:none}.as-timeline__body{display:flex;flex-direction:column;gap:var(--hm-space-micro);min-inline-size:0}.as-timeline__line{display:inline-flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-micro);color:var(--hm-text)}.as-timeline__comment{margin:0;padding:var(--hm-space-xs) var(--hm-space-sm);border-radius:var(--hm-radius-control);background:var(--hm-surface-muted);color:var(--hm-text);white-space:pre-line;overflow-wrap:anywhere}.as-timeline__comment--internal{border-inline-start:var(--hm-border-thin) solid var(--hm-warning);background:var(--hm-warning-bg)}\n"], dependencies: [{ kind: "component", type: AccountAvatarComponent, selector: "hm-account-avatar", inputs: ["name", "imageUrl", "size", "loading"] }, { kind: "component", type: AlertComponent, selector: "hm-alert", inputs: ["variant", "appearance", "heading", "closable", "size", "showIcon", "link", "linkLabel", "linkTarget", "dismissKey", "duration"], outputs: ["closed"] }, { kind: "component", type: ButtonComponent, selector: "hm-button", inputs: ["emphasis", "intent", "size", "type", "icon", "iconDirection", "iconEnd", "disabled", "loading", "loadingState", "block", "link", "newTab", "caret", "inMenu", "toggle", "pressed"], outputs: ["loadingStateChange", "pressedChange", "clicked"] }, { kind: "component", type: CheckboxComponent, selector: "hm-checkbox", inputs: ["label", "labelHidden", "checked", "indeterminate"], outputs: ["checkedChange"] }, { kind: "component", type: DatePickerComponent, selector: "hm-date-picker", inputs: ["value", "label", "labelHidden", "hint", "placeholder", "min", "max", "valueFormat"], outputs: ["valueChange"] }, { kind: "component", type: IconButtonComponent, selector: "hm-icon-button", inputs: ["label", "icon", "solid", "iconDirection", "emphasis", "intent", "size", "disabled", "toggle", "pressed", "keyShortcuts"], outputs: ["pressedChange", "clicked"] }, { kind: "component", type: IconComponent, selector: "hm-icon", inputs: ["shape", "direction", "size", "solid", "badge", "status", "flip", "inverse", "label"] }, { kind: "component", type: InputComponent, selector: "hm-input", inputs: ["value", "label", "type", "placeholder", "icon", "labelHidden", "hint", "prefix", "suffix", "prefixIcon", "mask", "maskValue", "min", "max", "step", "stepButtons", "maxLength", "autocomplete", "inputMode"], outputs: ["valueChange"] }, { kind: "component", type: ModalComponent, selector: "hm-modal", inputs: ["open", "heading", "size", "closeLabel", "closable", "beforeClose"], outputs: ["openChange", "closed"] }, { kind: "component", type: PopoverComponent, selector: "hm-popover", inputs: ["open", "heading", "headingLevel", "label", "side", "align", "arrow", "closable", "closeLabel", "closeOnOutsideClick", "openOn", "openDelay", "closeDelay"], outputs: ["openChange"] }, { kind: "directive", type: PopoverTriggerDirective, selector: "[hmPopoverTrigger]", inputs: ["hmPopoverTrigger"] }, { kind: "component", type: RichTextComponent, selector: "hm-rich-text", inputs: ["value", "label", "labelHidden", "hint", "placeholder", "readonly", "loading", "tools", "headingLevels", "hideToolbar", "rows", "maxRows", "maxLength", "showCounter", "pasteMode", "linkProtocols", "allowRelativeLinks", "spellcheck"], outputs: ["valueChange", "focusChange"] }, { kind: "component", type: SelectComponent, selector: "hm-select", inputs: ["value", "label", "labelHidden", "hint"], outputs: ["valueChange"] }, { kind: "component", type: SuggestionCoverPickerComponent, selector: "spine-suggestion-cover-picker", inputs: ["value"], outputs: ["picked"] }, { kind: "component", type: SuggestionLabelPickerComponent, selector: "spine-suggestion-label-picker", inputs: ["labels", "selected"], outputs: ["toggled", "created"] }, { kind: "component", type: SuggestionPeoplePickerComponent, selector: "spine-suggestion-people-picker", inputs: ["clientId", "exclude"], outputs: ["picked"] }, { kind: "component", type: SwitchComponent, selector: "hm-switch", inputs: ["value", "label", "hint", "labelPosition", "loading"], outputs: ["valueChange"] }, { kind: "component", type: TabComponent, selector: "hm-tab", inputs: ["id", "label", "disabled", "count", "route", "exact", "destroyOnHide"] }, { kind: "component", type: TabsComponent, selector: "hm-tabs", inputs: ["label", "active", "orientation", "mode", "overflow", "fillHeight", "noPanelGap"], outputs: ["activeChange"] }, { kind: "component", type: TagComponent, selector: "hm-tag", inputs: ["name", "color", "compact"] }, { kind: "component", type: TextareaComponent, selector: "hm-textarea", inputs: ["value", "label", "hint", "placeholder", "maxLength", "rows", "autoGrow", "labelHidden", "loading", "maxRows", "submitKey"], outputs: ["valueChange", "submitted"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1074
+ }
1075
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionTaskDetailComponent, decorators: [{
1076
+ type: Component,
1077
+ args: [{ selector: 'spine-suggestion-task-detail', imports: [
1078
+ AccountAvatarComponent, AlertComponent, ButtonComponent, CheckboxComponent, DatePickerComponent, IconButtonComponent,
1079
+ IconComponent, InputComponent, ModalComponent, PopoverComponent, PopoverTriggerDirective, RichTextComponent,
1080
+ SelectComponent, SuggestionCoverPickerComponent, SuggestionLabelPickerComponent, SuggestionPeoplePickerComponent,
1081
+ SwitchComponent, TabComponent, TabsComponent, TagComponent, TextareaComponent,
1082
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<hm-modal [heading]=\"heading()\" [open]=\"open()\" (openChange)=\"setOpen($event)\" size=\"xlarge\">\n @if (current(); as t) {\n @if (t.cover) {\n <div class=\"as-task__cover\" [class]=\"'as-tok--' + t.cover\" aria-hidden=\"true\"></div>\n }\n @if (loadError()) {\n <hm-alert variant=\"error\">{{ loadError() }}</hm-alert>\n }\n <div class=\"as-task\">\n <!-- Esquerda: o trabalho (t\u00EDtulo, descri\u00E7\u00E3o, checklist, conversa). -->\n <div class=\"as-task__main\">\n <div class=\"as-task__key-row\">\n <span class=\"as-task__key\">{{ t.key }}</span>\n <hm-icon-button size=\"small\" emphasis=\"subtle\" icon=\"copy\" [label]=\"text.copyKey\" (clicked)=\"copyKey()\" />\n <hm-tag [name]=\"text.kinds[t.kind]\" [color]=\"kindColors[t.kind]\" />\n @if (t.archived_at) { <hm-tag [name]=\"text.archivedTag\" /> }\n @if (t.merged_into) { <hm-tag [name]=\"text.mergedTag\" color=\"info\" /> }\n </div>\n\n <!-- Agrupada: o destino tem os relatores e os apoios; os coment\u00E1rios ficam aqui. -->\n @if (t.merged_into; as into) {\n <div class=\"as-task__merged\">\n <hm-icon class=\"as-task__merged-icon\" shape=\"layers\" />\n <div class=\"as-task__merged-text\">\n <strong>{{ mergedIntoText(into.key) }}</strong>\n <span>{{ into.title }}</span>\n <span class=\"as-field__hint\">{{ text.mergedIntoHint }}</span>\n </div>\n <div class=\"as-actions\">\n <hm-button size=\"small\" emphasis=\"medium\" (clicked)=\"openTask.emit(into.id)\">{{ openMergedText(into.key) }}</hm-button>\n <hm-button size=\"small\" emphasis=\"minimal\" icon=\"unlink\" [disabled]=\"busy()\" (clicked)=\"unmergeSelf()\">{{ text.unmerge }}</hm-button>\n </div>\n </div>\n }\n\n @if (editingTitle()) {\n <hm-input [label]=\"text.title\" labelHidden [maxLength]=\"limits.title\" [value]=\"titleDraft()\"\n (valueChange)=\"titleDraft.set($event)\" (keydown)=\"onTitleKey($event)\" (focusout)=\"saveTitle()\" />\n } @else {\n <h2 class=\"as-task__title\">\n <button type=\"button\" class=\"as-task__title-button\" [attr.aria-label]=\"text.editTitle + ': ' + t.title\" (click)=\"startTitle()\">\n {{ t.title }}<hm-icon class=\"as-task__title-pen\" shape=\"pencil\" />\n </button>\n </h2>\n }\n\n <section class=\"as-task__section\" aria-labelledby=\"as-task-description\">\n <div class=\"as-task__section-head\">\n <h3 id=\"as-task-description\" class=\"as-task__heading\">{{ text.description }}</h3>\n @if (!editingDescription()) {\n <hm-button size=\"small\" emphasis=\"minimal\" icon=\"pencil\" (clicked)=\"startDescription()\">{{ text.editDescription }}</hm-button>\n }\n </div>\n @if (editingDescription()) {\n <hm-rich-text [label]=\"text.description\" labelHidden [rows]=\"6\" [maxRows]=\"20\" [value]=\"descriptionDraft()\"\n (valueChange)=\"descriptionDraft.set($event)\" />\n <div class=\"as-actions\">\n <hm-button emphasis=\"medium\" (clicked)=\"editingDescription.set(false)\">{{ text.cancel }}</hm-button>\n <hm-button [loading]=\"busy()\" (clicked)=\"saveDescription()\">{{ text.save }}</hm-button>\n </div>\n } @else if (t.description_html) {\n <hm-rich-text class=\"as-task__description\" [label]=\"text.description\" labelHidden readonly hideToolbar [value]=\"t.description_html\" />\n } @else {\n <p class=\"muted\">{{ text.noDescription }}</p>\n }\n </section>\n\n <section class=\"as-task__section\" aria-labelledby=\"as-task-checklist\">\n <div class=\"as-task__section-head\">\n <h3 id=\"as-task-checklist\" class=\"as-task__heading\">\n {{ text.checklist }}\n @if (t.checklist.total) { <span class=\"as-task__count\">{{ t.checklist.done }}/{{ t.checklist.total }}</span> }\n </h3>\n <hm-switch [label]=\"text.checklistOnCard\" [value]=\"t.checklist_on_card\" (valueChange)=\"toggleChecklistOnCard($event)\" />\n </div>\n @if (t.checklist.total) {\n <div class=\"as-task__progress\" role=\"progressbar\" aria-valuemin=\"0\" [attr.aria-valuemax]=\"t.checklist.total\"\n [attr.aria-valuenow]=\"t.checklist.done\" [attr.aria-label]=\"text.checklist\">\n <span [style.inline-size.%]=\"(t.checklist.done / t.checklist.total) * 100\"></span>\n </div>\n }\n <ul class=\"as-checklist\">\n @for (item of checklistItems(); track item.id; let first = $first; let last = $last) {\n <li class=\"as-checklist__item\" [class.as-checklist__item--done]=\"item.done\">\n @if (renaming() === item.id) {\n <hm-input class=\"as-checklist__rename\" [label]=\"text.checklist\" labelHidden [maxLength]=\"500\" [value]=\"renameDraft()\"\n (valueChange)=\"renameDraft.set($event)\" (keydown)=\"onRenameKey($event, item)\" (focusout)=\"saveRename(item)\" />\n } @else {\n <hm-checkbox class=\"as-checklist__check\" [label]=\"item.text\" [checked]=\"item.done\" (checkedChange)=\"toggleItem(item, $event)\" />\n <span class=\"as-checklist__tools\">\n <hm-icon-button size=\"small\" emphasis=\"subtle\" icon=\"pencil\" [label]=\"text.editTitle + ': ' + item.text\" (clicked)=\"startRename(item)\" />\n <hm-icon-button size=\"small\" emphasis=\"subtle\" icon=\"angle-up\" [label]=\"itemLabel(text.checklistUp, item)\" [disabled]=\"first\" (clicked)=\"moveItem(item, -1)\" />\n <hm-icon-button size=\"small\" emphasis=\"subtle\" icon=\"angle-down\" [label]=\"itemLabel(text.checklistDown, item)\" [disabled]=\"last\" (clicked)=\"moveItem(item, 1)\" />\n <hm-icon-button size=\"small\" emphasis=\"subtle\" icon=\"trash\" [label]=\"itemLabel(text.checklistRemove, item)\" (clicked)=\"removeItem(item)\" />\n </span>\n }\n </li>\n } @empty {\n <li class=\"muted\">{{ text.checklistEmpty }}</li>\n }\n </ul>\n <div class=\"as-checklist__add\">\n <hm-input class=\"as-checklist__new\" [label]=\"text.checklistAdd\" labelHidden [placeholder]=\"text.checklistPlaceholder\"\n [maxLength]=\"500\" [value]=\"newItem()\" (valueChange)=\"newItem.set($event)\" (keydown)=\"onNewItemKey($event)\" />\n <hm-button emphasis=\"medium\" icon=\"plus\" [disabled]=\"!newItem().trim()\" (clicked)=\"addItem()\">{{ text.checklistAdd }}</hm-button>\n </div>\n </section>\n\n <hm-tabs [label]=\"text.taskLabel\" [active]=\"tab()\" (activeChange)=\"selectTab($event)\">\n <hm-tab id=\"comments\" [label]=\"text.tabComments\" [count]=\"commentsTotal() || null\">\n <div class=\"as-task__reply\">\n <hm-textarea [label]=\"text.comment\" [hint]=\"commentInternal() ? '' : text.commentHint\" [maxLength]=\"limits.description\"\n [rows]=\"2\" autoGrow [maxRows]=\"8\" [error]=\"commentError()\" [value]=\"comment()\" (valueChange)=\"comment.set($event)\" />\n <div class=\"as-task__reply-row\">\n <hm-checkbox [label]=\"text.commentInternal\" [checked]=\"commentInternal()\" (checkedChange)=\"commentInternal.set($event)\" />\n <hm-button icon=\"comment-message\" [loading]=\"commenting()\" (clicked)=\"sendComment()\">{{ text.commentSend }}</hm-button>\n </div>\n </div>\n <ol class=\"as-timeline\">\n @for (e of comments(); track e.id) {\n <li class=\"as-timeline__item\">\n <hm-account-avatar class=\"as-timeline__face\" size=\"small\" [name]=\"e.actor ? personName(e.actor) : text.someone\" [imageUrl]=\"e.actor?.picture\" />\n <div class=\"as-timeline__body\">\n <span class=\"as-timeline__line\">\n <strong>{{ e.actor ? personName(e.actor) : text.someone }}</strong> \u00B7\n <time class=\"muted\" [attr.datetime]=\"e.created_at\" [attr.title]=\"date(e.created_at, true)\">{{ age(e.created_at) }}</time>\n @if (e.kind === 'status') { \u00B7 <span class=\"muted\">{{ eventText(e) }}</span> }\n @if (e.internal) { <hm-tag [name]=\"text.internalTag\" color=\"warning\" /> }\n </span>\n <p class=\"as-timeline__comment\" [class.as-timeline__comment--internal]=\"e.internal\">{{ e.comment }}</p>\n </div>\n </li>\n } @empty {\n @if (!eventsLoading()) { <li class=\"muted\">{{ text.commentsEmpty }}</li> }\n }\n </ol>\n @if (comments().length < commentsTotal()) {\n <hm-button emphasis=\"medium\" [loading]=\"eventsLoading()\" (clicked)=\"loadEvents('comments', false)\">{{ text.loadMore }}</hm-button>\n }\n </hm-tab>\n <hm-tab id=\"activity\" [label]=\"text.tabActivity\">\n <ol class=\"as-timeline\">\n @for (e of activity(); track e.id) {\n <li class=\"as-timeline__item\">\n <hm-account-avatar class=\"as-timeline__face\" size=\"small\" [name]=\"e.actor ? personName(e.actor) : text.someone\" [imageUrl]=\"e.actor?.picture\" />\n <div class=\"as-timeline__body\">\n <span class=\"as-timeline__line\">{{ eventText(e) }} \u00B7\n <time class=\"muted\" [attr.datetime]=\"e.created_at\" [attr.title]=\"date(e.created_at, true)\">{{ age(e.created_at) }}</time>\n </span>\n @if (e.comment) {\n <p class=\"as-timeline__comment\" [class.as-timeline__comment--internal]=\"e.internal\">{{ e.comment }}</p>\n }\n </div>\n </li>\n } @empty {\n @if (!eventsLoading()) { <li class=\"muted\">{{ text.activityEmpty }}</li> }\n }\n </ol>\n @if (activity().length < activityTotal()) {\n <hm-button emphasis=\"medium\" [loading]=\"eventsLoading()\" (clicked)=\"loadEvents('activity', false)\">{{ text.loadMore }}</hm-button>\n }\n </hm-tab>\n </hm-tabs>\n </div>\n\n <!-- Direita: os campos da tarefa (cada mudan\u00E7a grava na hora). -->\n <aside class=\"as-task__side\" [attr.aria-label]=\"text.taskLabel\">\n <hm-select [label]=\"text.status\" [value]=\"t.status\" [disabled]=\"busy()\" (valueChange)=\"changeStatus($any($event))\">\n @for (st of statuses; track st) { <option [value]=\"st\">{{ text.statuses[st] }}</option> }\n </hm-select>\n\n <div class=\"as-field\">\n <div class=\"as-field__head\">\n <span class=\"as-field__label\">{{ text.assignees }}</span>\n @if (!isMine()) {\n <hm-button size=\"small\" emphasis=\"minimal\" icon=\"user-plus\" (clicked)=\"assignMe()\">{{ text.assignMe }}</hm-button>\n }\n </div>\n <ul class=\"as-people\">\n @for (p of t.assignees; track p.user_id) {\n <li class=\"as-people__item\">\n <hm-account-avatar size=\"small\" [name]=\"personName(p)\" [imageUrl]=\"p.picture\" />\n <span class=\"as-people__name\">{{ personName(p) }}</span>\n <hm-icon-button size=\"small\" emphasis=\"subtle\" icon=\"trash\" [label]=\"removePersonLabel(p)\" (clicked)=\"removePerson(p, 'responsavel')\" />\n </li>\n } @empty {\n <li class=\"muted\">{{ text.nobody }}</li>\n }\n </ul>\n <hm-button size=\"small\" emphasis=\"minimal\" icon=\"plus\" [hmPopoverTrigger]=\"assigneePop\">{{ text.addAssignee }}</hm-button>\n <hm-popover #assigneePop [label]=\"text.addAssignee\">\n @if (assigneePop.open()) {\n <spine-suggestion-people-picker [clientId]=\"clientId()\" [exclude]=\"assigneeIds()\" (picked)=\"addPerson($event, 'responsavel', assigneePop)\" />\n }\n </hm-popover>\n </div>\n\n <div class=\"as-field\">\n <span class=\"as-field__label\">{{ text.watchers }}</span>\n <ul class=\"as-people\">\n @for (p of t.watchers; track p.user_id) {\n <li class=\"as-people__item\">\n <hm-account-avatar size=\"small\" [name]=\"personName(p)\" [imageUrl]=\"p.picture\" />\n <span class=\"as-people__name\">{{ personName(p) }}</span>\n <hm-icon-button size=\"small\" emphasis=\"subtle\" icon=\"trash\" [label]=\"removePersonLabel(p)\" (clicked)=\"removePerson(p, 'envolvido')\" />\n </li>\n } @empty {\n <li class=\"muted\">{{ text.nobody }}</li>\n }\n </ul>\n <hm-button size=\"small\" emphasis=\"minimal\" icon=\"plus\" [hmPopoverTrigger]=\"watcherPop\">{{ text.addWatcher }}</hm-button>\n <hm-popover #watcherPop [label]=\"text.addWatcher\">\n @if (watcherPop.open()) {\n <spine-suggestion-people-picker [clientId]=\"clientId()\" [exclude]=\"watcherIds()\" (picked)=\"addPerson($event, 'envolvido', watcherPop)\" />\n }\n </hm-popover>\n </div>\n\n <!-- Quem pediu: o autor e, numa que recebeu agrupadas, os relatores que vieram delas. -->\n <div class=\"as-field\">\n <span class=\"as-field__label\">{{ (t.reporters?.length ?? 0) > 1 ? text.reporters : text.reporter }}</span>\n <ul class=\"as-people\">\n @for (p of reportersOf(t); track p.user_id || $index) {\n <li class=\"as-people__item\">\n <hm-account-avatar size=\"small\" [name]=\"personName(p.user_id ? p : null)\" [imageUrl]=\"p.picture\" />\n <span class=\"as-people__name\">{{ personName(p.user_id ? p : null) }}</span>\n @if ((t.reporters?.length ?? 0) > 1 && reporterHint(t, p); as hint) {\n <span class=\"as-field__hint\">{{ hint }}</span>\n }\n </li>\n }\n </ul>\n <span class=\"as-field__hint\">{{ t.origin === 'equipe' ? text.originTeam : text.originUser }}</span>\n </div>\n\n @if (t.merged?.length) {\n <div class=\"as-field\">\n <span class=\"as-field__label\">{{ text.mergedHere }}</span>\n <ul class=\"as-merged\">\n @for (m of t.merged; track m.id) {\n <li class=\"as-merged__item\">\n <button type=\"button\" class=\"as-merged__link\" (click)=\"openTask.emit(m.id)\">\n <span class=\"as-task__key\">{{ m.key }}</span> {{ m.title }}\n </button>\n <span class=\"as-merged__author\">\n <hm-account-avatar size=\"small\" [name]=\"personName(m.author)\" [imageUrl]=\"m.author?.picture\" />\n {{ personName(m.author) }}\n </span>\n <hm-icon-button size=\"small\" emphasis=\"subtle\" icon=\"unlink\" [label]=\"unmergeLabel(m.key)\"\n [disabled]=\"busy()\" (clicked)=\"unmergeOrigin(m)\" />\n </li>\n }\n </ul>\n </div>\n }\n\n <hm-select [label]=\"text.priority\" [value]=\"t.priority\" [disabled]=\"busy()\" (valueChange)=\"changePriority($any($event))\">\n @for (p of priorities; track p) { <option [value]=\"p\">{{ text.priorities[p] }}</option> }\n </hm-select>\n <hm-select [label]=\"text.kind\" [value]=\"t.kind\" [disabled]=\"busy()\" (valueChange)=\"changeKind($any($event))\">\n @for (k of kinds; track k) { <option [value]=\"k\">{{ text.kinds[k] }}</option> }\n </hm-select>\n <div class=\"as-field__dates\">\n <hm-date-picker [label]=\"text.startDate\" [value]=\"t.start_date\" [max]=\"$any(t.due_date)\" (valueChange)=\"changeDate('start_date', $event)\" />\n <hm-date-picker [label]=\"text.dueDate\" [value]=\"t.due_date\" [min]=\"$any(t.start_date)\" (valueChange)=\"changeDate('due_date', $event)\" />\n </div>\n @if (t.due_date) {\n <span class=\"as-due\" [attr.data-due]=\"dueSituation()\"><hm-icon shape=\"calendar\" />{{ dueText(t) }}</span>\n }\n\n <div class=\"as-field\">\n <div class=\"as-field__head\">\n <span class=\"as-field__label\">{{ text.labels }}</span>\n <hm-icon-button size=\"small\" emphasis=\"subtle\" icon=\"tag\" [label]=\"text.addLabel\" [hmPopoverTrigger]=\"labelPop\" />\n </div>\n <div class=\"as-field__chips\">\n @for (l of t.labels; track l.id) {\n <span class=\"as-label\" [class]=\"'as-tok--' + l.color\">{{ l.name }}</span>\n } @empty {\n <span class=\"muted\">{{ text.noLabels }}</span>\n }\n </div>\n <hm-popover #labelPop [label]=\"text.labels\">\n @if (labelPop.open()) {\n <spine-suggestion-label-picker [labels]=\"labels()\" [selected]=\"selectedLabels()\"\n (toggled)=\"toggleLabel($event.label, $event.linked)\" (created)=\"createLabel($event.name, $event.color)\" />\n }\n </hm-popover>\n </div>\n\n <div class=\"as-field\">\n <div class=\"as-field__head\">\n <span class=\"as-field__label\">{{ text.cover }}</span>\n <hm-icon-button size=\"small\" emphasis=\"subtle\" icon=\"color-palette\" [label]=\"text.menuCover\" [hmPopoverTrigger]=\"coverPop\" />\n </div>\n @if (t.cover) {\n <span class=\"as-field__cover\" [class]=\"'as-tok--' + t.cover\" aria-hidden=\"true\"></span>\n } @else {\n <span class=\"muted\">\u2014</span>\n }\n <hm-popover #coverPop [label]=\"text.cover\">\n <spine-suggestion-cover-picker [value]=\"t.cover\" (picked)=\"changeCover($event, coverPop)\" />\n </hm-popover>\n </div>\n\n <dl class=\"as-facts\">\n <dt>{{ text.screenOrigin }}</dt>\n <dd>\n {{ t.screen.label || text.noScreen }}\n @if (t.screen.path && canOpenScreen()) {\n <a class=\"as-facts__link\" [href]=\"screenHref(t.screen.path)\" (click)=\"openScreen($event, t.screen.path)\">\n <hm-icon shape=\"pop-out\" />{{ text.openScreen }}\n </a>\n }\n </dd>\n <dt>{{ text.votesField }}</dt>\n <dd>{{ t.votes }}</dd>\n <dt>{{ text.createdAt }}</dt>\n <dd>{{ date(t.created_at, true) }}</dd>\n <dt>{{ text.updatedAt }}</dt>\n <dd>{{ date(t.updated_at, true) }}</dd>\n </dl>\n @if (lastChange(t); as change) {\n <p class=\"as-field__hint\">{{ change }}</p>\n }\n\n <div class=\"as-task__danger\">\n <!-- Sem cadeia: nem a agrupada nem a que recebeu outras v\u00E3o para dentro de outra. -->\n @if (!t.merged_into && !t.merged_count) {\n <hm-button size=\"small\" emphasis=\"medium\" icon=\"layers\" (clicked)=\"mergeRequested.emit(t)\">{{ text.menuMerge }}</hm-button>\n }\n <hm-button size=\"small\" emphasis=\"medium\" [icon]=\"t.archived_at ? 'unarchive' : 'archive'\" (clicked)=\"toggleArchived()\">\n {{ t.archived_at ? text.menuRestore : text.menuArchive }}\n </hm-button>\n <hm-button size=\"small\" emphasis=\"medium\" intent=\"danger\" icon=\"trash\" (clicked)=\"remove()\">{{ text.menuDelete }}</hm-button>\n </div>\n </aside>\n </div>\n } @else if (draft()) {\n <!-- Nova tarefa: o essencial; o resto (pessoas, checklist, conversa) assim que existir. -->\n <div class=\"as-task\">\n <div class=\"as-task__main\">\n <hm-input [label]=\"text.title\" [placeholder]=\"text.addTaskPlaceholder\" [maxLength]=\"limits.title\" required\n [error]=\"draftTitleError()\" [value]=\"draftTitle()\" (valueChange)=\"draftTitle.set($event)\" />\n <hm-rich-text [label]=\"text.description\" [rows]=\"6\" [maxRows]=\"20\" [value]=\"draftDescription()\" (valueChange)=\"draftDescription.set($event)\" />\n <p class=\"as-field__hint\">{{ text.draftHint }}</p>\n </div>\n <aside class=\"as-task__side\" [attr.aria-label]=\"text.taskLabel\">\n <hm-select [label]=\"text.status\" [value]=\"draftColumn()\" (valueChange)=\"draftColumn.set($any($event))\">\n @for (st of statuses; track st) { <option [value]=\"st\">{{ text.statuses[st] }}</option> }\n </hm-select>\n <hm-select [label]=\"text.kind\" [value]=\"draftKind()\" (valueChange)=\"draftKind.set($any($event))\">\n @for (k of kinds; track k) { <option [value]=\"k\">{{ text.kinds[k] }}</option> }\n </hm-select>\n <hm-select [label]=\"text.priority\" [value]=\"draftPriority()\" (valueChange)=\"draftPriority.set($any($event))\">\n @for (p of priorities; track p) { <option [value]=\"p\">{{ text.priorities[p] }}</option> }\n </hm-select>\n <div class=\"as-field__dates\">\n <hm-date-picker [label]=\"text.startDate\" [value]=\"draftStart()\" [max]=\"$any(draftDue())\" (valueChange)=\"draftStart.set($event)\" />\n <hm-date-picker [label]=\"text.dueDate\" [value]=\"draftDue()\" [min]=\"$any(draftStart())\" (valueChange)=\"draftDue.set($event)\" />\n </div>\n <div class=\"as-field\">\n <span class=\"as-field__label\">{{ text.labels }}</span>\n <spine-suggestion-label-picker [labels]=\"labels()\" [selected]=\"draftLabels()\"\n (toggled)=\"toggleLabel($event.label, $event.linked)\" (created)=\"createLabel($event.name, $event.color)\" />\n </div>\n <hm-checkbox [label]=\"text.assignMeOnCreate\" [checked]=\"draftAssignMe()\" (checkedChange)=\"draftAssignMe.set($event)\" />\n </aside>\n </div>\n }\n\n <div footer class=\"as-task__footer\">\n @if (!current() && draft()) {\n <hm-button emphasis=\"medium\" (clicked)=\"closed.emit()\">{{ text.cancel }}</hm-button>\n <hm-button icon=\"plus\" [loading]=\"busy()\" (clicked)=\"create()\">{{ text.createTask }}</hm-button>\n } @else {\n <hm-button emphasis=\"medium\" (clicked)=\"closed.emit()\">{{ text.close }}</hm-button>\n }\n </div>\n</hm-modal>\n", styles: [".as-stack{display:flex;flex-direction:column;gap:var(--hm-space-md);min-inline-size:0}.as-actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--hm-space-xs)}.as-field-row{display:flex;flex-wrap:wrap;align-items:flex-end;gap:var(--hm-space-sm)}.as-field-row>hm-combobox{flex:1 1 16rem;min-inline-size:0}.as-summary{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-sm)}.as-summary__info{display:flex;flex:1 1 16rem;flex-direction:column;gap:var(--hm-space-micro);min-inline-size:0}.as-summary__title{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs);margin:0;color:var(--hm-text);font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-semibold)}.as-summary__detail{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.muted{color:var(--hm-muted)}.as-empty{margin:var(--hm-space-xs) 0 0;font-size:var(--hm-font-size-sm)}.as-secret{display:grid;gap:var(--hm-space-micro)}.as-secret__label{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.as-secret__row{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.as-secret__value{flex:1 1 240px;min-width:0;border:var(--hm-border-hairline) solid var(--hm-divider);border-radius:var(--hm-radius-control);padding:var(--hm-space-xs) var(--hm-space-sm);background:var(--hm-surface-muted);color:var(--hm-text);font-family:var(--hm-font-mono);font-size:var(--hm-font-size-sm);overflow-wrap:anywhere;-webkit-user-select:all;user-select:all}.as-tok--cat-1{--as-tok: var(--hm-data-categorical-1);background:var(--as-tok)}.as-tok--cat-2{--as-tok: var(--hm-data-categorical-2);background:var(--as-tok)}.as-tok--cat-3{--as-tok: var(--hm-data-categorical-3);background:var(--as-tok)}.as-tok--cat-4{--as-tok: var(--hm-data-categorical-4);background:var(--as-tok)}.as-tok--cat-5{--as-tok: var(--hm-data-categorical-5);background:var(--as-tok)}.as-tok--cat-6{--as-tok: var(--hm-data-categorical-6);background:var(--as-tok)}.as-tok--cat-7{--as-tok: var(--hm-data-categorical-7);background:var(--as-tok)}.as-tok--cat-8{--as-tok: var(--hm-data-categorical-8);background:var(--as-tok)}.as-tok--grad-1{--as-tok: var(--hm-data-categorical-1);background:linear-gradient(135deg,var(--hm-data-categorical-1),var(--hm-data-categorical-2))}.as-tok--grad-2{--as-tok: var(--hm-data-categorical-3);background:linear-gradient(135deg,var(--hm-data-categorical-3),var(--hm-data-categorical-4))}.as-tok--grad-3{--as-tok: var(--hm-data-categorical-5);background:linear-gradient(135deg,var(--hm-data-categorical-5),var(--hm-data-categorical-6))}.as-tok--grad-4{--as-tok: var(--hm-data-categorical-7);background:linear-gradient(135deg,var(--hm-data-categorical-7),var(--hm-data-categorical-8))}.as-tok--grad-5{--as-tok: var(--hm-data-categorical-1);background:linear-gradient(135deg,var(--hm-data-categorical-1),var(--hm-data-categorical-5))}.as-tok--grad-6{--as-tok: var(--hm-data-categorical-2);background:linear-gradient(135deg,var(--hm-data-categorical-2),var(--hm-data-categorical-6))}.as-label{display:inline-flex;align-items:center;gap:var(--hm-space-micro);max-inline-size:100%;padding:0 var(--hm-space-xxxs);border-radius:var(--hm-radius-pill);background:color-mix(in srgb,var(--as-tok, var(--hm-border)) 18%,var(--hm-surface-raised));color:var(--hm-text);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold);line-height:1.6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.as-label:before{flex:none;inline-size:.5rem;block-size:.5rem;border-radius:50%;background:var(--as-tok, var(--hm-border));content:\"\"}@media(forced-colors:active){.as-label{border:var(--hm-border-hairline) solid CanvasText}}.as-task{display:grid;grid-template-columns:minmax(0,1fr) minmax(15rem,20rem);gap:var(--hm-space-lg);align-items:start}@media(max-width:48rem){.as-task{grid-template-columns:minmax(0,1fr)}}.as-task__cover{block-size:3rem;margin-block-end:var(--hm-space-sm);border-radius:var(--hm-radius-control)}.as-task__main{display:flex;flex-direction:column;gap:var(--hm-space-md);min-inline-size:0}.as-task__key-row{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.as-task__key{color:var(--hm-muted);font-family:var(--hm-font-mono, monospace);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.as-task__title{margin:0;font-size:var(--hm-font-size-xl, 1.375rem)}.as-task__title-button{display:inline-flex;align-items:baseline;gap:var(--hm-space-xs);padding:0;border:0;background:none;color:var(--hm-text);font:inherit;font-weight:var(--hm-font-weight-bold);text-align:start;overflow-wrap:anywhere;cursor:text}.as-task__title-button:hover .as-task__title-pen,.as-task__title-button:focus-visible .as-task__title-pen{opacity:1}.as-task__title-button:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color);outline-offset:2px}.as-task__title-pen{color:var(--hm-muted);opacity:0;font-size:var(--hm-font-size-md)}.as-task__section{display:flex;flex-direction:column;gap:var(--hm-space-xs);padding:var(--hm-space-sm);border-radius:var(--hm-radius-control);background:var(--hm-surface-muted)}.as-task__section-head{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--hm-space-xs)}.as-task__heading{display:flex;align-items:center;gap:var(--hm-space-xs);margin:0;color:var(--hm-text);font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-semibold)}.as-task__count{color:var(--hm-muted);font-size:var(--hm-font-size-sm);font-variant-numeric:tabular-nums}.as-task__progress{block-size:.375rem;border-radius:var(--hm-radius-pill);background:var(--hm-border);overflow:hidden}.as-task__progress span{display:block;block-size:100%;background:var(--hm-success)}.as-task__reply{display:flex;flex-direction:column;gap:var(--hm-space-xs);margin-block:var(--hm-space-sm)}.as-task__reply-row{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--hm-space-xs)}.as-task__side{display:flex;flex-direction:column;gap:var(--hm-space-sm);padding:var(--hm-space-sm);border-radius:var(--hm-radius-control);background:var(--hm-surface-muted)}.as-task__danger{display:flex;flex-wrap:wrap;gap:var(--hm-space-xs);padding-block-start:var(--hm-space-xs);border-block-start:var(--hm-border-hairline) solid var(--hm-border)}.as-task__footer{display:flex;justify-content:flex-end;gap:var(--hm-space-xs)}.as-field{display:flex;flex-direction:column;gap:var(--hm-space-micro)}.as-field__head{display:flex;align-items:center;justify-content:space-between;gap:var(--hm-space-xs)}.as-field__label{color:var(--hm-muted);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.as-field__hint{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.as-field__chips{display:flex;flex-wrap:wrap;gap:var(--hm-space-micro)}.as-field__dates{display:grid;grid-template-columns:1fr 1fr;gap:var(--hm-space-xs)}.as-field__cover{display:block;block-size:1.5rem;border-radius:var(--hm-radius-control)}.as-people{display:flex;flex-direction:column;gap:var(--hm-space-micro);margin:0;padding:0;list-style:none}.as-people__item{display:flex;align-items:center;gap:var(--hm-space-xs);min-inline-size:0}.as-people__name{flex:1;min-inline-size:0;overflow:hidden;color:var(--hm-text);font-size:var(--hm-font-size-sm);text-overflow:ellipsis;white-space:nowrap}.as-task__merged{display:flex;flex-wrap:wrap;align-items:flex-start;gap:var(--hm-space-xs) var(--hm-space-sm);padding:var(--hm-space-xs) var(--hm-space-sm);border-inline-start:var(--hm-border-thin) solid var(--hm-info);border-radius:var(--hm-radius-control);background:var(--hm-info-bg);color:var(--hm-text)}.as-task__merged-icon{flex:none;color:var(--hm-info)}.as-task__merged-text{display:flex;flex:1 1 12rem;flex-direction:column;gap:var(--hm-space-micro);min-inline-size:0;overflow-wrap:anywhere}.as-merged{display:flex;flex-direction:column;gap:var(--hm-space-micro);margin:0;padding:0;list-style:none}.as-merged__item{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:var(--hm-space-micro) var(--hm-space-xs);padding:var(--hm-space-xxxs) var(--hm-space-xs);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface-raised)}.as-merged__item hm-icon-button{grid-row:1/span 2;grid-column:2}.as-merged__link{padding:0;border:0;background:none;color:var(--hm-accent);font:inherit;font-size:var(--hm-font-size-sm);text-align:start;overflow-wrap:anywhere;cursor:pointer}.as-merged__link:hover{text-decoration:underline}.as-merged__link:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color);outline-offset:2px}.as-merged__author{display:inline-flex;align-items:center;gap:var(--hm-space-micro);color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.as-due{display:inline-flex;align-items:center;gap:var(--hm-space-micro);color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.as-due[data-due=overdue]{color:var(--hm-danger);font-weight:var(--hm-font-weight-semibold)}.as-due[data-due=today],.as-due[data-due=tomorrow]{color:var(--hm-text);font-weight:var(--hm-font-weight-semibold)}.as-facts{display:grid;grid-template-columns:max-content 1fr;gap:var(--hm-space-xxxs) var(--hm-space-sm);margin:0;font-size:var(--hm-font-size-sm)}.as-facts dt{color:var(--hm-muted)}.as-facts dd{margin:0;color:var(--hm-text);overflow-wrap:anywhere}.as-facts__link{display:inline-flex;align-items:center;gap:var(--hm-space-micro);margin-inline-start:var(--hm-space-xxxs);color:var(--hm-accent)}.as-checklist{display:flex;flex-direction:column;gap:var(--hm-space-micro);margin:0;padding:0;list-style:none}.as-checklist__item{display:flex;align-items:center;gap:var(--hm-space-xs);min-block-size:var(--hm-control-height-dense)}.as-checklist__item--done .as-checklist__check{color:var(--hm-muted);text-decoration:line-through}.as-checklist__item:hover .as-checklist__tools,.as-checklist__item:focus-within .as-checklist__tools{opacity:1}.as-checklist__check{flex:1;min-inline-size:0}.as-checklist__rename{flex:1}.as-checklist__tools{display:inline-flex;opacity:0}.as-checklist__add{display:flex;gap:var(--hm-space-xs);align-items:flex-start}.as-checklist__new{flex:1}@media(hover:none){.as-checklist__tools{opacity:1}}.as-timeline{display:flex;flex-direction:column;gap:var(--hm-space-sm);margin:var(--hm-space-sm) 0 0;padding:0;list-style:none}.as-timeline__item{position:relative;display:flex;align-items:flex-start;gap:var(--hm-space-xs);font-size:var(--hm-font-size-sm)}.as-timeline__item:not(:last-child):before{position:absolute;inset-block:var(--hm-space-lg) calc(var(--hm-space-sm) * -1);inset-inline-start:calc(var(--hm-space-sm) - var(--hm-border-hairline));border-inline-start:var(--hm-border-thin) solid var(--hm-border);content:\"\"}.as-timeline__face{position:relative;flex:none}.as-timeline__body{display:flex;flex-direction:column;gap:var(--hm-space-micro);min-inline-size:0}.as-timeline__line{display:inline-flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-micro);color:var(--hm-text)}.as-timeline__comment{margin:0;padding:var(--hm-space-xs) var(--hm-space-sm);border-radius:var(--hm-radius-control);background:var(--hm-surface-muted);color:var(--hm-text);white-space:pre-line;overflow-wrap:anywhere}.as-timeline__comment--internal{border-inline-start:var(--hm-border-thin) solid var(--hm-warning);background:var(--hm-warning-bg)}\n"] }]
1083
+ }], ctorParameters: () => [], propDecorators: { clientId: [{ type: i0.Input, args: [{ isSignal: true, alias: "clientId", required: true }] }], task: [{ type: i0.Input, args: [{ isSignal: true, alias: "task", required: false }] }], draft: [{ type: i0.Input, args: [{ isSignal: true, alias: "draft", required: false }] }], draftStatus: [{ type: i0.Input, args: [{ isSignal: true, alias: "draftStatus", required: false }] }], labels: [{ type: i0.Input, args: [{ isSignal: true, alias: "labels", required: false }] }], canOpenScreen: [{ type: i0.Input, args: [{ isSignal: true, alias: "canOpenScreen", required: false }] }], changed: [{ type: i0.Output, args: ["changed"] }], created: [{ type: i0.Output, args: ["created"] }], removed: [{ type: i0.Output, args: ["removed"] }], closed: [{ type: i0.Output, args: ["closed"] }], labelCreated: [{ type: i0.Output, args: ["labelCreated"] }], mergeRequested: [{ type: i0.Output, args: ["mergeRequested"] }], openTask: [{ type: i0.Output, args: ["openTask"] }], unmerged: [{ type: i0.Output, args: ["unmerged"] }] } });
1084
+
1085
+ /** Sem acento e sem caixa: "relatório" acha "RELATORIO". */
1086
+ const fold$1 = (s) => s.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase();
1087
+ /** Teto da lista do seletor: a busca afunila o resto. */
1088
+ const MAX_OPTIONS = 50;
1089
+ /**
1090
+ * "Agrupar em…": escolhe a sugestão principal para uma repetida (`source`), entre as abertas do quadro (`candidates`,
1091
+ * menos ela e as já agrupadas), com busca pela chave e pelo título. Escolher pede a prévia ao servidor ("3 apoios e 1
1092
+ * relator passam para SUG-12"); "Agrupar" confirma (`HermesDialogService.confirm`) e grava. Uma sugestão que já recebeu
1093
+ * outras não pode ser agrupada (sem cadeia): o seletor avisa em vez de listar.
1094
+ */
1095
+ class SuggestionMergePickerComponent {
1096
+ constructor() {
1097
+ this.messages = inject(SPINE_MESSAGES);
1098
+ this.text = this.messages.suggestions;
1099
+ this.service = inject(AutoServiceSuggestionsService);
1100
+ this.dialogs = inject(HermesDialogService);
1101
+ this.toast = inject(HermesToastService);
1102
+ this.clientId = input.required(...(ngDevMode ? [{ debugName: "clientId" }] : /* istanbul ignore next */ []));
1103
+ /** A que vai ser agrupada; null = fechado. */
1104
+ this.source = input(null, ...(ngDevMode ? [{ debugName: "source" }] : /* istanbul ignore next */ []));
1105
+ /** As do quadro (o seletor tira a própria e as agrupadas). */
1106
+ this.candidates = input([], ...(ngDevMode ? [{ debugName: "candidates" }] : /* istanbul ignore next */ []));
1107
+ /** Agrupou: o destino inteiro, como o servidor devolveu. */
1108
+ this.merged = output();
1109
+ this.closed = output();
1110
+ this.statusColors = SPINE_SUGGESTION_STATUS_COLORS;
1111
+ this.term = signal('', ...(ngDevMode ? [{ debugName: "term" }] : /* istanbul ignore next */ []));
1112
+ this.picked = signal(null, ...(ngDevMode ? [{ debugName: "picked" }] : /* istanbul ignore next */ []));
1113
+ this.preview = signal(null, ...(ngDevMode ? [{ debugName: "preview" }] : /* istanbul ignore next */ []));
1114
+ this.previewError = signal(false, ...(ngDevMode ? [{ debugName: "previewError" }] : /* istanbul ignore next */ []));
1115
+ this.busy = signal(false, ...(ngDevMode ? [{ debugName: "busy" }] : /* istanbul ignore next */ []));
1116
+ this.heading = computed(() => formatMessage(this.text.mergeHeading, { key: this.source()?.key ?? '' }), ...(ngDevMode ? [{ debugName: "heading" }] : /* istanbul ignore next */ []));
1117
+ this.options = computed(() => {
1118
+ const source = this.source();
1119
+ const term = fold$1(this.term().trim());
1120
+ return this.candidates()
1121
+ .filter((c) => c.id !== source?.id && !c.merged_into)
1122
+ .filter((c) => !term || fold$1(`${c.key} ${c.title}`).includes(term))
1123
+ .slice(0, MAX_OPTIONS);
1124
+ }, ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
1125
+ // Cada abertura começa do zero.
1126
+ effect(() => {
1127
+ this.source();
1128
+ untracked(() => {
1129
+ this.term.set('');
1130
+ this.picked.set(null);
1131
+ this.preview.set(null);
1132
+ this.previewError.set(false);
1133
+ });
1134
+ });
1135
+ }
1136
+ onOpen(open) {
1137
+ if (!open)
1138
+ this.closed.emit();
1139
+ }
1140
+ async pick(target) {
1141
+ const source = this.source();
1142
+ if (!source || this.picked()?.id === target.id)
1143
+ return;
1144
+ this.picked.set(target);
1145
+ this.preview.set(null);
1146
+ this.previewError.set(false);
1147
+ try {
1148
+ const preview = await this.service.mergePreview(this.clientId(), source.id, target.id);
1149
+ if (this.picked()?.id === target.id)
1150
+ this.preview.set(preview);
1151
+ }
1152
+ catch {
1153
+ if (this.picked()?.id === target.id)
1154
+ this.previewError.set(true);
1155
+ }
1156
+ }
1157
+ previewText(p) {
1158
+ const locale = this.messages.meta.locale;
1159
+ return formatMessage(this.text.mergePreview, {
1160
+ votes: pluralMessage(this.text.votes, p.votes, locale),
1161
+ reporters: pluralMessage(this.text.reportersCount, p.reporters, locale),
1162
+ key: p.target.key,
1163
+ });
1164
+ }
1165
+ votesText(count) {
1166
+ return pluralMessage(this.text.votes, count, this.messages.meta.locale);
1167
+ }
1168
+ async confirm() {
1169
+ const source = this.source();
1170
+ const preview = this.preview();
1171
+ const target = this.picked();
1172
+ if (!source || !preview || !target || this.busy())
1173
+ return;
1174
+ const ok = await this.dialogs.confirm({
1175
+ heading: formatMessage(this.text.mergeConfirmHeading, { key: source.key, target: target.key }),
1176
+ message: `${this.previewText(preview)} ${this.text.mergeConfirmMessage}`,
1177
+ confirmLabel: this.text.mergeConfirm,
1178
+ });
1179
+ if (!ok)
1180
+ return;
1181
+ this.busy.set(true);
1182
+ try {
1183
+ const saved = await this.service.merge(this.clientId(), source.id, target.id);
1184
+ this.toast.success(formatMessage(this.text.mergedToast, { key: source.key, target: target.key }));
1185
+ this.merged.emit({ source, target: saved });
1186
+ }
1187
+ catch {
1188
+ this.toast.error(this.text.mergeError);
1189
+ }
1190
+ finally {
1191
+ this.busy.set(false);
1192
+ }
1193
+ }
1194
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionMergePickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1195
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: SuggestionMergePickerComponent, isStandalone: true, selector: "spine-suggestion-merge-picker", inputs: { clientId: { classPropertyName: "clientId", publicName: "clientId", isSignal: true, isRequired: true, transformFunction: null }, source: { classPropertyName: "source", publicName: "source", isSignal: true, isRequired: false, transformFunction: null }, candidates: { classPropertyName: "candidates", publicName: "candidates", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { merged: "merged", closed: "closed" }, ngImport: i0, template: `
1196
+ <hm-modal [heading]="heading()" [open]="!!source()" (openChange)="onOpen($event)">
1197
+ @if (source(); as s) {
1198
+ <div class="as-merge">
1199
+ <p class="as-merge__intro">{{ text.mergeIntro }}</p>
1200
+ @if (s.merged_count) {
1201
+ <hm-alert variant="warning">{{ text.mergeHasMerged }}</hm-alert>
1202
+ } @else {
1203
+ <hm-search-field [label]="text.mergeSearch" labelHidden [placeholder]="text.mergeSearch" [landmark]="false"
1204
+ [value]="term()" (search)="term.set($event)" />
1205
+ <ul class="as-merge__list" [attr.aria-label]="text.mergeSearch">
1206
+ @for (c of options(); track c.id) {
1207
+ <li>
1208
+ <button type="button" class="as-merge__option" [class.as-merge__option--on]="picked()?.id === c.id"
1209
+ [attr.aria-pressed]="picked()?.id === c.id" (click)="pick(c)">
1210
+ <span class="as-merge__key">{{ c.key }}</span>
1211
+ <span class="as-merge__title">{{ c.title }}</span>
1212
+ <span class="as-merge__meta">
1213
+ <hm-tag [name]="text.statuses[c.status]" [color]="statusColors[c.status]" />
1214
+ <span>{{ votesText(c.votes) }}</span>
1215
+ </span>
1216
+ </button>
1217
+ </li>
1218
+ } @empty {
1219
+ <li class="as-merge__empty">{{ text.mergeNone }}</li>
1220
+ }
1221
+ </ul>
1222
+ @if (picked()) {
1223
+ <p class="as-merge__preview" aria-live="polite">
1224
+ @if (previewError()) { {{ text.mergePreviewError }} }
1225
+ @else if (preview(); as p) { {{ previewText(p) }} }
1226
+ @else { {{ text.mergeCalculating }} }
1227
+ </p>
1228
+ }
1229
+ }
1230
+ </div>
1231
+ }
1232
+ <div footer class="as-merge__footer">
1233
+ <hm-button emphasis="medium" (clicked)="closed.emit()">{{ text.cancel }}</hm-button>
1234
+ <hm-button icon="layers" [disabled]="!preview() || !!source()?.merged_count" [loading]="busy()" (clicked)="confirm()">
1235
+ {{ text.mergeConfirm }}
1236
+ </hm-button>
1237
+ </div>
1238
+ </hm-modal>
1239
+ `, isInline: true, styles: [".as-merge{display:flex;flex-direction:column;gap:var(--hm-space-sm);min-inline-size:0}.as-merge__intro,.as-merge__empty{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.as-merge__list{display:flex;flex-direction:column;gap:var(--hm-space-xxxs);max-block-size:22rem;margin:0;padding:0;overflow:auto;list-style:none}.as-merge__option{display:grid;grid-template-columns:auto 1fr;gap:var(--hm-space-micro) var(--hm-space-xs);inline-size:100%;padding:var(--hm-space-xs) var(--hm-space-sm);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface-raised);color:var(--hm-text);font:inherit;text-align:start;cursor:pointer}.as-merge__option:hover{background:var(--hm-surface-muted)}.as-merge__option:focus-visible{outline:var(--hm-border-thin) solid var(--hm-accent);outline-offset:var(--hm-border-thin)}.as-merge__option--on{border-color:var(--hm-accent);background:var(--hm-accent-subtle)}.as-merge__key{color:var(--hm-muted);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold)}.as-merge__title{font-weight:var(--hm-font-weight-semibold);overflow-wrap:anywhere}.as-merge__meta{display:inline-flex;grid-column:2;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xxxs);color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.as-merge__preview{margin:0;padding:var(--hm-space-xs) var(--hm-space-sm);border-radius:var(--hm-radius-control);background:var(--hm-surface-muted);color:var(--hm-text)}.as-merge__footer{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--hm-space-xs);inline-size:100%}\n"], dependencies: [{ kind: "component", type: AlertComponent, selector: "hm-alert", inputs: ["variant", "appearance", "heading", "closable", "size", "showIcon", "link", "linkLabel", "linkTarget", "dismissKey", "duration"], outputs: ["closed"] }, { kind: "component", type: ButtonComponent, selector: "hm-button", inputs: ["emphasis", "intent", "size", "type", "icon", "iconDirection", "iconEnd", "disabled", "loading", "loadingState", "block", "link", "newTab", "caret", "inMenu", "toggle", "pressed"], outputs: ["loadingStateChange", "pressedChange", "clicked"] }, { kind: "component", type: ModalComponent, selector: "hm-modal", inputs: ["open", "heading", "size", "closeLabel", "closable", "beforeClose"], outputs: ["openChange", "closed"] }, { kind: "component", type: SearchFieldComponent, selector: "hm-search-field", inputs: ["value", "label", "labelHidden", "placeholder", "hint", "size", "debounce", "landmark", "loading"], outputs: ["valueChange", "search", "cleared"] }, { kind: "component", type: TagComponent, selector: "hm-tag", inputs: ["name", "color", "compact"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1240
+ }
1241
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SuggestionMergePickerComponent, decorators: [{
1242
+ type: Component,
1243
+ args: [{ selector: 'spine-suggestion-merge-picker', imports: [AlertComponent, ButtonComponent, ModalComponent, SearchFieldComponent, TagComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: `
1244
+ <hm-modal [heading]="heading()" [open]="!!source()" (openChange)="onOpen($event)">
1245
+ @if (source(); as s) {
1246
+ <div class="as-merge">
1247
+ <p class="as-merge__intro">{{ text.mergeIntro }}</p>
1248
+ @if (s.merged_count) {
1249
+ <hm-alert variant="warning">{{ text.mergeHasMerged }}</hm-alert>
1250
+ } @else {
1251
+ <hm-search-field [label]="text.mergeSearch" labelHidden [placeholder]="text.mergeSearch" [landmark]="false"
1252
+ [value]="term()" (search)="term.set($event)" />
1253
+ <ul class="as-merge__list" [attr.aria-label]="text.mergeSearch">
1254
+ @for (c of options(); track c.id) {
1255
+ <li>
1256
+ <button type="button" class="as-merge__option" [class.as-merge__option--on]="picked()?.id === c.id"
1257
+ [attr.aria-pressed]="picked()?.id === c.id" (click)="pick(c)">
1258
+ <span class="as-merge__key">{{ c.key }}</span>
1259
+ <span class="as-merge__title">{{ c.title }}</span>
1260
+ <span class="as-merge__meta">
1261
+ <hm-tag [name]="text.statuses[c.status]" [color]="statusColors[c.status]" />
1262
+ <span>{{ votesText(c.votes) }}</span>
1263
+ </span>
1264
+ </button>
1265
+ </li>
1266
+ } @empty {
1267
+ <li class="as-merge__empty">{{ text.mergeNone }}</li>
1268
+ }
1269
+ </ul>
1270
+ @if (picked()) {
1271
+ <p class="as-merge__preview" aria-live="polite">
1272
+ @if (previewError()) { {{ text.mergePreviewError }} }
1273
+ @else if (preview(); as p) { {{ previewText(p) }} }
1274
+ @else { {{ text.mergeCalculating }} }
1275
+ </p>
1276
+ }
1277
+ }
1278
+ </div>
1279
+ }
1280
+ <div footer class="as-merge__footer">
1281
+ <hm-button emphasis="medium" (clicked)="closed.emit()">{{ text.cancel }}</hm-button>
1282
+ <hm-button icon="layers" [disabled]="!preview() || !!source()?.merged_count" [loading]="busy()" (clicked)="confirm()">
1283
+ {{ text.mergeConfirm }}
1284
+ </hm-button>
1285
+ </div>
1286
+ </hm-modal>
1287
+ `, styles: [".as-merge{display:flex;flex-direction:column;gap:var(--hm-space-sm);min-inline-size:0}.as-merge__intro,.as-merge__empty{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.as-merge__list{display:flex;flex-direction:column;gap:var(--hm-space-xxxs);max-block-size:22rem;margin:0;padding:0;overflow:auto;list-style:none}.as-merge__option{display:grid;grid-template-columns:auto 1fr;gap:var(--hm-space-micro) var(--hm-space-xs);inline-size:100%;padding:var(--hm-space-xs) var(--hm-space-sm);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface-raised);color:var(--hm-text);font:inherit;text-align:start;cursor:pointer}.as-merge__option:hover{background:var(--hm-surface-muted)}.as-merge__option:focus-visible{outline:var(--hm-border-thin) solid var(--hm-accent);outline-offset:var(--hm-border-thin)}.as-merge__option--on{border-color:var(--hm-accent);background:var(--hm-accent-subtle)}.as-merge__key{color:var(--hm-muted);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold)}.as-merge__title{font-weight:var(--hm-font-weight-semibold);overflow-wrap:anywhere}.as-merge__meta{display:inline-flex;grid-column:2;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xxxs);color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.as-merge__preview{margin:0;padding:var(--hm-space-xs) var(--hm-space-sm);border-radius:var(--hm-radius-control);background:var(--hm-surface-muted);color:var(--hm-text)}.as-merge__footer{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--hm-space-xs);inline-size:100%}\n"] }]
1288
+ }], ctorParameters: () => [], propDecorators: { clientId: [{ type: i0.Input, args: [{ isSignal: true, alias: "clientId", required: true }] }], source: [{ type: i0.Input, args: [{ isSignal: true, alias: "source", required: false }] }], candidates: [{ type: i0.Input, args: [{ isSignal: true, alias: "candidates", required: false }] }], merged: [{ type: i0.Output, args: ["merged"] }], closed: [{ type: i0.Output, args: ["closed"] }] } });
1289
+
1290
+ /** As colunas do quadro: recusada não tem coluna (sai na hora; fica no arquivo). */
1291
+ const BOARD_STATUSES = ['em_analise', 'planejada', 'em_desenvolvimento', 'concluida'];
1292
+ const OPEN_STATUSES = ['em_analise', 'planejada', 'em_desenvolvimento'];
1293
+ /** Quantas levam o selo "Mais votada" (das com algum apoio, no quadro inteiro, não só no filtrado). */
1294
+ const TOP_VOTED = 3;
1295
+ /** Responsáveis que aparecem no cartão; o resto vira "+N". */
1296
+ const FACES = 3;
1297
+ const ARCHIVE_PAGE = 20;
1298
+ const EXCERPT = 140;
1299
+ /** Parâmetro do link copiado ("?sugestao=SUG-12"): abrir a tela com ele abre a tarefa. */
1300
+ const SUGGESTION_LINK_PARAM = 'sugestao';
1301
+ const PRIORITY_RANK = { urgente: 0, alta: 1, media: 2, baixa: 3 };
1302
+ const byPosition = (a, b) => a.position - b.position || a.created_at.localeCompare(b.created_at);
1303
+ const SORTS = {
1304
+ position: byPosition,
1305
+ votes: (a, b) => b.votes - a.votes || byPosition(a, b),
1306
+ recent: (a, b) => b.created_at.localeCompare(a.created_at),
1307
+ priority: (a, b) => PRIORITY_RANK[a.priority] - PRIORITY_RANK[b.priority] || byPosition(a, b),
1308
+ due: (a, b) => (a.due_date ?? '9999').localeCompare(b.due_date ?? '9999') || byPosition(a, b),
1309
+ };
1310
+ /** Sem acento e minúsculo: "analise" acha "Análise". */
1311
+ const fold = (s) => s.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase();
1312
+ /** Intervalos do "há 3 dias" (segundos por unidade), do maior para o menor. */
1313
+ const AGES = [
1314
+ ['year', 31_536_000], ['month', 2_592_000], ['week', 604_800], ['day', 86_400], ['hour', 3_600], ['minute', 60],
1315
+ ];
1316
+ /**
1317
+ * O quadro de trabalho das sugestões do app (`hm-kanban`), montado na tela Sugestões do auto-serviço.
1318
+ *
1319
+ * - **Quadro**: o servidor manda o recorte (`board=true`): abertas e as concluídas dos últimos `done_days` dias;
1320
+ * recusadas e arquivadas saem. Indicadores do quadro inteiro, tudo calculado aqui com o que já vem (vencidas e as
1321
+ * que venceram ontem, vencendo em breve, em andamento por status, concluídas na janela e hoje, atribuídas a mim e as
1322
+ * abertas por prioridade; vencidas, vencendo, minhas e cada prioridade também filtram) e os filtros da tela — texto
1323
+ * (inclusive a chave "SUG-12"), responsável ("minhas", "sem responsável"), tipo, status (só aquela coluna),
1324
+ * prioridade, rótulo, prazo, e em "Mais filtros" tela e quem pediu, além da ordenação — sobre a lista inteira.
1325
+ * - **Coluna**: "Mais antiga há 4h 12min" (pelo `status_at`) e as vencidas abaixo do título, vazio explicado e
1326
+ * "+ Criar tarefa" no rodapé (os templates `hmKanbanColumnSummary`/`Empty`/`Footer` do `hm-kanban`).
1327
+ * - **Cartão**: capa (token da paleta), chave, selo de prioridade, título, contexto (tipo · tela), rótulos, trecho,
1328
+ * checklist (e os itens, com "Mostrar no cartão"), prazo por extenso (vencida em vermelho; hoje/amanhã em âmbar) e o
1329
+ * rodapé com comentários, checklist, apoios, a data e os responsáveis com "+N"; o "⋮": status e prioridade
1330
+ * (submenus), atribuir a mim, copiar link e chave, rótulos, capa (popover), arquivar e excluir (com confirmação).
1331
+ * - **Criar**: sempre o cadastro completo (a tarefa em branco no modal), já com o status da coluna: "Nova tarefa" no
1332
+ * topo (o "▾" escolhe a coluna), o "+" do cabeçalho e o "+ Criar tarefa" do rodapé de cada coluna.
1333
+ * - **Arrastar** (ponteiro, toque ou teclado; o `hm-kanban` cuida do gesto e dos anúncios) muda status e/ou posição: a
1334
+ * tela aplica na hora e grava; se o servidor recusar, avisa e relê. A posição gravada é sempre a da coluna INTEIRA
1335
+ * (pelo vizinho visível). Fora da "ordem do quadro", arrastar só troca de coluna (vai para o fim).
1336
+ * - **Arquivo**: arquivadas, recusadas, agrupadas (selo "Agrupada", com "Desagrupar") e concluídas antigas, paginado e
1337
+ * buscado no servidor, com "Reabrir"/"Restaurar".
1338
+ * - **Agrupar**: "Agrupar em…" no "⋯" do cartão e no detalhe abre o seletor (`spine-suggestion-merge-picker`) com as
1339
+ * abertas do quadro e a prévia do servidor; confirmar agrupa, tira a origem do quadro e abre o destino.
1340
+ * - **Tarefa**: `spine-suggestion-task-detail` (modal de duas colunas); o link "?sugestao=SUG-12" abre direto nela.
1341
+ */
1342
+ class AutoServiceSuggestionsComponent {
1343
+ constructor() {
1344
+ this.messages = inject(SPINE_MESSAGES);
1345
+ this.text = this.messages.suggestions;
1346
+ this.service = inject(AutoServiceSuggestionsService);
1347
+ this.toast = inject(HermesToastService);
1348
+ this.dialogs = inject(HermesDialogService);
1349
+ this.preferences = inject(SpinePreferencesService);
1350
+ this.userStorage = inject(SpineUserStorage, { optional: true });
1351
+ this.router = inject(Router, { optional: true });
1352
+ this.location = inject(Location, { optional: true });
1353
+ /** O client da sessão: a tela da sugestão só abre daqui se o app administrado for este. */
1354
+ this.sessionClientId = inject(AUTH_CONFIG, { optional: true })?.clientId ?? null;
1355
+ this.clientId = input.required(...(ngDevMode ? [{ debugName: "clientId" }] : /* istanbul ignore next */ []));
1356
+ this.kinds = SPINE_SUGGESTION_KINDS;
1357
+ this.statuses = SPINE_SUGGESTION_STATUSES;
1358
+ this.statusColors = SPINE_SUGGESTION_STATUS_COLORS;
1359
+ this.statusIcons = SPINE_SUGGESTION_STATUS_ICONS;
1360
+ this.kindColors = SPINE_SUGGESTION_KIND_COLORS;
1361
+ this.priorities = TASK_PRIORITIES;
1362
+ this.sortKeys = ['position', 'votes', 'recent', 'priority', 'due'];
1363
+ this.today = todayIso(this.preferences.effectiveTimeZone());
1364
+ this.mode = signal('board', ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
1365
+ this.modeOptions = [
1366
+ { value: 'board', label: this.text.viewBoard, icon: 'view-columns' },
1367
+ { value: 'archive', label: this.text.viewArchive, icon: 'archive' },
1368
+ ];
1369
+ // --- quadro ---
1370
+ this.all = signal([], ...(ngDevMode ? [{ debugName: "all" }] : /* istanbul ignore next */ []));
1371
+ this.catalog = signal([], ...(ngDevMode ? [{ debugName: "catalog" }] : /* istanbul ignore next */ []));
1372
+ this.doneDays = signal(AUTO_SERVICE_SUGGESTION_DONE_DAYS, ...(ngDevMode ? [{ debugName: "doneDays" }] : /* istanbul ignore next */ []));
1373
+ this.loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
1374
+ this.loadError = signal('', ...(ngDevMode ? [{ debugName: "loadError" }] : /* istanbul ignore next */ []));
1375
+ this.search = signal('', ...(ngDevMode ? [{ debugName: "search" }] : /* istanbul ignore next */ []));
1376
+ this.kind = signal('', ...(ngDevMode ? [{ debugName: "kind" }] : /* istanbul ignore next */ []));
1377
+ this.screen = signal('', ...(ngDevMode ? [{ debugName: "screen" }] : /* istanbul ignore next */ []));
1378
+ this.author = signal('', ...(ngDevMode ? [{ debugName: "author" }] : /* istanbul ignore next */ []));
1379
+ /** '' | 'me' | 'none' | user_id */
1380
+ this.assignee = signal('', ...(ngDevMode ? [{ debugName: "assignee" }] : /* istanbul ignore next */ []));
1381
+ this.label = signal('', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
1382
+ this.priority = signal('', ...(ngDevMode ? [{ debugName: "priority" }] : /* istanbul ignore next */ []));
1383
+ /** '' | 'overdue' | 'soon' */
1384
+ this.due = signal('', ...(ngDevMode ? [{ debugName: "due" }] : /* istanbul ignore next */ []));
1385
+ /** '' | um status do quadro: só essa coluna aparece. */
1386
+ this.status = signal('', ...(ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []));
1387
+ this.sort = signal('position', ...(ngDevMode ? [{ debugName: "sort" }] : /* istanbul ignore next */ []));
1388
+ /** "Mais filtros" aberto (tela e quem pediu). */
1389
+ this.moreFilters = signal(false, ...(ngDevMode ? [{ debugName: "moreFilters" }] : /* istanbul ignore next */ []));
1390
+ this.boardStatuses = BOARD_STATUSES;
1391
+ /** Os filtros escondidos em "Mais filtros" que estão ligados (o botão mostra a conta). */
1392
+ this.extraFilters = computed(() => [this.screen(), this.author()].filter(Boolean).length, ...(ngDevMode ? [{ debugName: "extraFilters" }] : /* istanbul ignore next */ []));
1393
+ this.moreFiltersText = computed(() => (this.extraFilters()
1394
+ ? formatMessage(this.text.moreFiltersActive, { count: this.extraFilters() }) : this.text.moreFilters), ...(ngDevMode ? [{ debugName: "moreFiltersText" }] : /* istanbul ignore next */ []));
1395
+ this.me = computed(() => this.userStorage?.userId() ?? '', ...(ngDevMode ? [{ debugName: "me" }] : /* istanbul ignore next */ []));
1396
+ this.filtered = computed(() => {
1397
+ const term = fold(this.search().trim());
1398
+ const keyMatch = this.search().trim().match(/^(?:sug-?)?(\d{1,9})$/i);
1399
+ const [kind, screen, author, assignee, label, priority, due] = [this.kind(), this.screen(), this.author(), this.assignee(), this.label(), this.priority(), this.due()];
1400
+ const me = this.me();
1401
+ return this.all().filter((s) => (!kind || s.kind === kind)
1402
+ && (!screen || s.screen.path === screen)
1403
+ && (!author || s.author?.user_id === author)
1404
+ && (!assignee || (assignee === 'none' ? s.assignees.length === 0 : assignedTo(s, assignee === 'me' ? me : assignee)))
1405
+ && (!label || s.labels.some((l) => l.id === label))
1406
+ && (!priority || s.priority === priority)
1407
+ && (!due || this.matchesDue(s, due))
1408
+ && (!term || (keyMatch && s.key_number === Number(keyMatch[1]))
1409
+ || fold(`${s.title} ${s.description} ${s.screen.label ?? ''} ${this.personName(s.author)} ${s.key}`).includes(term)));
1410
+ }, ...(ngDevMode ? [{ debugName: "filtered" }] : /* istanbul ignore next */ []));
1411
+ this.isFiltered = computed(() => !!(this.search().trim() || this.kind() || this.screen() || this.author()
1412
+ || this.assignee() || this.label() || this.priority() || this.due() || this.status()), ...(ngDevMode ? [{ debugName: "isFiltered" }] : /* istanbul ignore next */ []));
1413
+ this.filteredText = computed(() => formatMessage(this.text.boardFiltered, {
1414
+ shown: this.columns().reduce((n, c) => n + c.items.length, 0), total: this.all().length,
1415
+ }), ...(ngDevMode ? [{ debugName: "filteredText" }] : /* istanbul ignore next */ []));
1416
+ this.columns = computed(() => {
1417
+ const list = this.filtered();
1418
+ const order = SORTS[this.sort()];
1419
+ const only = this.status();
1420
+ return BOARD_STATUSES.filter((status) => !only || status === only).map((status) => ({
1421
+ id: status, title: this.text.statuses[status], items: list.filter((s) => s.status === status).sort(order),
1422
+ }));
1423
+ }, ...(ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []));
1424
+ /**
1425
+ * Resumo de cada coluna (o que está à vista nela): há quanto tempo a mais antiga está no status ("Mais antiga há
1426
+ * 4h 12min", pelo `status_at`) e quantas vencidas.
1427
+ */
1428
+ this.columnSummaries = computed(() => {
1429
+ const now = Date.now();
1430
+ return new Map(this.columns().map((c) => {
1431
+ const since = c.items.map((s) => Date.parse(s.status_at ?? s.created_at)).filter((t) => !Number.isNaN(t));
1432
+ const oldest = since.length ? this.duration(now - Math.min(...since)) : '';
1433
+ const overdue = c.items.filter((s) => dueState(s, this.today) === 'overdue').length;
1434
+ return [c.id, {
1435
+ oldest: oldest ? formatMessage(this.text.columnOldest, { age: oldest }) : '',
1436
+ overdue: overdue ? pluralMessage(this.text.columnOverdue, overdue, this.messages.meta.locale) : '',
1437
+ }];
1438
+ }));
1439
+ }, ...(ngDevMode ? [{ debugName: "columnSummaries" }] : /* istanbul ignore next */ []));
1440
+ /** Telas, pessoas e responsáveis que aparecem no quadro, para os filtros (só o que existe). */
1441
+ this.screens = computed(() => {
1442
+ const byPath = new Map();
1443
+ for (const s of this.all())
1444
+ if (s.screen.path)
1445
+ byPath.set(s.screen.path, s.screen.label || s.screen.path);
1446
+ return [...byPath].map(([path, label]) => ({ path, label })).sort((a, b) => a.label.localeCompare(b.label));
1447
+ }, ...(ngDevMode ? [{ debugName: "screens" }] : /* istanbul ignore next */ []));
1448
+ this.authors = computed(() => this.people((s) => (s.author ? [s.author] : [])), ...(ngDevMode ? [{ debugName: "authors" }] : /* istanbul ignore next */ []));
1449
+ this.assigneeOptions = computed(() => this.people((s) => s.assignees), ...(ngDevMode ? [{ debugName: "assigneeOptions" }] : /* istanbul ignore next */ []));
1450
+ /** Indicadores do quadro inteiro (o filtro não muda: é o retrato do app). */
1451
+ this.stats = computed(() => {
1452
+ const list = this.all();
1453
+ const count = (status) => list.filter((s) => s.status === status).length;
1454
+ const open = list.filter((s) => OPEN_STATUSES.includes(s.status));
1455
+ const yesterday = addDays(this.today, -1);
1456
+ const zone = this.preferences.effectiveTimeZone();
1457
+ const overdue = open.filter((s) => dueState(s, this.today) === 'overdue');
1458
+ const done = list.filter((s) => s.status === 'concluida');
1459
+ return {
1460
+ open: open.length, em_analise: count('em_analise'), planejada: count('planejada'),
1461
+ em_desenvolvimento: count('em_desenvolvimento'), concluida: done.length,
1462
+ mine: open.filter((s) => assignedTo(s, this.me())).length,
1463
+ overdue: overdue.length,
1464
+ /** Venceram ontem: passaram a vencidas hoje (o "+N desde ontem"). */
1465
+ overdueNew: overdue.filter((s) => s.due_date === yesterday).length,
1466
+ soon: open.filter((s) => { const d = dueState(s, this.today); return d === 'today' || d === 'tomorrow'; }).length,
1467
+ doneToday: done.filter((s) => todayIso(zone, new Date(s.status_at ?? s.updated_at)) === this.today).length,
1468
+ byPriority: TASK_PRIORITIES.map((p) => ({ priority: p, count: open.filter((s) => s.priority === p).length })),
1469
+ };
1470
+ }, ...(ngDevMode ? [{ debugName: "stats" }] : /* istanbul ignore next */ []));
1471
+ this.doneLabel = computed(() => formatMessage(this.text.kpiDone, { days: this.doneDays() }), ...(ngDevMode ? [{ debugName: "doneLabel" }] : /* istanbul ignore next */ []));
1472
+ /** "Em análise 3 · Planejada 4 · Em desenvolvimento 5" no indicador das abertas. */
1473
+ this.openParts = computed(() => OPEN_STATUSES.map((st) => ({
1474
+ status: st,
1475
+ text: formatMessage(this.text.kpiStatusCount, { status: this.text.statuses[st], count: this.all().filter((s) => s.status === st).length }),
1476
+ })), ...(ngDevMode ? [{ debugName: "openParts" }] : /* istanbul ignore next */ []));
1477
+ this.overdueNote = computed(() => (this.stats().overdueNew
1478
+ ? formatMessage(this.text.kpiOverdueTrend, { count: this.stats().overdueNew }) : this.text.kpiOverdueSteady), ...(ngDevMode ? [{ debugName: "overdueNote" }] : /* istanbul ignore next */ []));
1479
+ this.doneNote = computed(() => (this.stats().doneToday
1480
+ ? formatMessage(this.text.kpiDoneToday, { count: this.stats().doneToday }) : this.text.kpiDoneNoneToday), ...(ngDevMode ? [{ debugName: "doneNote" }] : /* istanbul ignore next */ []));
1481
+ this.kpiLoading = computed(() => this.loading() && !this.all().length, ...(ngDevMode ? [{ debugName: "kpiLoading" }] : /* istanbul ignore next */ []));
1482
+ /** Itens do "▾" ao lado de "Nova tarefa": criar direto numa coluna. */
1483
+ this.newTaskItems = BOARD_STATUSES.map((st) => ({
1484
+ id: st, label: formatMessage(this.text.newTaskIn, { status: this.text.statuses[st] }), icon: SPINE_SUGGESTION_STATUS_ICONS[st],
1485
+ }));
1486
+ this.topIds = computed(() => new Set(this.all().filter((s) => s.votes > 0).sort(SORTS.votes).slice(0, TOP_VOTED).map((s) => s.id)), ...(ngDevMode ? [{ debugName: "topIds" }] : /* istanbul ignore next */ []));
1487
+ /** O menu "⋯" de cada cartão, montado uma vez por versão da tarefa (o menu aberto não perde o foco). */
1488
+ this.menus = computed(() => new Map(this.all().map((t) => [t.id, this.buildMenu(t)])), ...(ngDevMode ? [{ debugName: "menus" }] : /* istanbul ignore next */ []));
1489
+ this.itemKey = (s) => s.id;
1490
+ this.itemLabel = (s) => `${s.key} ${s.title}`;
1491
+ // --- capa pelo menu do cartão ---
1492
+ this.coverPop = viewChild('coverPop', ...(ngDevMode ? [{ debugName: "coverPop" }] : /* istanbul ignore next */ []));
1493
+ this.coverTarget = signal(null, ...(ngDevMode ? [{ debugName: "coverTarget" }] : /* istanbul ignore next */ []));
1494
+ // --- arquivo ---
1495
+ this.archived = signal([], ...(ngDevMode ? [{ debugName: "archived" }] : /* istanbul ignore next */ []));
1496
+ this.archiveTotal = signal(0, ...(ngDevMode ? [{ debugName: "archiveTotal" }] : /* istanbul ignore next */ []));
1497
+ this.archivePage = signal({ page: 1, size: ARCHIVE_PAGE }, ...(ngDevMode ? [{ debugName: "archivePage" }] : /* istanbul ignore next */ []));
1498
+ this.archiveSearch = signal('', ...(ngDevMode ? [{ debugName: "archiveSearch" }] : /* istanbul ignore next */ []));
1499
+ this.archiveKind = signal('', ...(ngDevMode ? [{ debugName: "archiveKind" }] : /* istanbul ignore next */ []));
1500
+ this.archiveStatus = signal('', ...(ngDevMode ? [{ debugName: "archiveStatus" }] : /* istanbul ignore next */ []));
1501
+ this.archiveLoading = signal(false, ...(ngDevMode ? [{ debugName: "archiveLoading" }] : /* istanbul ignore next */ []));
1502
+ this.archiveError = signal('', ...(ngDevMode ? [{ debugName: "archiveError" }] : /* istanbul ignore next */ []));
1503
+ this.reopening = signal(null, ...(ngDevMode ? [{ debugName: "reopening" }] : /* istanbul ignore next */ []));
1504
+ this.archiveCountText = computed(() => pluralMessage(this.text.archiveCount, this.archiveTotal(), this.messages.meta.locale), ...(ngDevMode ? [{ debugName: "archiveCountText" }] : /* istanbul ignore next */ []));
1505
+ this.archiveEmptyHint = computed(() => formatMessage(this.text.archiveEmptyHint, { days: this.doneDays() }), ...(ngDevMode ? [{ debugName: "archiveEmptyHint" }] : /* istanbul ignore next */ []));
1506
+ // --- tarefa aberta ---
1507
+ this.selected = signal(null, ...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
1508
+ this.drafting = signal(false, ...(ngDevMode ? [{ debugName: "drafting" }] : /* istanbul ignore next */ []));
1509
+ this.draftStatus = signal('em_analise', ...(ngDevMode ? [{ debugName: "draftStatus" }] : /* istanbul ignore next */ []));
1510
+ this.canOpenScreen = computed(() => !!this.sessionClientId && this.sessionClientId.toLowerCase() === this.clientId().toLowerCase(), ...(ngDevMode ? [{ debugName: "canOpenScreen" }] : /* istanbul ignore next */ []));
1511
+ this.linkHandled = false;
1512
+ // --- agrupar ---
1513
+ /** A que está sendo agrupada (o seletor aberto); o detalhe fecha antes (um modal por vez). */
1514
+ this.mergeSource = signal(null, ...(ngDevMode ? [{ debugName: "mergeSource" }] : /* istanbul ignore next */ []));
1515
+ effect(() => {
1516
+ const clientId = this.clientId();
1517
+ untracked(() => {
1518
+ this.selected.set(null);
1519
+ this.drafting.set(false);
1520
+ void this.loadLabels(clientId);
1521
+ if (this.mode() === 'archive')
1522
+ void this.loadArchive(1, clientId);
1523
+ else
1524
+ void this.load(clientId);
1525
+ });
1526
+ });
1527
+ }
1528
+ people(of) {
1529
+ const byId = new Map();
1530
+ for (const s of this.all())
1531
+ for (const p of of(s))
1532
+ byId.set(p.user_id, p);
1533
+ return [...byId.values()].sort((a, b) => this.personName(a).localeCompare(this.personName(b)));
1534
+ }
1535
+ matchesDue(s, due) {
1536
+ const state = dueState(s, this.today);
1537
+ return due === 'overdue' ? state === 'overdue' : state === 'today' || state === 'tomorrow';
1538
+ }
1539
+ setMode(value) {
1540
+ const mode = value === 'archive' ? 'archive' : 'board';
1541
+ if (mode === this.mode())
1542
+ return;
1543
+ this.mode.set(mode);
1544
+ // Cada troca relê: o que foi reaberto no arquivo aparece no quadro e vice-versa.
1545
+ if (mode === 'archive')
1546
+ void this.loadArchive(1);
1547
+ else
1548
+ void this.load();
1549
+ }
1550
+ refresh() {
1551
+ if (this.mode() === 'archive')
1552
+ void this.loadArchive(this.archivePage().page);
1553
+ else
1554
+ void this.load();
1555
+ }
1556
+ async load(clientId = this.clientId()) {
1557
+ this.loading.set(true);
1558
+ this.loadError.set('');
1559
+ try {
1560
+ const board = await this.service.board(clientId);
1561
+ this.all.set(board.suggestions);
1562
+ this.doneDays.set(board.doneDays);
1563
+ void this.openFromLink(clientId);
1564
+ }
1565
+ catch {
1566
+ this.loadError.set(this.text.loadError);
1567
+ }
1568
+ finally {
1569
+ this.loading.set(false);
1570
+ }
1571
+ }
1572
+ async loadLabels(clientId = this.clientId()) {
1573
+ try {
1574
+ this.catalog.set(await this.service.labels(clientId));
1575
+ }
1576
+ catch {
1577
+ this.catalog.set([]);
1578
+ }
1579
+ }
1580
+ /** "?sugestao=SUG-12" na URL: abre a tarefa (uma vez por tela). */
1581
+ async openFromLink(clientId) {
1582
+ if (this.linkHandled || !this.router)
1583
+ return;
1584
+ this.linkHandled = true;
1585
+ const key = this.router.parseUrl(this.router.url).queryParams[SUGGESTION_LINK_PARAM];
1586
+ if (typeof key !== 'string' || !key)
1587
+ return;
1588
+ const known = this.all().find((s) => s.key.toLowerCase() === key.toLowerCase());
1589
+ try {
1590
+ this.openDetail(known ?? (await this.service.lookup(clientId, key)));
1591
+ }
1592
+ catch {
1593
+ this.toast.error(this.text.loadError);
1594
+ }
1595
+ }
1596
+ clearFilters() {
1597
+ for (const s of [this.search, this.kind, this.screen, this.author, this.assignee, this.label, this.priority, this.due, this.status]) {
1598
+ s.set('');
1599
+ }
1600
+ }
1601
+ /** Indicador que filtra ("atribuídas a mim", "vencidas", "vencendo em breve"): liga/desliga o filtro equivalente. */
1602
+ toggleKpi(which, on) {
1603
+ if (which === 'mine')
1604
+ this.assignee.set(on ? 'me' : '');
1605
+ else
1606
+ this.due.set(on ? which : '');
1607
+ }
1608
+ /** Uma prioridade da legenda do indicador: filtra por ela (de novo, desliga). */
1609
+ togglePriority(p) {
1610
+ this.priority.update((current) => (current === p ? '' : p));
1611
+ }
1612
+ /** Largura de cada prioridade na barra empilhada (% das abertas). */
1613
+ priorityShare(count) {
1614
+ const total = this.stats().open;
1615
+ return total ? (count / total) * 100 : 0;
1616
+ }
1617
+ priorityPart(p, count) {
1618
+ return formatMessage(this.text.kpiPriorityPart, { priority: this.text.priorities[p], count });
1619
+ }
1620
+ /**
1621
+ * O quadro já moveu o cartão (na lista filtrada); aqui vira a posição na coluna INTEIRA — logo depois do vizinho de
1622
+ * cima que está visível (ou antes do de baixo; sozinho na coluna filtrada = fim) — aplica na lista e grava. Com
1623
+ * outra ordenação, só a coluna conta: vai para o fim dela, e reordenar na mesma coluna não grava.
1624
+ */
1625
+ async onMoved(event) {
1626
+ const item = event.item;
1627
+ const to = event.to;
1628
+ const changed = event.from !== to;
1629
+ let change;
1630
+ if (this.sort() !== 'position') {
1631
+ if (!changed) {
1632
+ this.all.update((list) => [...list]); // devolve o cartão ao lugar da ordenação
1633
+ return;
1634
+ }
1635
+ const end = this.all().filter((s) => s.status === to && s.id !== item.id).length;
1636
+ this.all.update((list) => list.map((s) => (s.id === item.id ? { ...s, status: to, position: end } : s)));
1637
+ change = { status: to };
1638
+ }
1639
+ else {
1640
+ const shown = event.columns.find((c) => c.id === to)?.items ?? [];
1641
+ const above = shown[event.toIndex - 1];
1642
+ const below = shown[event.toIndex + 1];
1643
+ const column = this.all().filter((s) => s.status === to && s.id !== item.id).sort(byPosition);
1644
+ let index = column.length;
1645
+ if (above)
1646
+ index = column.findIndex((s) => s.id === above.id) + 1;
1647
+ else if (below)
1648
+ index = Math.max(0, column.findIndex((s) => s.id === below.id));
1649
+ const order = [...column.slice(0, index).map((s) => s.id), item.id, ...column.slice(index).map((s) => s.id)];
1650
+ const position = new Map(order.map((id, i) => [id, i]));
1651
+ this.all.update((list) => list.map((s) => (position.has(s.id)
1652
+ ? { ...s, position: position.get(s.id), ...(s.id === item.id ? { status: to } : {}) } : s)));
1653
+ change = { ...(changed ? { status: to } : {}), position: index };
1654
+ }
1655
+ try {
1656
+ this.replace(await this.service.move(this.clientId(), item.id, change));
1657
+ }
1658
+ catch {
1659
+ this.toast.error(this.text.moveError);
1660
+ await this.load();
1661
+ }
1662
+ }
1663
+ // --- criação ---
1664
+ /**
1665
+ * Abre o cadastro completo (a tarefa em branco no modal) já na coluna escolhida: "Nova tarefa" do topo (Em análise),
1666
+ * o "▾" ao lado dele, o "+" do cabeçalho e o "+ Criar tarefa" do rodapé de cada coluna.
1667
+ */
1668
+ newTask(status = 'em_analise') {
1669
+ this.selected.set(null);
1670
+ this.draftStatus.set(BOARD_STATUSES.includes(status) ? status : 'em_analise');
1671
+ this.drafting.set(true);
1672
+ }
1673
+ /** Nome do "+" da coluna: "Nova tarefa em Planejada". */
1674
+ newTaskInLabel(status) {
1675
+ return formatMessage(this.text.newTaskIn, { status: this.text.statuses[status] ?? status });
1676
+ }
1677
+ // --- tarefa aberta ---
1678
+ openDetail(s) {
1679
+ this.drafting.set(false);
1680
+ this.selected.set(s);
1681
+ }
1682
+ closeDetail() {
1683
+ this.selected.set(null);
1684
+ this.drafting.set(false);
1685
+ if (this.mode() === 'archive')
1686
+ void this.loadArchive(this.archivePage().page);
1687
+ }
1688
+ onCreated(task) {
1689
+ if (this.mode() === 'board')
1690
+ this.all.update((list) => [...list, task]);
1691
+ this.drafting.set(false);
1692
+ this.selected.set(task);
1693
+ }
1694
+ onRemoved(task) {
1695
+ this.all.update((list) => list.filter((s) => s.id !== task.id));
1696
+ this.archived.update((list) => list.filter((s) => s.id !== task.id));
1697
+ this.archiveTotal.update((n) => Math.max(0, n - (this.mode() === 'archive' ? 1 : 0)));
1698
+ this.selected.set(null);
1699
+ }
1700
+ onLabelCreated(label) {
1701
+ this.catalog.update((list) => [...list, label].sort((a, b) => a.name.localeCompare(b.name)));
1702
+ }
1703
+ /**
1704
+ * Troca a tarefa nas listas (o que o servidor devolveu), mantendo o autor que já veio com a foto. Recusada,
1705
+ * arquivada e agrupada saem do quadro na hora (o servidor também as tira do próximo `board=true`).
1706
+ */
1707
+ replace(saved) {
1708
+ const merge = (s) => ({ ...s, ...saved, author: saved.author ?? s.author });
1709
+ const leaves = saved.status === 'recusada' || !!saved.archived_at || !!saved.merged_into;
1710
+ this.all.update((list) => (leaves
1711
+ ? list.filter((s) => s.id !== saved.id)
1712
+ : list.map((s) => (s.id === saved.id ? merge(s) : s))));
1713
+ this.archived.update((list) => list.map((s) => (s.id === saved.id ? merge(s) : s)));
1714
+ }
1715
+ // --- menu do cartão ---
1716
+ buildMenu(t) {
1717
+ const linked = new Set(t.labels.map((l) => l.id));
1718
+ const labels = this.catalog().length
1719
+ ? this.catalog().map((l) => ({ id: `label:${l.id}`, label: l.name, icon: linked.has(l.id) ? 'check' : 'tag' }))
1720
+ : [{ id: 'label:none', label: this.text.menuNoLabels, disabled: true }];
1721
+ return [
1722
+ {
1723
+ id: 'status', label: this.text.menuStatus, icon: 'view-columns',
1724
+ children: SPINE_SUGGESTION_STATUSES.map((st) => ({
1725
+ id: `status:${st}`, label: this.text.statuses[st], icon: st === t.status ? 'check' : SPINE_SUGGESTION_STATUS_ICONS[st], disabled: st === t.status,
1726
+ })),
1727
+ },
1728
+ {
1729
+ id: 'priority', label: this.text.menuPriority, icon: TASK_PRIORITY_ICONS[t.priority],
1730
+ children: TASK_PRIORITIES.map((p) => ({ id: `priority:${p}`, label: this.text.priorities[p], icon: p === t.priority ? 'check' : TASK_PRIORITY_ICONS[p] })),
1731
+ },
1732
+ { id: 'assign-me', label: this.text.menuAssignMe, icon: 'user-plus', disabled: assignedTo(t, this.me()) },
1733
+ { id: 'labels', label: this.text.menuLabel, icon: 'tag', children: labels },
1734
+ { id: 'cover', label: this.text.menuCover, icon: 'color-palette' },
1735
+ { id: 'copy-link', label: this.text.menuCopyLink, icon: 'link', separatorAbove: true },
1736
+ { id: 'copy-key', label: this.text.menuCopyKey, icon: 'copy' },
1737
+ // Sem cadeia: a que já recebeu outras não pode ser agrupada (o seletor explica; aqui fica desligado).
1738
+ { id: 'merge', label: this.text.menuMerge, icon: 'layers', separatorAbove: true, disabled: !!t.merged_count },
1739
+ { id: 'archive', label: this.text.menuArchive, icon: 'archive', separatorAbove: true },
1740
+ { id: 'delete', label: this.text.menuDelete, icon: 'trash', danger: true },
1741
+ ];
1742
+ }
1743
+ menuOf(t) {
1744
+ return this.menus().get(t.id) ?? [];
1745
+ }
1746
+ menuLabel(t) {
1747
+ return formatMessage(this.text.cardMenu, { key: t.key });
1748
+ }
1749
+ /** Uma escolha do "⋯": cada uma grava e devolve a tarefa nova (ou copia, ou abre a capa ancorada no botão). */
1750
+ async onMenu(t, id, anchor) {
1751
+ const clientId = this.clientId();
1752
+ const [action, value] = id.split(':');
1753
+ try {
1754
+ if (action === 'status')
1755
+ this.replace(await this.service.move(clientId, t.id, { status: value }));
1756
+ else if (action === 'priority')
1757
+ this.replace(await this.service.update(clientId, t.id, { priority: value }));
1758
+ else if (action === 'assign-me') {
1759
+ this.replace(await this.service.addPerson(clientId, t.id, 'me', 'responsavel'));
1760
+ this.toast.success(this.text.assignedToMe);
1761
+ }
1762
+ else if (action === 'label') {
1763
+ this.replace(await this.service.setLabel(clientId, t.id, value, !t.labels.some((l) => l.id === value)));
1764
+ }
1765
+ else if (action === 'cover') {
1766
+ this.coverTarget.set(t);
1767
+ this.coverPop()?.show(anchor);
1768
+ }
1769
+ else if (action === 'copy-link') {
1770
+ if (await copyText(this.linkOf(t)))
1771
+ this.toast.success(this.text.linkCopied);
1772
+ else
1773
+ this.toast.error(this.text.copyError);
1774
+ }
1775
+ else if (action === 'copy-key') {
1776
+ if (await copyText(t.key))
1777
+ this.toast.success(this.text.keyCopied);
1778
+ else
1779
+ this.toast.error(this.text.copyError);
1780
+ }
1781
+ else if (action === 'merge') {
1782
+ this.startMerge(t);
1783
+ }
1784
+ else if (action === 'archive') {
1785
+ this.replace(await this.service.setArchived(clientId, t.id, true));
1786
+ this.toast.success(this.text.archivedToast);
1787
+ }
1788
+ else if (action === 'delete')
1789
+ await this.confirmDelete(t);
1790
+ }
1791
+ catch {
1792
+ this.toast.error(this.text.saveError);
1793
+ }
1794
+ }
1795
+ startMerge(t) {
1796
+ this.selected.set(null);
1797
+ this.drafting.set(false);
1798
+ this.mergeSource.set(t);
1799
+ }
1800
+ /** Agrupou: a origem sai do quadro, o destino é trocado pela versão do servidor e abre (mostra "Agrupadas aqui"). */
1801
+ onMerged(event) {
1802
+ this.mergeSource.set(null);
1803
+ this.all.update((list) => list.filter((s) => s.id !== event.source.id));
1804
+ this.replace(event.target);
1805
+ this.selected.set(event.target);
1806
+ }
1807
+ /** O detalhe pediu outra tarefa (o destino de uma agrupada, uma das agrupadas): abre a completa. */
1808
+ async openTask(id) {
1809
+ try {
1810
+ const task = await this.service.task(this.clientId(), id);
1811
+ this.openDetail(task);
1812
+ }
1813
+ catch {
1814
+ this.toast.error(this.text.loadError);
1815
+ }
1816
+ }
1817
+ /** Desagrupou: a origem volta ao quadro (relê o que estiver à vista). */
1818
+ onUnmerged() {
1819
+ if (this.mode() === 'archive')
1820
+ void this.loadArchive(this.archivePage().page);
1821
+ else
1822
+ void this.load();
1823
+ }
1824
+ mergedIntoText(key) {
1825
+ return formatMessage(this.text.mergedInto, { key });
1826
+ }
1827
+ /** "Desagrupar" no arquivo. */
1828
+ async unmergeFromArchive(t) {
1829
+ this.reopening.set(t.id);
1830
+ try {
1831
+ await this.service.unmerge(this.clientId(), t.id);
1832
+ this.toast.success(formatMessage(this.text.unmergedToast, { key: t.key }));
1833
+ void this.loadArchive(this.archivePage().page);
1834
+ }
1835
+ catch {
1836
+ this.toast.error(this.text.saveError);
1837
+ }
1838
+ finally {
1839
+ this.reopening.set(null);
1840
+ }
1841
+ }
1842
+ async pickCover(cover) {
1843
+ const t = this.coverTarget();
1844
+ this.coverPop()?.close();
1845
+ if (!t || cover === t.cover)
1846
+ return;
1847
+ try {
1848
+ this.replace(await this.service.update(this.clientId(), t.id, { cover }));
1849
+ }
1850
+ catch {
1851
+ this.toast.error(this.text.saveError);
1852
+ }
1853
+ }
1854
+ async confirmDelete(t) {
1855
+ const ok = await this.dialogs.confirm({
1856
+ heading: formatMessage(this.text.deleteHeading, { key: t.key }), message: this.text.deleteMessage,
1857
+ confirmLabel: this.text.deleteConfirm, danger: true,
1858
+ });
1859
+ if (!ok)
1860
+ return;
1861
+ await this.service.remove(this.clientId(), t.id);
1862
+ this.onRemoved(t);
1863
+ this.toast.success(this.text.deleted);
1864
+ }
1865
+ /** O link da tela com a tarefa: o caminho atual (sem a query) + "?sugestao=SUG-12". */
1866
+ linkOf(t) {
1867
+ const path = (this.location?.path() ?? '').split('?')[0];
1868
+ const base = this.location?.prepareExternalUrl(path) ?? path;
1869
+ const origin = typeof window !== 'undefined' ? window.location.origin : '';
1870
+ return `${origin}${base}?${SUGGESTION_LINK_PARAM}=${encodeURIComponent(t.key)}`;
1871
+ }
1872
+ /** Item do checklist marcado no próprio cartão. */
1873
+ async toggleCardItem(t, item) {
1874
+ try {
1875
+ this.replace(await this.service.updateChecklistItem(this.clientId(), t.id, item.id, { done: !item.done }));
1876
+ }
1877
+ catch {
1878
+ this.toast.error(this.text.saveError);
1879
+ }
1880
+ }
1881
+ // --- arquivo ---
1882
+ async loadArchive(page = 1, clientId = this.clientId()) {
1883
+ const size = this.archivePage().size;
1884
+ this.archiveLoading.set(true);
1885
+ this.archiveError.set('');
1886
+ try {
1887
+ const res = await this.service.archive(clientId, {
1888
+ limit: size, offset: (page - 1) * size, search: this.archiveSearch().trim(),
1889
+ kind: this.archiveKind(), status: this.archiveStatus(),
1890
+ });
1891
+ this.archived.set(res.suggestions);
1892
+ this.archiveTotal.set(res.total);
1893
+ this.archivePage.set({ page, size });
1894
+ }
1895
+ catch {
1896
+ this.archiveError.set(this.text.loadError);
1897
+ }
1898
+ finally {
1899
+ this.archiveLoading.set(false);
1900
+ }
1901
+ }
1902
+ onArchiveSearch(term) {
1903
+ this.archiveSearch.set(term);
1904
+ void this.loadArchive(1);
1905
+ }
1906
+ onArchiveFilter(which, value) {
1907
+ (which === 'kind' ? this.archiveKind : this.archiveStatus).set(value);
1908
+ void this.loadArchive(1);
1909
+ }
1910
+ onArchivePage(state) {
1911
+ this.archivePage.set({ page: state.page, size: state.size });
1912
+ void this.loadArchive(state.page);
1913
+ }
1914
+ /** Recusada volta para Em análise (fim da coluna); arquivada volta ao quadro com o status que tinha. */
1915
+ async reopen(s) {
1916
+ if (this.reopening())
1917
+ return;
1918
+ this.reopening.set(s.id);
1919
+ try {
1920
+ if (s.archived_at)
1921
+ await this.service.setArchived(this.clientId(), s.id, false);
1922
+ if (s.status === 'recusada')
1923
+ await this.service.move(this.clientId(), s.id, { status: 'em_analise' });
1924
+ this.toast.success(s.status === 'recusada' ? this.text.reopened : this.text.restoredToast);
1925
+ const page = this.archivePage().page;
1926
+ // Última da página: volta uma, para não cair numa página vazia.
1927
+ await this.loadArchive(this.archived().length === 1 && page > 1 ? page - 1 : page);
1928
+ }
1929
+ catch {
1930
+ this.toast.error(this.text.reopenError);
1931
+ }
1932
+ finally {
1933
+ this.reopening.set(null);
1934
+ }
1935
+ }
1936
+ // --- textos do cartão ---
1937
+ sortLabel(k) {
1938
+ if (k === 'priority')
1939
+ return this.text.priority;
1940
+ if (k === 'due')
1941
+ return this.text.dueDate;
1942
+ return this.text.sorts[k];
1943
+ }
1944
+ kindLabel(s) {
1945
+ return this.text.kinds[s.kind];
1946
+ }
1947
+ kindIcon(s) {
1948
+ return SPINE_SUGGESTION_KIND_ICONS[s.kind];
1949
+ }
1950
+ priorityIcon(s) {
1951
+ return TASK_PRIORITY_ICONS[s.priority];
1952
+ }
1953
+ priorityName(s) {
1954
+ return this.text.priorities[s.priority];
1955
+ }
1956
+ isTop(s) {
1957
+ return this.topIds().has(s.id);
1958
+ }
1959
+ excerpt(s) {
1960
+ const d = s.description.replace(/\s+/g, ' ').trim();
1961
+ return d.length > EXCERPT ? `${d.slice(0, EXCERPT - 1).trimEnd()}…` : d;
1962
+ }
1963
+ faces(s) {
1964
+ return s.assignees.slice(0, FACES);
1965
+ }
1966
+ moreFaces(s) {
1967
+ const rest = s.assignees.length - FACES;
1968
+ return rest > 0 ? formatMessage(this.text.moreAssignees, { count: rest }) : '';
1969
+ }
1970
+ dueOf(s) {
1971
+ return dueState(s, this.today);
1972
+ }
1973
+ /** "Vencida há 3 dias", "Vence hoje", "Vence amanhã", "No prazo · até 12 de nov." (concluída: "Até 12 de nov."). */
1974
+ dueText(s) {
1975
+ if (!s.due_date)
1976
+ return '';
1977
+ const state = this.dueOf(s);
1978
+ if (state === 'overdue') {
1979
+ return pluralMessage(this.text.dueLateDays, daysBetween(s.due_date, this.today), this.messages.meta.locale);
1980
+ }
1981
+ if (state === 'today')
1982
+ return this.text.dueToday;
1983
+ if (state === 'tomorrow')
1984
+ return this.text.dueTomorrow;
1985
+ if (state === 'later')
1986
+ return formatMessage(this.text.dueWithin, { date: this.day(s.due_date) });
1987
+ return formatMessage(this.text.dueOn, { date: this.day(s.due_date) });
1988
+ }
1989
+ dueIcon(s) {
1990
+ const state = this.dueOf(s);
1991
+ if (state === 'overdue')
1992
+ return 'alarm-clock';
1993
+ if (state === 'today' || state === 'tomorrow')
1994
+ return 'hourglass';
1995
+ return 'calendar';
1996
+ }
1997
+ /** Contexto do cartão (na cor de link): o tipo e a tela de origem. */
1998
+ contextOf(s) {
1999
+ return [this.kindLabel(s), s.screen.label].filter(Boolean).join(' · ');
2000
+ }
2001
+ createdText(s) {
2002
+ return formatMessage(this.text.createdOn, { date: this.date(s.created_at, true) });
2003
+ }
2004
+ /** Explicação da coluna vazia: com filtro, "nenhuma com estes filtros"; sem, o que costuma cair nela. */
2005
+ emptyHint(status) {
2006
+ if (this.isFiltered())
2007
+ return this.text.columnEmptyFiltered;
2008
+ const hint = this.text.columnEmptyHints[status] ?? '';
2009
+ return formatMessage(hint, { days: this.doneDays() });
2010
+ }
2011
+ emptyIcon(status) {
2012
+ return this.isFiltered() ? 'filter' : (SPINE_SUGGESTION_STATUS_ICONS[status] ?? 'inbox');
2013
+ }
2014
+ /** "3d 4h", "4h 12min", "12min" (o catálogo dá o formato). */
2015
+ duration(ms) {
2016
+ const { d, h, m } = splitDuration(ms);
2017
+ if (d)
2018
+ return formatMessage(this.text.duration.days, { d, h });
2019
+ if (h)
2020
+ return formatMessage(this.text.duration.hours, { h, m });
2021
+ return formatMessage(this.text.duration.minutes, { m });
2022
+ }
2023
+ checklistText(s) {
2024
+ return formatMessage(this.text.checklistSpoken, { done: s.checklist.done, total: s.checklist.total });
2025
+ }
2026
+ cardItems(s) {
2027
+ return s.checklist_on_card ? [...(s.checklist.items ?? [])].sort((a, b) => a.position - b.position) : [];
2028
+ }
2029
+ personName(p) {
2030
+ return p?.name || p?.username || this.text.removedAuthor;
2031
+ }
2032
+ votesText(count) {
2033
+ return pluralMessage(this.text.votes, count, this.messages.meta.locale);
2034
+ }
2035
+ commentsText(count) {
2036
+ return pluralMessage(this.text.comments, count, this.messages.meta.locale);
2037
+ }
2038
+ closedOn(s) {
2039
+ if (s.archived_at)
2040
+ return `${this.text.archivedTag} · ${this.date(s.archived_at)}`;
2041
+ const template = s.status === 'recusada' ? this.text.closedOn.recusada : this.text.closedOn.concluida;
2042
+ return formatMessage(template, { date: this.date(s.status_at ?? s.updated_at) });
2043
+ }
2044
+ /** "há 3 dias", "ontem", "há 2 horas" (Intl.RelativeTimeFormat no idioma do catálogo). */
2045
+ age(iso) {
2046
+ const seconds = Math.round((Date.now() - new Date(iso).getTime()) / 1000);
2047
+ try {
2048
+ const rtf = new Intl.RelativeTimeFormat(this.messages.meta.locale, { numeric: 'auto' });
2049
+ for (const [unit, size] of AGES)
2050
+ if (Math.abs(seconds) >= size)
2051
+ return rtf.format(-Math.floor(seconds / size), unit);
2052
+ return rtf.format(0, 'minute');
2053
+ }
2054
+ catch {
2055
+ return this.date(iso);
2056
+ }
2057
+ }
2058
+ date(iso, withTime = false) {
2059
+ try {
2060
+ return new Intl.DateTimeFormat(this.messages.meta.locale, {
2061
+ dateStyle: 'short', ...(withTime ? { timeStyle: 'short' } : {}), timeZone: this.preferences.effectiveTimeZone(),
2062
+ }).format(new Date(iso));
2063
+ }
2064
+ catch {
2065
+ return iso.slice(0, 10);
2066
+ }
2067
+ }
2068
+ /** Data sem hora (AAAA-MM-DD): o dia é o dia, sem fuso. */
2069
+ day(iso) {
2070
+ try {
2071
+ return new Intl.DateTimeFormat(this.messages.meta.locale, { day: '2-digit', month: 'short', timeZone: 'UTC' }).format(new Date(`${iso}T00:00:00Z`));
2072
+ }
2073
+ catch {
2074
+ return iso;
2075
+ }
2076
+ }
2077
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AutoServiceSuggestionsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2078
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: AutoServiceSuggestionsComponent, isStandalone: true, selector: "spine-auto-service-suggestions", inputs: { clientId: { classPropertyName: "clientId", publicName: "clientId", isSignal: true, isRequired: true, transformFunction: null } }, viewQueries: [{ propertyName: "coverPop", first: true, predicate: ["coverPop"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"as-stack as-suggestions\">\n <!-- Quadro x Arquivo, o atualizar e a nova tarefa (com o \"\u25BE\" para criar direto numa coluna). -->\n <div class=\"as-suggestions__bar\">\n <hm-segmented-control [label]=\"text.viewLabel\" [options]=\"modeOptions\" [value]=\"mode()\" (valueChange)=\"setMode($event)\" />\n <span class=\"as-suggestions__spacer\"></span>\n <hm-icon-button emphasis=\"subtle\" icon=\"refresh\" [label]=\"text.refresh\" (clicked)=\"refresh()\" />\n <span class=\"as-split\" role=\"group\" [attr.aria-label]=\"text.newTask\">\n <hm-button icon=\"plus\" (clicked)=\"newTask()\">{{ text.newTask }}</hm-button>\n <hm-menu-button class=\"as-split__more\" icon=\"chevron-down\" [showArrow]=\"false\" align=\"end\" [label]=\"text.newTaskOptions\"\n [items]=\"newTaskItems\" (chosen)=\"newTask($any($event))\" />\n </span>\n </div>\n\n @if (mode() === 'board') {\n @if (loadError()) {\n <hm-alert variant=\"error\">{{ loadError() }}</hm-alert>\n } @else {\n <!-- Indicadores do quadro inteiro (o filtro n\u00E3o mexe neles). Os bot\u00F5es filtram: vencidas, vencendo em breve,\n atribu\u00EDdas a mim e cada prioridade da legenda. Cor sempre com texto junto. -->\n <section class=\"as-kpis\" [attr.aria-label]=\"text.kpis\" [attr.aria-busy]=\"kpiLoading() ? 'true' : null\">\n <button type=\"button\" class=\"as-kpi\" data-tone=\"danger\" [attr.aria-pressed]=\"due() === 'overdue'\"\n (click)=\"toggleKpi('overdue', due() !== 'overdue')\">\n <span class=\"as-kpi__icon\" aria-hidden=\"true\"><hm-icon shape=\"alarm-clock\" /></span>\n <span class=\"as-kpi__body\">\n <span class=\"as-kpi__label\">{{ text.kpiOverdue }}</span>\n <span class=\"as-kpi__value\">{{ kpiLoading() ? '\u2014' : stats().overdue }}</span>\n <span class=\"as-kpi__note\" [class.as-kpi__note--up]=\"stats().overdueNew > 0\">\n @if (stats().overdueNew) { <hm-icon shape=\"arrow-up\" /> }{{ overdueNote() }}\n </span>\n </span>\n </button>\n\n <button type=\"button\" class=\"as-kpi\" data-tone=\"warning\" [attr.aria-pressed]=\"due() === 'soon'\"\n (click)=\"toggleKpi('soon', due() !== 'soon')\">\n <span class=\"as-kpi__icon\" aria-hidden=\"true\"><hm-icon shape=\"hourglass\" /></span>\n <span class=\"as-kpi__body\">\n <span class=\"as-kpi__label\">{{ text.kpiSoon }}</span>\n <span class=\"as-kpi__value\">{{ kpiLoading() ? '\u2014' : stats().soon }}</span>\n <span class=\"as-kpi__note\">{{ text.dueSoonFilter }}</span>\n </span>\n </button>\n\n <div class=\"as-kpi\" data-tone=\"info\">\n <span class=\"as-kpi__icon\" aria-hidden=\"true\"><hm-icon [shape]=\"statusIcons.em_desenvolvimento\" /></span>\n <span class=\"as-kpi__body\">\n <span class=\"as-kpi__label\">{{ text.kpiOpen }}</span>\n <span class=\"as-kpi__value\">{{ kpiLoading() ? '\u2014' : stats().open }}</span>\n <span class=\"as-kpi__parts\">\n @for (part of openParts(); track part.status) {\n <span class=\"as-kpi__part\" [attr.data-status]=\"part.status\"><span class=\"as-kpi__dot\" aria-hidden=\"true\"></span>{{ part.text }}</span>\n }\n </span>\n </span>\n </div>\n\n <div class=\"as-kpi\" data-tone=\"success\">\n <span class=\"as-kpi__icon\" aria-hidden=\"true\"><hm-icon [shape]=\"statusIcons.concluida\" /></span>\n <span class=\"as-kpi__body\">\n <span class=\"as-kpi__label\">{{ doneLabel() }}</span>\n <span class=\"as-kpi__value\">{{ kpiLoading() ? '\u2014' : stats().concluida }}</span>\n <span class=\"as-kpi__note\" [class.as-kpi__note--good]=\"stats().doneToday > 0\">\n @if (stats().doneToday) { <hm-icon shape=\"arrow-up\" /> }{{ doneNote() }}\n </span>\n </span>\n </div>\n\n <button type=\"button\" class=\"as-kpi\" data-tone=\"accent\" [attr.aria-pressed]=\"assignee() === 'me'\"\n (click)=\"toggleKpi('mine', assignee() !== 'me')\">\n <span class=\"as-kpi__icon\" aria-hidden=\"true\"><hm-icon shape=\"user-checkmark\" /></span>\n <span class=\"as-kpi__body\">\n <span class=\"as-kpi__label\">{{ text.kpiMine }}</span>\n <span class=\"as-kpi__value\">{{ kpiLoading() ? '\u2014' : stats().mine }}</span>\n <span class=\"as-kpi__note\">{{ text.kpiMineHint }}</span>\n </span>\n </button>\n\n <div class=\"as-kpi as-kpi--wide\" data-tone=\"neutral\">\n <span class=\"as-kpi__icon\" aria-hidden=\"true\"><hm-icon shape=\"bar-chart\" /></span>\n <span class=\"as-kpi__body\">\n <span class=\"as-kpi__label\">{{ text.kpiPriority }}</span>\n @if (stats().open) {\n <span class=\"as-kpi__bar\" aria-hidden=\"true\">\n @for (p of stats().byPriority; track p.priority) {\n @if (p.count) {\n <span class=\"as-kpi__seg\" [attr.data-priority]=\"p.priority\" [style.inline-size.%]=\"priorityShare(p.count)\"></span>\n }\n }\n </span>\n <span class=\"as-kpi__legend\">\n @for (p of stats().byPriority; track p.priority) {\n <button type=\"button\" class=\"as-kpi__chip\" [attr.data-priority]=\"p.priority\" [attr.aria-pressed]=\"priority() === p.priority\"\n [disabled]=\"!p.count && priority() !== p.priority\" (click)=\"togglePriority(p.priority)\">\n <span class=\"as-kpi__dot\" aria-hidden=\"true\"></span>{{ priorityPart(p.priority, p.count) }}\n </button>\n }\n </span>\n } @else {\n <span class=\"as-kpi__note\">{{ text.kpiPriorityNone }}</span>\n }\n </span>\n </div>\n </section>\n\n <!-- Filtros numa linha: busca larga, os mais usados e \"Mais filtros\" (tela, quem pediu); \u00E0 direita, a ordena\u00E7\u00E3o.\n Valem sobre a lista inteira: a posi\u00E7\u00E3o gravada ao arrastar \u00E9 sempre a da coluna inteira. -->\n <div class=\"as-filters\" role=\"group\" [attr.aria-label]=\"text.filters\">\n <hm-search-field class=\"as-filters__search\" [label]=\"text.search\" labelHidden [placeholder]=\"text.search\"\n [landmark]=\"false\" [debounce]=\"150\" [value]=\"search()\" (search)=\"search.set($event)\" />\n <hm-select class=\"as-filters__select\" [label]=\"text.assigneeFilter\" labelHidden [(value)]=\"assignee\">\n <option value=\"\">{{ text.allAssignees }}</option>\n <option value=\"me\">{{ text.assigneeMine }}</option>\n <option value=\"none\">{{ text.assigneeNone }}</option>\n @for (p of assigneeOptions(); track p.user_id) {\n <option [value]=\"p.user_id\">{{ personName(p) }}</option>\n }\n </hm-select>\n <hm-select class=\"as-filters__select\" [label]=\"text.kindFilter\" labelHidden [(value)]=\"kind\">\n <option value=\"\">{{ text.allKinds }}</option>\n @for (k of kinds; track k) {\n <option [value]=\"k\">{{ text.kinds[k] }}</option>\n }\n </hm-select>\n <hm-select class=\"as-filters__select\" [label]=\"text.statusFilter\" labelHidden [(value)]=\"status\">\n <option value=\"\">{{ text.allBoardStatuses }}</option>\n @for (st of boardStatuses; track st) {\n <option [value]=\"st\">{{ text.statuses[st] }}</option>\n }\n </hm-select>\n <hm-select class=\"as-filters__select\" [label]=\"text.priorityFilter\" labelHidden [(value)]=\"priority\">\n <option value=\"\">{{ text.allPriorities }}</option>\n @for (p of priorities; track p) {\n <option [value]=\"p\">{{ text.priorities[p] }}</option>\n }\n </hm-select>\n <hm-select class=\"as-filters__select\" [label]=\"text.labelFilter\" labelHidden [(value)]=\"label\">\n <option value=\"\">{{ text.allLabels }}</option>\n @for (l of catalog(); track l.id) {\n <option [value]=\"l.id\">{{ l.name }}</option>\n }\n </hm-select>\n <hm-select class=\"as-filters__select\" [label]=\"text.dueFilter\" labelHidden [(value)]=\"due\">\n <option value=\"\">{{ text.dueAny }}</option>\n <option value=\"overdue\">{{ text.dueOverdueFilter }}</option>\n <option value=\"soon\">{{ text.dueSoonFilter }}</option>\n </hm-select>\n <button type=\"button\" class=\"as-filters__more\" [class.as-filters__more--on]=\"extraFilters() > 0\"\n [attr.aria-expanded]=\"moreFilters()\" [attr.aria-controls]=\"moreFilters() ? 'as-more-filters' : null\"\n (click)=\"moreFilters.set(!moreFilters())\">\n <hm-icon shape=\"filter\" />{{ moreFiltersText() }}\n </button>\n <span class=\"as-filters__spacer\"></span>\n <hm-select class=\"as-filters__select as-filters__sort\" [label]=\"text.sort\" labelHidden [value]=\"sort()\" (valueChange)=\"sort.set($any($event))\">\n @for (k of sortKeys; track k) {\n <option [value]=\"k\">{{ text.sort }}: {{ sortLabel(k) }}</option>\n }\n </hm-select>\n </div>\n @if (moreFilters()) {\n <div class=\"as-filters as-filters--more\" id=\"as-more-filters\">\n <hm-select class=\"as-filters__select\" [label]=\"text.screenFilter\" labelHidden [(value)]=\"screen\">\n <option value=\"\">{{ text.allScreens }}</option>\n @for (sc of screens(); track sc.path) {\n <option [value]=\"sc.path\">{{ sc.label }}</option>\n }\n </hm-select>\n <hm-select class=\"as-filters__select\" [label]=\"text.authorFilter\" labelHidden [(value)]=\"author\">\n <option value=\"\">{{ text.allAuthors }}</option>\n @for (p of authors(); track p.user_id) {\n <option [value]=\"p.user_id\">{{ personName(p) }}</option>\n }\n </hm-select>\n </div>\n }\n <div class=\"as-suggestions__status\">\n @if (sort() !== 'position') {\n <p class=\"as-suggestions__hint\">{{ text.sortHint }}</p>\n }\n @if (isFiltered()) {\n <span class=\"as-suggestions__count\" aria-live=\"polite\">{{ filteredText() }}</span>\n <hm-button size=\"small\" emphasis=\"minimal\" icon=\"filter-close\" (clicked)=\"clearFilters()\">{{ text.clearFilters }}</hm-button>\n }\n </div>\n\n @if (!loading() && all().length === 0) {\n <hm-empty icon=\"lightbulb\" [heading]=\"text.boardEmpty\" [description]=\"text.boardEmptyHint\" />\n }\n\n <hm-kanban class=\"as-board\" [label]=\"text.boardLabel\" [columns]=\"columns()\" [itemKey]=\"itemKey\" [itemLabel]=\"itemLabel\"\n (moved)=\"onMoved($event)\">\n <!-- \"+\" no cabe\u00E7alho: abre o cadastro completo (o modal da \"Nova tarefa\") j\u00E1 nesta coluna. -->\n <ng-template hmKanbanColumnActions let-column>\n <hm-icon-button size=\"small\" emphasis=\"subtle\" icon=\"plus\" [label]=\"newTaskInLabel(column.id)\" (clicked)=\"newTask(column.id)\" />\n </ng-template>\n\n <!-- Logo abaixo do cabe\u00E7alho: h\u00E1 quanto tempo a mais antiga est\u00E1 no status e quantas venceram. -->\n <ng-template hmKanbanColumnSummary let-column>\n @if (columnSummaries().get(column.id); as sum) {\n @if (sum.oldest) {\n <span class=\"as-colsum\"><hm-icon shape=\"clock\" />{{ sum.oldest }}</span>\n }\n @if (sum.overdue) {\n <span class=\"as-colsum as-colsum--late\"><hm-icon shape=\"alarm-clock\" />{{ sum.overdue }}</span>\n }\n }\n </ng-template>\n\n <!-- Coluna vazia: \u00EDcone grande, a frase e o que costuma cair nela (com filtro, \"nenhuma com estes filtros\"). -->\n <ng-template hmKanbanColumnEmpty let-column>\n <span class=\"as-colempty\">\n <hm-icon class=\"as-colempty__icon\" [shape]=\"emptyIcon(column.id)\" />\n <span class=\"as-colempty__title\">{{ text.columnEmpty }}</span>\n <span class=\"as-colempty__hint\">{{ emptyHint(column.id) }}</span>\n </span>\n </ng-template>\n\n <!-- Rodap\u00E9: \"+ Criar tarefa\" abre o mesmo cadastro, com o status da coluna j\u00E1 escolhido. -->\n <ng-template hmKanbanColumnFooter let-column>\n <button type=\"button\" class=\"as-colfoot\" (click)=\"newTask(column.id)\">\n <hm-icon shape=\"plus\" />{{ text.createTask }}<span class=\"as-sr-only\">: {{ column.title }}</span>\n </button>\n </ng-template>\n\n <ng-template hmKanbanCard let-item>\n <div class=\"as-card\" [class.as-card--top]=\"isTop(item)\">\n @if (item.cover) {\n <div class=\"as-card__cover\" [class]=\"'as-tok--' + item.cover\" aria-hidden=\"true\"></div>\n }\n <div class=\"as-card__body\">\n <div class=\"as-card__top\">\n <span class=\"as-card__key\">{{ item.key }}</span>\n <span class=\"as-card__priority\" [attr.data-priority]=\"item.priority\">\n <hm-icon [shape]=\"priorityIcon(item)\" /><span class=\"as-sr-only\">{{ text.priority }}: </span>{{ priorityName(item) }}\n </span>\n @if (isTop(item)) {\n <span class=\"as-card__badge\"><hm-icon shape=\"flame\" />{{ text.topVoted }}</span>\n }\n <span #menuAnchor class=\"as-card__menu\">\n <hm-menu-button size=\"small\" icon=\"ellipsis-vertical\" [showArrow]=\"false\" align=\"end\" [label]=\"menuLabel(item)\"\n [items]=\"menuOf(item)\" (chosen)=\"onMenu(item, $event, menuAnchor)\" />\n </span>\n </div>\n <button type=\"button\" class=\"as-card__title\" (click)=\"openDetail(item)\">{{ item.title }}</button>\n <span class=\"as-card__context\" [attr.data-kind]=\"item.kind\"><hm-icon [shape]=\"kindIcon(item)\" />{{ contextOf(item) }}</span>\n @if (item.labels.length) {\n <div class=\"as-card__labels\">\n @for (l of item.labels; track l.id) {\n <span class=\"as-label\" [class]=\"'as-tok--' + l.color\">{{ l.name }}</span>\n }\n </div>\n }\n @if (excerpt(item); as ex) {\n <p class=\"as-card__excerpt\">{{ ex }}</p>\n }\n @if (cardItems(item).length) {\n <ul class=\"as-card__checklist\">\n @for (c of cardItems(item); track c.id) {\n <li>\n <button type=\"button\" class=\"as-card__check\" [attr.aria-pressed]=\"c.done\" (click)=\"toggleCardItem(item, c)\">\n <hm-icon [shape]=\"c.done ? 'check-circle' : 'circle-empty'\" /><span [class.as-card__done]=\"c.done\">{{ c.text }}</span>\n </button>\n </li>\n }\n </ul>\n }\n @if (item.due_date) {\n <span class=\"as-card__due\" [attr.data-due]=\"dueOf(item) ?? 'closed'\"><hm-icon [shape]=\"dueIcon(item)\" />{{ dueText(item) }}</span>\n }\n <div class=\"as-card__foot\">\n <span class=\"as-card__count\" [class.as-card__count--zero]=\"!item.comments_count\" [attr.title]=\"commentsText(item.comments_count)\">\n <hm-icon shape=\"comment-message\" /><span aria-hidden=\"true\">{{ item.comments_count }}</span><span class=\"as-sr-only\">{{ commentsText(item.comments_count) }}</span>\n </span>\n @if (item.checklist.total) {\n <span class=\"as-card__count\" [class.as-card__count--full]=\"item.checklist.done === item.checklist.total\" [attr.title]=\"checklistText(item)\">\n <hm-icon shape=\"list-check\" /><span aria-hidden=\"true\">{{ item.checklist.done }}/{{ item.checklist.total }}</span><span class=\"as-sr-only\">{{ checklistText(item) }}</span>\n </span>\n }\n <span class=\"as-card__count\" [class.as-card__count--on]=\"item.votes > 0\" [class.as-card__count--zero]=\"!item.votes\" [attr.title]=\"votesText(item.votes)\">\n <hm-icon shape=\"thumbs-up\" /><span aria-hidden=\"true\">{{ item.votes }}</span><span class=\"as-sr-only\">{{ votesText(item.votes) }}</span>\n </span>\n <span class=\"as-card__date\" [attr.title]=\"createdText(item)\">\n <hm-icon shape=\"clock\" /><span aria-hidden=\"true\">{{ date(item.created_at) }}</span><span class=\"as-sr-only\">{{ createdText(item) }}</span>\n </span>\n <span class=\"as-card__faces\">\n @for (p of faces(item); track p.user_id) {\n <hm-account-avatar size=\"small\" [name]=\"personName(p)\" [imageUrl]=\"p.picture\" [attr.title]=\"personName(p)\" />\n } @empty {\n <span class=\"as-card__nobody\" [attr.title]=\"text.assigneeNone\">\n <hm-icon shape=\"user-circle\" /><span class=\"as-sr-only\">{{ text.assigneeNone }}</span>\n </span>\n }\n @if (moreFaces(item); as more) { <span class=\"as-card__more\">{{ more }}</span> }\n </span>\n </div>\n </div>\n </div>\n </ng-template>\n </hm-kanban>\n }\n } @else {\n <!-- Arquivo: arquivadas, recusadas e conclu\u00EDdas fora da janela do quadro, paginado no servidor. -->\n <div class=\"as-suggestions__filters\">\n <hm-search-field class=\"as-suggestions__search\" [label]=\"text.archiveSearch\" labelHidden [placeholder]=\"text.archiveSearch\"\n [landmark]=\"false\" [debounce]=\"300\" [value]=\"archiveSearch()\" (search)=\"onArchiveSearch($event)\" />\n <hm-select class=\"as-suggestions__select\" [label]=\"text.statusFilter\" labelHidden [value]=\"archiveStatus()\"\n (valueChange)=\"onArchiveFilter('status', $any($event))\">\n <option value=\"\">{{ text.statusFilter }}: {{ text.allStatuses }}</option>\n @for (st of statuses; track st) {\n <option [value]=\"st\">{{ text.statuses[st] }}</option>\n }\n </hm-select>\n <hm-select class=\"as-suggestions__select\" [label]=\"text.kindFilter\" labelHidden [value]=\"archiveKind()\"\n (valueChange)=\"onArchiveFilter('kind', $any($event))\">\n <option value=\"\">{{ text.allKinds }}</option>\n @for (k of kinds; track k) {\n <option [value]=\"k\">{{ text.kinds[k] }}</option>\n }\n </hm-select>\n @if (archiveTotal()) {\n <span class=\"as-suggestions__count\" aria-live=\"polite\">{{ archiveCountText() }}</span>\n }\n </div>\n\n @if (archiveError()) {\n <hm-alert variant=\"error\">{{ archiveError() }}</hm-alert>\n } @else if (!archiveLoading() && archived().length === 0) {\n <hm-empty icon=\"archive\" [heading]=\"text.archiveEmpty\" [description]=\"archiveEmptyHint()\" />\n } @else {\n <ul class=\"as-archive\" [attr.aria-label]=\"text.archiveLabel\" [attr.aria-busy]=\"archiveLoading() ? 'true' : null\">\n @for (s of archived(); track s.id) {\n <li class=\"as-archive__item\">\n <div class=\"as-archive__main\">\n <div class=\"as-archive__tags\">\n <span class=\"as-card__key\">{{ s.key }}</span>\n <hm-tag [name]=\"text.statuses[s.status]\" [color]=\"statusColors[s.status]\" />\n <hm-tag [name]=\"text.kinds[s.kind]\" [color]=\"kindColors[s.kind]\" />\n @if (s.merged_into; as into) {\n <!-- Agrupada: no arquivo, com o destino; os coment\u00E1rios ficam nela. -->\n <hm-tag [name]=\"text.mergedTag\" color=\"info\" />\n <button type=\"button\" class=\"as-archive__link\" (click)=\"openTask(into.id)\">{{ mergedIntoText(into.key) }}</button>\n } @else {\n <span class=\"as-archive__date\">{{ closedOn(s) }}</span>\n }\n </div>\n <button type=\"button\" class=\"as-card__title\" (click)=\"openDetail(s)\">{{ s.title }}</button>\n <div class=\"as-archive__meta\">\n <hm-account-avatar size=\"small\" [name]=\"personName(s.author)\" [imageUrl]=\"s.author?.picture\" />\n <span>{{ personName(s.author) }}</span>\n <span aria-hidden=\"true\">\u00B7</span>\n <span>{{ s.screen.label || text.noScreen }}</span>\n @if (s.votes) {\n <span aria-hidden=\"true\">\u00B7</span>\n <span>{{ votesText(s.votes) }}</span>\n }\n </div>\n </div>\n @if (s.merged_into) {\n <hm-button size=\"small\" emphasis=\"medium\" icon=\"unlink\" [loading]=\"reopening() === s.id\" (clicked)=\"unmergeFromArchive(s)\">\n {{ text.unmerge }}\n </hm-button>\n } @else if (s.archived_at) {\n <hm-button size=\"small\" emphasis=\"medium\" icon=\"unarchive\" [loading]=\"reopening() === s.id\" (clicked)=\"reopen(s)\">\n {{ text.menuRestore }}\n </hm-button>\n } @else if (s.status === 'recusada') {\n <hm-button size=\"small\" emphasis=\"medium\" icon=\"unarchive\" [loading]=\"reopening() === s.id\" (clicked)=\"reopen(s)\">\n {{ text.reopen }}\n </hm-button>\n }\n </li>\n }\n </ul>\n @if (archiveTotal() > archivePage().size) {\n <hm-paginator variant=\"simple\" [label]=\"text.pagination\" [state]=\"archivePage()\" [totalItems]=\"archiveTotal()\"\n [sizes]=\"[20, 50]\" (stateChanged)=\"onArchivePage($event)\" />\n }\n }\n }\n</div>\n\n<!-- Capa pelo \"\u22EF\" do cart\u00E3o: o popover abre ancorado no bot\u00E3o do cart\u00E3o escolhido. -->\n<hm-popover #coverPop [label]=\"text.cover\">\n <spine-suggestion-cover-picker [value]=\"coverTarget()?.cover ?? null\" (picked)=\"pickCover($event)\" />\n</hm-popover>\n\n<spine-suggestion-task-detail [clientId]=\"clientId()\" [task]=\"selected()\" [draft]=\"drafting()\" [draftStatus]=\"draftStatus()\"\n [labels]=\"catalog()\" [canOpenScreen]=\"canOpenScreen()\"\n (changed)=\"replace($event)\" (created)=\"onCreated($event)\" (removed)=\"onRemoved($event)\"\n (closed)=\"closeDetail()\" (labelCreated)=\"onLabelCreated($event)\"\n (mergeRequested)=\"startMerge($event)\" (openTask)=\"openTask($event)\" (unmerged)=\"onUnmerged()\" />\n\n<!-- \"Agrupar em\u2026\" (do \"\u22EF\" do cart\u00E3o ou do detalhe): as abertas do quadro, com a pr\u00E9via do servidor. -->\n<spine-suggestion-merge-picker [clientId]=\"clientId()\" [source]=\"mergeSource()\" [candidates]=\"all()\"\n (merged)=\"onMerged($event)\" (closed)=\"mergeSource.set(null)\" />\n", styles: [".as-stack{display:flex;flex-direction:column;gap:var(--hm-space-md);min-inline-size:0}.as-actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--hm-space-xs)}.as-field-row{display:flex;flex-wrap:wrap;align-items:flex-end;gap:var(--hm-space-sm)}.as-field-row>hm-combobox{flex:1 1 16rem;min-inline-size:0}.as-summary{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-sm)}.as-summary__info{display:flex;flex:1 1 16rem;flex-direction:column;gap:var(--hm-space-micro);min-inline-size:0}.as-summary__title{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs);margin:0;color:var(--hm-text);font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-semibold)}.as-summary__detail{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.muted{color:var(--hm-muted)}.as-empty{margin:var(--hm-space-xs) 0 0;font-size:var(--hm-font-size-sm)}.as-secret{display:grid;gap:var(--hm-space-micro)}.as-secret__label{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.as-secret__row{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.as-secret__value{flex:1 1 240px;min-width:0;border:var(--hm-border-hairline) solid var(--hm-divider);border-radius:var(--hm-radius-control);padding:var(--hm-space-xs) var(--hm-space-sm);background:var(--hm-surface-muted);color:var(--hm-text);font-family:var(--hm-font-mono);font-size:var(--hm-font-size-sm);overflow-wrap:anywhere;-webkit-user-select:all;user-select:all}.as-tok--cat-1{--as-tok: var(--hm-data-categorical-1);background:var(--as-tok)}.as-tok--cat-2{--as-tok: var(--hm-data-categorical-2);background:var(--as-tok)}.as-tok--cat-3{--as-tok: var(--hm-data-categorical-3);background:var(--as-tok)}.as-tok--cat-4{--as-tok: var(--hm-data-categorical-4);background:var(--as-tok)}.as-tok--cat-5{--as-tok: var(--hm-data-categorical-5);background:var(--as-tok)}.as-tok--cat-6{--as-tok: var(--hm-data-categorical-6);background:var(--as-tok)}.as-tok--cat-7{--as-tok: var(--hm-data-categorical-7);background:var(--as-tok)}.as-tok--cat-8{--as-tok: var(--hm-data-categorical-8);background:var(--as-tok)}.as-tok--grad-1{--as-tok: var(--hm-data-categorical-1);background:linear-gradient(135deg,var(--hm-data-categorical-1),var(--hm-data-categorical-2))}.as-tok--grad-2{--as-tok: var(--hm-data-categorical-3);background:linear-gradient(135deg,var(--hm-data-categorical-3),var(--hm-data-categorical-4))}.as-tok--grad-3{--as-tok: var(--hm-data-categorical-5);background:linear-gradient(135deg,var(--hm-data-categorical-5),var(--hm-data-categorical-6))}.as-tok--grad-4{--as-tok: var(--hm-data-categorical-7);background:linear-gradient(135deg,var(--hm-data-categorical-7),var(--hm-data-categorical-8))}.as-tok--grad-5{--as-tok: var(--hm-data-categorical-1);background:linear-gradient(135deg,var(--hm-data-categorical-1),var(--hm-data-categorical-5))}.as-tok--grad-6{--as-tok: var(--hm-data-categorical-2);background:linear-gradient(135deg,var(--hm-data-categorical-2),var(--hm-data-categorical-6))}.as-label{display:inline-flex;align-items:center;gap:var(--hm-space-micro);max-inline-size:100%;padding:0 var(--hm-space-xxxs);border-radius:var(--hm-radius-pill);background:color-mix(in srgb,var(--as-tok, var(--hm-border)) 18%,var(--hm-surface-raised));color:var(--hm-text);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold);line-height:1.6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.as-label:before{flex:none;inline-size:.5rem;block-size:.5rem;border-radius:50%;background:var(--as-tok, var(--hm-border));content:\"\"}@media(forced-colors:active){.as-label{border:var(--hm-border-hairline) solid CanvasText}}:host{display:block;container-type:inline-size}.as-sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}.as-suggestions__bar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-sm)}.as-suggestions__spacer{flex:1}.as-split{display:inline-flex;align-items:stretch}.as-split ::ng-deep hm-button .hm-btn{border-start-end-radius:0;border-end-end-radius:0}.as-split ::ng-deep .as-split__more .hm-menu-button{--hm-icon-btn-color: var(--hm-on-action);--hm-icon-btn-hover: var(--hm-action-hover);block-size:var(--hm-control-height);border-inline-start:var(--hm-border-hairline) solid color-mix(in srgb,var(--hm-on-action) 40%,transparent);border-start-start-radius:0;border-end-start-radius:0;background:var(--hm-action)}.as-kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(12rem,1fr));gap:var(--hm-space-sm)}.as-kpis[aria-busy=true] .as-kpi__value{color:var(--hm-muted)}.as-kpi{--as-tone: var(--hm-muted);--as-tone-bg: var(--hm-hover);--as-tint: 0%;display:flex;align-items:flex-start;gap:var(--hm-space-sm);box-sizing:border-box;min-inline-size:0;margin:0;padding:var(--hm-space-sm);border:var(--hm-border-hairline) solid color-mix(in srgb,var(--as-tone) 35%,var(--hm-border));border-radius:var(--hm-radius);background:color-mix(in srgb,var(--as-tone) var(--as-tint),var(--hm-surface-raised));color:var(--hm-text);font:inherit;text-align:start}.as-kpi[data-tone=danger]{--as-tone: var(--hm-danger);--as-tone-bg: var(--hm-danger-bg);--as-tint: 7%}.as-kpi[data-tone=warning]{--as-tone: var(--hm-warning);--as-tone-bg: var(--hm-warning-bg);--as-tint: 7%}.as-kpi[data-tone=info]{--as-tone: var(--hm-info);--as-tone-bg: var(--hm-info-bg);--as-tint: 7%}.as-kpi[data-tone=success]{--as-tone: var(--hm-success);--as-tone-bg: var(--hm-success-bg);--as-tint: 7%}.as-kpi[data-tone=accent]{--as-tone: var(--hm-accent);--as-tone-bg: var(--hm-accent-subtle);--as-tint: 7%}.as-kpi--wide{grid-column:span 2}.as-kpi__icon{display:inline-flex;flex:none;align-items:center;justify-content:center;inline-size:var(--hm-control-height-large);block-size:var(--hm-control-height-large);border-radius:var(--hm-radius);background:var(--as-tone-bg);color:var(--as-tone)}.as-kpi__icon hm-icon{--hm-icon-size: calc(var(--hm-control-height-large) * .55)}.as-kpi__body{display:flex;flex:1;flex-direction:column;gap:var(--hm-space-micro);min-inline-size:0}.as-kpi__label{color:var(--hm-muted);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-medium)}.as-kpi__value{color:var(--hm-text);font-size:var(--hm-font-size-xxl);font-weight:var(--hm-font-weight-bold);font-variant-numeric:tabular-nums;line-height:var(--hm-line-height-tight)}.as-kpi__note{display:inline-flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-micro);color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.as-kpi__note--up{color:var(--hm-danger);font-weight:var(--hm-font-weight-semibold)}.as-kpi__note--good{color:var(--hm-success);font-weight:var(--hm-font-weight-semibold)}.as-kpi__parts,.as-kpi__legend{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-micro) var(--hm-space-xs);font-size:var(--hm-font-size-xs)}.as-kpi__part{display:inline-flex;align-items:center;gap:var(--hm-space-micro);color:var(--hm-muted);white-space:nowrap}.as-kpi__dot{flex:none;inline-size:.5rem;block-size:.5rem;border-radius:50%;background:var(--as-dot, var(--hm-muted))}.as-kpi__part[data-status=em_analise]{--as-dot: var(--hm-info)}.as-kpi__part[data-status=planejada]{--as-dot: var(--hm-accent)}.as-kpi__part[data-status=em_desenvolvimento]{--as-dot: var(--hm-warning)}.as-kpi__bar{display:flex;gap:var(--hm-border-thin);block-size:var(--hm-space-xs);margin-block:var(--hm-space-micro);overflow:hidden;border-radius:var(--hm-radius-pill);background:var(--hm-hover)}.as-kpi__seg{flex:none;min-inline-size:var(--hm-space-micro);background:var(--as-dot, var(--hm-muted))}.as-kpi [data-priority=urgente]{--as-dot: var(--hm-danger)}.as-kpi [data-priority=alta]{--as-dot: var(--hm-warning)}.as-kpi [data-priority=media]{--as-dot: var(--hm-info)}.as-kpi [data-priority=baixa]{--as-dot: var(--hm-muted)}.as-kpi__chip{display:inline-flex;align-items:center;gap:var(--hm-space-micro);padding:0 var(--hm-space-xxxs);border:var(--hm-border-hairline) solid transparent;border-radius:var(--hm-radius-pill);background:none;color:var(--hm-text);font:inherit;font-variant-numeric:tabular-nums;white-space:nowrap;cursor:pointer}.as-kpi__chip:hover:not(:disabled){background:var(--hm-hover)}.as-kpi__chip:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color)}.as-kpi__chip[aria-pressed=true]{border-color:var(--hm-text);background:var(--hm-hover);font-weight:var(--hm-font-weight-semibold)}.as-kpi__chip:disabled{color:var(--hm-muted);cursor:default}button.as-kpi{cursor:pointer;transition:border-color var(--hm-duration-quick) var(--hm-ease-standard),background var(--hm-duration-quick) var(--hm-ease-standard)}button.as-kpi:hover{border-color:var(--as-tone)}button.as-kpi:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color);outline-offset:var(--hm-border-hairline)}button.as-kpi[aria-pressed=true]{--as-tint: 16%;border-color:var(--as-tone);box-shadow:inset 0 0 0 var(--hm-border-thin) var(--as-tone)}.as-filters{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.as-filters__search{flex:3 1 16rem;min-inline-size:0}.as-filters__select{flex:1 1 9rem;min-inline-size:8rem;max-inline-size:13rem}.as-filters__spacer{flex:1 0 0}.as-filters__more{display:inline-flex;flex:none;align-items:center;gap:var(--hm-space-nano);box-sizing:border-box;block-size:var(--hm-control-height);padding-inline:var(--hm-space-xs);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface-raised);color:var(--hm-text);font:inherit;font-size:var(--hm-font-size-sm);white-space:nowrap;cursor:pointer}.as-filters__more:hover{background:var(--hm-hover)}.as-filters__more:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color)}.as-filters__more[aria-expanded=true],.as-filters__more--on{border-color:var(--hm-action);color:var(--hm-action);font-weight:var(--hm-font-weight-semibold)}.as-suggestions__status{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xxxs) var(--hm-space-sm)}.as-suggestions__hint{flex:1 1 20rem;margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.as-suggestions__count{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.as-suggestions__filters{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs) var(--hm-space-sm)}.as-suggestions__search{flex:1 1 16rem;min-inline-size:0}.as-suggestions__select{flex:0 1 11rem;min-inline-size:9rem}.as-board{--hm-kanban-column-width: 17.5rem}.as-board ::ng-deep .hm-kanban__board{min-block-size:max(26rem,100dvh - 24rem)}.as-board ::ng-deep .hm-kanban-column{--as-status: var(--hm-border);flex:1 0 var(--hm-kanban-column-width);max-inline-size:36rem;gap:var(--hm-space-xs);border-block-start:var(--hm-border-thin) solid var(--as-status);background:color-mix(in srgb,var(--as-status) 9%,var(--hm-kanban-column-bg))}.as-board ::ng-deep .hm-kanban-column--collapsed{flex:0 0 var(--hm-kanban-collapsed-width)}.as-board ::ng-deep .hm-kanban-column[data-column=em_analise]{--as-status: var(--hm-info)}.as-board ::ng-deep .hm-kanban-column[data-column=planejada]{--as-status: var(--hm-accent)}.as-board ::ng-deep .hm-kanban-column[data-column=em_desenvolvimento]{--as-status: var(--hm-warning)}.as-board ::ng-deep .hm-kanban-column[data-column=concluida]{--as-status: var(--hm-success)}.as-board ::ng-deep .hm-kanban-column__title:before{display:inline-block;inline-size:.625rem;block-size:.625rem;margin-inline-end:var(--hm-space-xs);border-radius:50%;background:var(--as-status);content:\"\"}.as-board ::ng-deep .hm-kanban-column__count{padding-inline:var(--hm-space-xs);font-weight:var(--hm-font-weight-semibold)}.as-board ::ng-deep .hm-kanban-column__summary{padding-inline:var(--hm-space-micro)}.as-board ::ng-deep .hm-kanban-column__list{gap:var(--hm-space-xs)}.as-board ::ng-deep .hm-kanban-card{padding:0;overflow:hidden}.as-board ::ng-deep .hm-kanban-card:has(.as-card--top){border-color:var(--hm-warning)}.as-colsum{display:inline-flex;align-items:center;gap:var(--hm-space-micro);white-space:nowrap}.as-colsum--late{color:var(--hm-danger);font-weight:var(--hm-font-weight-semibold)}.as-colempty{display:flex;flex-direction:column;align-items:center;gap:var(--hm-space-xxxs);padding:var(--hm-space-lg) var(--hm-space-sm);text-align:center}.as-colempty__icon{--hm-icon-size: var(--hm-control-height-large);color:var(--hm-muted);opacity:.6}.as-colempty__title{color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.as-colempty__hint{max-inline-size:16rem;color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.as-colfoot{display:flex;align-items:center;gap:var(--hm-space-nano);box-sizing:border-box;inline-size:100%;block-size:var(--hm-control-height-dense);padding-inline:var(--hm-space-xs);border:0;border-radius:var(--hm-radius-control);background:transparent;color:var(--hm-muted);font:inherit;font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold);cursor:pointer}.as-colfoot:hover{background:var(--hm-hover);color:var(--hm-text)}.as-colfoot:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color)}.as-card{display:flex;flex-direction:column;min-inline-size:0}.as-card__cover{block-size:2.5rem}.as-card__body{display:flex;flex-direction:column;gap:var(--hm-space-xxxs);padding:var(--hm-space-sm);min-inline-size:0}.as-card__top{display:flex;align-items:center;gap:var(--hm-space-xxxs) var(--hm-space-xs);min-inline-size:0}.as-card__key{flex:none;color:var(--hm-muted);font-family:var(--hm-font-mono, monospace);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold)}.as-card__menu{margin-inline-start:auto}.as-card__priority{display:inline-flex;align-items:center;gap:var(--hm-space-micro);padding:0 var(--hm-space-xxxs);border-radius:var(--hm-radius-pill);background:var(--hm-hover);color:var(--hm-text);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold);line-height:1.6;white-space:nowrap}.as-card__priority hm-icon{--hm-icon-size: .875rem;color:var(--hm-muted)}.as-card__priority[data-priority=urgente]{background:var(--hm-danger-bg);color:var(--hm-danger)}.as-card__priority[data-priority=urgente] hm-icon{color:var(--hm-danger)}.as-card__priority[data-priority=alta]{background:var(--hm-warning-bg)}.as-card__priority[data-priority=alta] hm-icon{color:var(--hm-warning)}.as-card__priority[data-priority=media]{background:var(--hm-info-bg)}.as-card__priority[data-priority=media] hm-icon{color:var(--hm-info)}.as-card__badge{display:inline-flex;align-items:center;gap:var(--hm-space-micro);padding:0 var(--hm-space-xxxs);border-radius:var(--hm-radius-pill);background:var(--hm-warning-bg);color:var(--hm-text);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold);white-space:nowrap}.as-card__title{margin-block-start:var(--hm-space-micro);padding:0;border:0;background:none;color:var(--hm-text);font:inherit;font-weight:var(--hm-font-weight-bold);line-height:var(--hm-line-height-snug);text-align:start;overflow-wrap:anywhere;cursor:pointer}.as-card__title:hover,.as-card__title:focus-visible{color:var(--hm-accent);text-decoration:underline}.as-card__context{display:inline-flex;align-items:center;gap:var(--hm-space-micro);min-inline-size:0;color:var(--hm-action);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-medium);overflow-wrap:anywhere}.as-card__context hm-icon{flex:none}.as-card__context[data-kind=melhoria] hm-icon{color:var(--hm-accent)}.as-card__context[data-kind=nova_funcionalidade] hm-icon{color:var(--hm-info)}.as-card__context[data-kind=problema] hm-icon{color:var(--hm-danger)}.as-card__labels{display:flex;flex-wrap:wrap;gap:var(--hm-space-micro)}.as-card__excerpt{display:-webkit-box;margin:0;overflow:hidden;color:var(--hm-muted);font-size:var(--hm-font-size-sm);-webkit-box-orient:vertical;-webkit-line-clamp:2;line-clamp:2}.as-card__checklist{display:flex;flex-direction:column;gap:1px;margin:var(--hm-space-micro) 0 0;padding:var(--hm-space-micro);border-radius:var(--hm-radius-control);background:var(--hm-hover);list-style:none}.as-card__check{display:flex;align-items:flex-start;gap:var(--hm-space-micro);inline-size:100%;padding:1px var(--hm-space-micro);border:0;border-radius:var(--hm-radius-control);background:none;color:var(--hm-text);font:inherit;font-size:var(--hm-font-size-sm);text-align:start;cursor:pointer}.as-card__check[aria-pressed=true] hm-icon{color:var(--hm-success)}.as-card__check:hover{background:var(--hm-hover)}.as-card__check:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color)}.as-card__done{color:var(--hm-muted);text-decoration:line-through}.as-card__due{display:inline-flex;align-self:flex-start;align-items:center;gap:var(--hm-space-micro);margin-block-start:var(--hm-space-micro);padding:0 var(--hm-space-xxxs);border-radius:var(--hm-radius-pill);background:var(--hm-hover);color:var(--hm-muted);font-size:var(--hm-font-size-xs);line-height:1.6;white-space:nowrap}.as-card__due[data-due=overdue]{background:var(--hm-danger-bg);color:var(--hm-danger);font-weight:var(--hm-font-weight-semibold)}.as-card__due[data-due=today],.as-card__due[data-due=tomorrow]{background:var(--hm-warning-bg);color:var(--hm-text);font-weight:var(--hm-font-weight-semibold)}.as-card__due[data-due=today] hm-icon,.as-card__due[data-due=tomorrow] hm-icon{color:var(--hm-warning)}.as-card__foot{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xxxs) var(--hm-space-sm);margin-block-start:var(--hm-space-xxxs);padding-block-start:var(--hm-space-xs);border-block-start:var(--hm-border-hairline) solid var(--hm-border)}.as-card__count,.as-card__date{display:inline-flex;align-items:center;gap:var(--hm-space-micro);color:var(--hm-muted);font-size:var(--hm-font-size-xs);font-variant-numeric:tabular-nums;white-space:nowrap}.as-card__count--on{color:var(--hm-text);font-weight:var(--hm-font-weight-semibold)}.as-card__count--full{color:var(--hm-success)}.as-card__count--zero{opacity:.75}.as-card__faces{display:inline-flex;align-items:center;margin-inline-start:auto}.as-card__faces>hm-account-avatar{border-radius:50%;box-shadow:0 0 0 var(--hm-border-thin) var(--hm-kanban-card-bg, var(--hm-surface-raised))}.as-card__faces>hm-account-avatar+hm-account-avatar{margin-inline-start:calc(var(--hm-space-xs) * -1)}.as-card__more{margin-inline-start:var(--hm-space-micro);color:var(--hm-muted);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold)}.as-card__nobody{display:inline-flex;color:var(--hm-muted);opacity:.7}.as-archive{display:flex;flex-direction:column;gap:var(--hm-space-xs);margin:0;padding:0;list-style:none}.as-archive[aria-busy=true]{opacity:.6}.as-archive__item{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-sm);padding:var(--hm-space-sm);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.as-archive__main{display:flex;flex:1 1 20rem;flex-direction:column;gap:var(--hm-space-xxxs);min-inline-size:0}.as-archive__tags,.as-archive__meta{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xxxs) var(--hm-space-xs)}.as-archive__meta,.as-archive__date{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.as-archive__link{padding:0;border:0;background:none;color:var(--hm-accent);font:inherit;font-size:var(--hm-font-size-sm);cursor:pointer}.as-archive__link:hover{text-decoration:underline}.as-archive__link:focus-visible{outline:var(--hm-border-thin) solid var(--hm-accent);outline-offset:var(--hm-border-thin)}@media(forced-colors:active){.as-kpi,.as-card__priority,.as-card__due{border:var(--hm-border-hairline) solid CanvasText}button.as-kpi[aria-pressed=true],.as-kpi__chip[aria-pressed=true]{outline:var(--hm-border-thin) solid Highlight}}@container (max-width: 52rem){.as-kpi--wide{grid-column:1/-1}}@container (max-width: 40rem){.as-kpis{grid-template-columns:repeat(auto-fit,minmax(9.5rem,1fr))}.as-kpi{gap:var(--hm-space-xs);padding:var(--hm-space-xs)}.as-kpi__icon{inline-size:var(--hm-control-height);block-size:var(--hm-control-height)}.as-kpi__icon hm-icon{--hm-icon-size: calc(var(--hm-control-height) * .55)}.as-kpi__value{font-size:var(--hm-font-size-xl)}.as-board{--hm-kanban-column-width: min(18rem, 85cqi)}.as-board ::ng-deep .hm-kanban__board{min-block-size:0;scroll-snap-type:x proximity;scroll-padding-inline:var(--hm-space-xs)}.as-board ::ng-deep .hm-kanban-column{flex:0 0 var(--hm-kanban-column-width);scroll-snap-align:start}.as-board ::ng-deep .hm-kanban-column--collapsed{flex:0 0 var(--hm-kanban-collapsed-width)}.as-filters__select,.as-suggestions__select{flex:1 1 9rem;max-inline-size:none}.as-filters__spacer{display:none}}\n"], dependencies: [{ kind: "component", type: AccountAvatarComponent, selector: "hm-account-avatar", inputs: ["name", "imageUrl", "size", "loading"] }, { kind: "component", type: AlertComponent, selector: "hm-alert", inputs: ["variant", "appearance", "heading", "closable", "size", "showIcon", "link", "linkLabel", "linkTarget", "dismissKey", "duration"], outputs: ["closed"] }, { kind: "component", type: ButtonComponent, selector: "hm-button", inputs: ["emphasis", "intent", "size", "type", "icon", "iconDirection", "iconEnd", "disabled", "loading", "loadingState", "block", "link", "newTab", "caret", "inMenu", "toggle", "pressed"], outputs: ["loadingStateChange", "pressedChange", "clicked"] }, { kind: "component", type: EmptyComponent, selector: "hm-empty", inputs: ["heading", "description", "icon", "appearance", "size", "headingLevel"] }, { kind: "component", type: IconButtonComponent, selector: "hm-icon-button", inputs: ["label", "icon", "solid", "iconDirection", "emphasis", "intent", "size", "disabled", "toggle", "pressed", "keyShortcuts"], outputs: ["pressedChange", "clicked"] }, { kind: "component", type: IconComponent, selector: "hm-icon", inputs: ["shape", "direction", "size", "solid", "badge", "status", "flip", "inverse", "label"] }, { kind: "directive", type: KanbanCardDirective, selector: "ng-template[hmKanbanCard]" }, { kind: "directive", type: KanbanColumnActionsDirective, selector: "ng-template[hmKanbanColumnActions]" }, { kind: "directive", type: KanbanColumnEmptyDirective, selector: "ng-template[hmKanbanColumnEmpty]" }, { kind: "directive", type: KanbanColumnFooterDirective, selector: "ng-template[hmKanbanColumnFooter]" }, { kind: "directive", type: KanbanColumnSummaryDirective, selector: "ng-template[hmKanbanColumnSummary]" }, { kind: "component", type: KanbanComponent, selector: "hm-kanban", inputs: ["columns", "collapsed", "canMove", "itemKey", "itemLabel", "label", "collapsible", "headingLevel", "disabled"], outputs: ["columnsChange", "collapsedChange", "moved"] }, { kind: "component", type: MenuButtonComponent, selector: "hm-menu-button", inputs: ["label", "icon", "items", "showArrow", "align", "side", "size", "shape", "layer", "closeOnCheck", "disabled"], outputs: ["chosen", "checkedChange"] }, { kind: "component", type: PaginatorComponent, selector: "hm-paginator", inputs: ["state", "totalItems", "variant", "label", "showPage", "hasNext", "hasPrevious", "sizes", "maxNumbers"], outputs: ["stateChanged", "nextPage", "previousPage"] }, { kind: "component", type: PopoverComponent, selector: "hm-popover", inputs: ["open", "heading", "headingLevel", "label", "side", "align", "arrow", "closable", "closeLabel", "closeOnOutsideClick", "openOn", "openDelay", "closeDelay"], outputs: ["openChange"] }, { kind: "component", type: SearchFieldComponent, selector: "hm-search-field", inputs: ["value", "label", "labelHidden", "placeholder", "hint", "size", "debounce", "landmark", "loading"], outputs: ["valueChange", "search", "cleared"] }, { kind: "component", type: SegmentedControlComponent, selector: "hm-segmented-control", inputs: ["options", "value", "label", "size", "emphasis"], outputs: ["valueChange"] }, { kind: "component", type: SelectComponent, selector: "hm-select", inputs: ["value", "label", "labelHidden", "hint"], outputs: ["valueChange"] }, { kind: "component", type: SuggestionCoverPickerComponent, selector: "spine-suggestion-cover-picker", inputs: ["value"], outputs: ["picked"] }, { kind: "component", type: SuggestionMergePickerComponent, selector: "spine-suggestion-merge-picker", inputs: ["clientId", "source", "candidates"], outputs: ["merged", "closed"] }, { kind: "component", type: SuggestionTaskDetailComponent, selector: "spine-suggestion-task-detail", inputs: ["clientId", "task", "draft", "draftStatus", "labels", "canOpenScreen"], outputs: ["changed", "created", "removed", "closed", "labelCreated", "mergeRequested", "openTask", "unmerged"] }, { kind: "component", type: TagComponent, selector: "hm-tag", inputs: ["name", "color", "compact"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
2079
+ }
2080
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AutoServiceSuggestionsComponent, decorators: [{
2081
+ type: Component,
2082
+ args: [{ selector: 'spine-auto-service-suggestions', imports: [
2083
+ AccountAvatarComponent, AlertComponent, ButtonComponent, EmptyComponent, IconButtonComponent, IconComponent,
2084
+ KanbanCardDirective, KanbanColumnActionsDirective, KanbanColumnEmptyDirective, KanbanColumnFooterDirective,
2085
+ KanbanColumnSummaryDirective, KanbanComponent, MenuButtonComponent, PaginatorComponent, PopoverComponent,
2086
+ SearchFieldComponent, SegmentedControlComponent, SelectComponent, SuggestionCoverPickerComponent,
2087
+ SuggestionMergePickerComponent, SuggestionTaskDetailComponent, TagComponent,
2088
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"as-stack as-suggestions\">\n <!-- Quadro x Arquivo, o atualizar e a nova tarefa (com o \"\u25BE\" para criar direto numa coluna). -->\n <div class=\"as-suggestions__bar\">\n <hm-segmented-control [label]=\"text.viewLabel\" [options]=\"modeOptions\" [value]=\"mode()\" (valueChange)=\"setMode($event)\" />\n <span class=\"as-suggestions__spacer\"></span>\n <hm-icon-button emphasis=\"subtle\" icon=\"refresh\" [label]=\"text.refresh\" (clicked)=\"refresh()\" />\n <span class=\"as-split\" role=\"group\" [attr.aria-label]=\"text.newTask\">\n <hm-button icon=\"plus\" (clicked)=\"newTask()\">{{ text.newTask }}</hm-button>\n <hm-menu-button class=\"as-split__more\" icon=\"chevron-down\" [showArrow]=\"false\" align=\"end\" [label]=\"text.newTaskOptions\"\n [items]=\"newTaskItems\" (chosen)=\"newTask($any($event))\" />\n </span>\n </div>\n\n @if (mode() === 'board') {\n @if (loadError()) {\n <hm-alert variant=\"error\">{{ loadError() }}</hm-alert>\n } @else {\n <!-- Indicadores do quadro inteiro (o filtro n\u00E3o mexe neles). Os bot\u00F5es filtram: vencidas, vencendo em breve,\n atribu\u00EDdas a mim e cada prioridade da legenda. Cor sempre com texto junto. -->\n <section class=\"as-kpis\" [attr.aria-label]=\"text.kpis\" [attr.aria-busy]=\"kpiLoading() ? 'true' : null\">\n <button type=\"button\" class=\"as-kpi\" data-tone=\"danger\" [attr.aria-pressed]=\"due() === 'overdue'\"\n (click)=\"toggleKpi('overdue', due() !== 'overdue')\">\n <span class=\"as-kpi__icon\" aria-hidden=\"true\"><hm-icon shape=\"alarm-clock\" /></span>\n <span class=\"as-kpi__body\">\n <span class=\"as-kpi__label\">{{ text.kpiOverdue }}</span>\n <span class=\"as-kpi__value\">{{ kpiLoading() ? '\u2014' : stats().overdue }}</span>\n <span class=\"as-kpi__note\" [class.as-kpi__note--up]=\"stats().overdueNew > 0\">\n @if (stats().overdueNew) { <hm-icon shape=\"arrow-up\" /> }{{ overdueNote() }}\n </span>\n </span>\n </button>\n\n <button type=\"button\" class=\"as-kpi\" data-tone=\"warning\" [attr.aria-pressed]=\"due() === 'soon'\"\n (click)=\"toggleKpi('soon', due() !== 'soon')\">\n <span class=\"as-kpi__icon\" aria-hidden=\"true\"><hm-icon shape=\"hourglass\" /></span>\n <span class=\"as-kpi__body\">\n <span class=\"as-kpi__label\">{{ text.kpiSoon }}</span>\n <span class=\"as-kpi__value\">{{ kpiLoading() ? '\u2014' : stats().soon }}</span>\n <span class=\"as-kpi__note\">{{ text.dueSoonFilter }}</span>\n </span>\n </button>\n\n <div class=\"as-kpi\" data-tone=\"info\">\n <span class=\"as-kpi__icon\" aria-hidden=\"true\"><hm-icon [shape]=\"statusIcons.em_desenvolvimento\" /></span>\n <span class=\"as-kpi__body\">\n <span class=\"as-kpi__label\">{{ text.kpiOpen }}</span>\n <span class=\"as-kpi__value\">{{ kpiLoading() ? '\u2014' : stats().open }}</span>\n <span class=\"as-kpi__parts\">\n @for (part of openParts(); track part.status) {\n <span class=\"as-kpi__part\" [attr.data-status]=\"part.status\"><span class=\"as-kpi__dot\" aria-hidden=\"true\"></span>{{ part.text }}</span>\n }\n </span>\n </span>\n </div>\n\n <div class=\"as-kpi\" data-tone=\"success\">\n <span class=\"as-kpi__icon\" aria-hidden=\"true\"><hm-icon [shape]=\"statusIcons.concluida\" /></span>\n <span class=\"as-kpi__body\">\n <span class=\"as-kpi__label\">{{ doneLabel() }}</span>\n <span class=\"as-kpi__value\">{{ kpiLoading() ? '\u2014' : stats().concluida }}</span>\n <span class=\"as-kpi__note\" [class.as-kpi__note--good]=\"stats().doneToday > 0\">\n @if (stats().doneToday) { <hm-icon shape=\"arrow-up\" /> }{{ doneNote() }}\n </span>\n </span>\n </div>\n\n <button type=\"button\" class=\"as-kpi\" data-tone=\"accent\" [attr.aria-pressed]=\"assignee() === 'me'\"\n (click)=\"toggleKpi('mine', assignee() !== 'me')\">\n <span class=\"as-kpi__icon\" aria-hidden=\"true\"><hm-icon shape=\"user-checkmark\" /></span>\n <span class=\"as-kpi__body\">\n <span class=\"as-kpi__label\">{{ text.kpiMine }}</span>\n <span class=\"as-kpi__value\">{{ kpiLoading() ? '\u2014' : stats().mine }}</span>\n <span class=\"as-kpi__note\">{{ text.kpiMineHint }}</span>\n </span>\n </button>\n\n <div class=\"as-kpi as-kpi--wide\" data-tone=\"neutral\">\n <span class=\"as-kpi__icon\" aria-hidden=\"true\"><hm-icon shape=\"bar-chart\" /></span>\n <span class=\"as-kpi__body\">\n <span class=\"as-kpi__label\">{{ text.kpiPriority }}</span>\n @if (stats().open) {\n <span class=\"as-kpi__bar\" aria-hidden=\"true\">\n @for (p of stats().byPriority; track p.priority) {\n @if (p.count) {\n <span class=\"as-kpi__seg\" [attr.data-priority]=\"p.priority\" [style.inline-size.%]=\"priorityShare(p.count)\"></span>\n }\n }\n </span>\n <span class=\"as-kpi__legend\">\n @for (p of stats().byPriority; track p.priority) {\n <button type=\"button\" class=\"as-kpi__chip\" [attr.data-priority]=\"p.priority\" [attr.aria-pressed]=\"priority() === p.priority\"\n [disabled]=\"!p.count && priority() !== p.priority\" (click)=\"togglePriority(p.priority)\">\n <span class=\"as-kpi__dot\" aria-hidden=\"true\"></span>{{ priorityPart(p.priority, p.count) }}\n </button>\n }\n </span>\n } @else {\n <span class=\"as-kpi__note\">{{ text.kpiPriorityNone }}</span>\n }\n </span>\n </div>\n </section>\n\n <!-- Filtros numa linha: busca larga, os mais usados e \"Mais filtros\" (tela, quem pediu); \u00E0 direita, a ordena\u00E7\u00E3o.\n Valem sobre a lista inteira: a posi\u00E7\u00E3o gravada ao arrastar \u00E9 sempre a da coluna inteira. -->\n <div class=\"as-filters\" role=\"group\" [attr.aria-label]=\"text.filters\">\n <hm-search-field class=\"as-filters__search\" [label]=\"text.search\" labelHidden [placeholder]=\"text.search\"\n [landmark]=\"false\" [debounce]=\"150\" [value]=\"search()\" (search)=\"search.set($event)\" />\n <hm-select class=\"as-filters__select\" [label]=\"text.assigneeFilter\" labelHidden [(value)]=\"assignee\">\n <option value=\"\">{{ text.allAssignees }}</option>\n <option value=\"me\">{{ text.assigneeMine }}</option>\n <option value=\"none\">{{ text.assigneeNone }}</option>\n @for (p of assigneeOptions(); track p.user_id) {\n <option [value]=\"p.user_id\">{{ personName(p) }}</option>\n }\n </hm-select>\n <hm-select class=\"as-filters__select\" [label]=\"text.kindFilter\" labelHidden [(value)]=\"kind\">\n <option value=\"\">{{ text.allKinds }}</option>\n @for (k of kinds; track k) {\n <option [value]=\"k\">{{ text.kinds[k] }}</option>\n }\n </hm-select>\n <hm-select class=\"as-filters__select\" [label]=\"text.statusFilter\" labelHidden [(value)]=\"status\">\n <option value=\"\">{{ text.allBoardStatuses }}</option>\n @for (st of boardStatuses; track st) {\n <option [value]=\"st\">{{ text.statuses[st] }}</option>\n }\n </hm-select>\n <hm-select class=\"as-filters__select\" [label]=\"text.priorityFilter\" labelHidden [(value)]=\"priority\">\n <option value=\"\">{{ text.allPriorities }}</option>\n @for (p of priorities; track p) {\n <option [value]=\"p\">{{ text.priorities[p] }}</option>\n }\n </hm-select>\n <hm-select class=\"as-filters__select\" [label]=\"text.labelFilter\" labelHidden [(value)]=\"label\">\n <option value=\"\">{{ text.allLabels }}</option>\n @for (l of catalog(); track l.id) {\n <option [value]=\"l.id\">{{ l.name }}</option>\n }\n </hm-select>\n <hm-select class=\"as-filters__select\" [label]=\"text.dueFilter\" labelHidden [(value)]=\"due\">\n <option value=\"\">{{ text.dueAny }}</option>\n <option value=\"overdue\">{{ text.dueOverdueFilter }}</option>\n <option value=\"soon\">{{ text.dueSoonFilter }}</option>\n </hm-select>\n <button type=\"button\" class=\"as-filters__more\" [class.as-filters__more--on]=\"extraFilters() > 0\"\n [attr.aria-expanded]=\"moreFilters()\" [attr.aria-controls]=\"moreFilters() ? 'as-more-filters' : null\"\n (click)=\"moreFilters.set(!moreFilters())\">\n <hm-icon shape=\"filter\" />{{ moreFiltersText() }}\n </button>\n <span class=\"as-filters__spacer\"></span>\n <hm-select class=\"as-filters__select as-filters__sort\" [label]=\"text.sort\" labelHidden [value]=\"sort()\" (valueChange)=\"sort.set($any($event))\">\n @for (k of sortKeys; track k) {\n <option [value]=\"k\">{{ text.sort }}: {{ sortLabel(k) }}</option>\n }\n </hm-select>\n </div>\n @if (moreFilters()) {\n <div class=\"as-filters as-filters--more\" id=\"as-more-filters\">\n <hm-select class=\"as-filters__select\" [label]=\"text.screenFilter\" labelHidden [(value)]=\"screen\">\n <option value=\"\">{{ text.allScreens }}</option>\n @for (sc of screens(); track sc.path) {\n <option [value]=\"sc.path\">{{ sc.label }}</option>\n }\n </hm-select>\n <hm-select class=\"as-filters__select\" [label]=\"text.authorFilter\" labelHidden [(value)]=\"author\">\n <option value=\"\">{{ text.allAuthors }}</option>\n @for (p of authors(); track p.user_id) {\n <option [value]=\"p.user_id\">{{ personName(p) }}</option>\n }\n </hm-select>\n </div>\n }\n <div class=\"as-suggestions__status\">\n @if (sort() !== 'position') {\n <p class=\"as-suggestions__hint\">{{ text.sortHint }}</p>\n }\n @if (isFiltered()) {\n <span class=\"as-suggestions__count\" aria-live=\"polite\">{{ filteredText() }}</span>\n <hm-button size=\"small\" emphasis=\"minimal\" icon=\"filter-close\" (clicked)=\"clearFilters()\">{{ text.clearFilters }}</hm-button>\n }\n </div>\n\n @if (!loading() && all().length === 0) {\n <hm-empty icon=\"lightbulb\" [heading]=\"text.boardEmpty\" [description]=\"text.boardEmptyHint\" />\n }\n\n <hm-kanban class=\"as-board\" [label]=\"text.boardLabel\" [columns]=\"columns()\" [itemKey]=\"itemKey\" [itemLabel]=\"itemLabel\"\n (moved)=\"onMoved($event)\">\n <!-- \"+\" no cabe\u00E7alho: abre o cadastro completo (o modal da \"Nova tarefa\") j\u00E1 nesta coluna. -->\n <ng-template hmKanbanColumnActions let-column>\n <hm-icon-button size=\"small\" emphasis=\"subtle\" icon=\"plus\" [label]=\"newTaskInLabel(column.id)\" (clicked)=\"newTask(column.id)\" />\n </ng-template>\n\n <!-- Logo abaixo do cabe\u00E7alho: h\u00E1 quanto tempo a mais antiga est\u00E1 no status e quantas venceram. -->\n <ng-template hmKanbanColumnSummary let-column>\n @if (columnSummaries().get(column.id); as sum) {\n @if (sum.oldest) {\n <span class=\"as-colsum\"><hm-icon shape=\"clock\" />{{ sum.oldest }}</span>\n }\n @if (sum.overdue) {\n <span class=\"as-colsum as-colsum--late\"><hm-icon shape=\"alarm-clock\" />{{ sum.overdue }}</span>\n }\n }\n </ng-template>\n\n <!-- Coluna vazia: \u00EDcone grande, a frase e o que costuma cair nela (com filtro, \"nenhuma com estes filtros\"). -->\n <ng-template hmKanbanColumnEmpty let-column>\n <span class=\"as-colempty\">\n <hm-icon class=\"as-colempty__icon\" [shape]=\"emptyIcon(column.id)\" />\n <span class=\"as-colempty__title\">{{ text.columnEmpty }}</span>\n <span class=\"as-colempty__hint\">{{ emptyHint(column.id) }}</span>\n </span>\n </ng-template>\n\n <!-- Rodap\u00E9: \"+ Criar tarefa\" abre o mesmo cadastro, com o status da coluna j\u00E1 escolhido. -->\n <ng-template hmKanbanColumnFooter let-column>\n <button type=\"button\" class=\"as-colfoot\" (click)=\"newTask(column.id)\">\n <hm-icon shape=\"plus\" />{{ text.createTask }}<span class=\"as-sr-only\">: {{ column.title }}</span>\n </button>\n </ng-template>\n\n <ng-template hmKanbanCard let-item>\n <div class=\"as-card\" [class.as-card--top]=\"isTop(item)\">\n @if (item.cover) {\n <div class=\"as-card__cover\" [class]=\"'as-tok--' + item.cover\" aria-hidden=\"true\"></div>\n }\n <div class=\"as-card__body\">\n <div class=\"as-card__top\">\n <span class=\"as-card__key\">{{ item.key }}</span>\n <span class=\"as-card__priority\" [attr.data-priority]=\"item.priority\">\n <hm-icon [shape]=\"priorityIcon(item)\" /><span class=\"as-sr-only\">{{ text.priority }}: </span>{{ priorityName(item) }}\n </span>\n @if (isTop(item)) {\n <span class=\"as-card__badge\"><hm-icon shape=\"flame\" />{{ text.topVoted }}</span>\n }\n <span #menuAnchor class=\"as-card__menu\">\n <hm-menu-button size=\"small\" icon=\"ellipsis-vertical\" [showArrow]=\"false\" align=\"end\" [label]=\"menuLabel(item)\"\n [items]=\"menuOf(item)\" (chosen)=\"onMenu(item, $event, menuAnchor)\" />\n </span>\n </div>\n <button type=\"button\" class=\"as-card__title\" (click)=\"openDetail(item)\">{{ item.title }}</button>\n <span class=\"as-card__context\" [attr.data-kind]=\"item.kind\"><hm-icon [shape]=\"kindIcon(item)\" />{{ contextOf(item) }}</span>\n @if (item.labels.length) {\n <div class=\"as-card__labels\">\n @for (l of item.labels; track l.id) {\n <span class=\"as-label\" [class]=\"'as-tok--' + l.color\">{{ l.name }}</span>\n }\n </div>\n }\n @if (excerpt(item); as ex) {\n <p class=\"as-card__excerpt\">{{ ex }}</p>\n }\n @if (cardItems(item).length) {\n <ul class=\"as-card__checklist\">\n @for (c of cardItems(item); track c.id) {\n <li>\n <button type=\"button\" class=\"as-card__check\" [attr.aria-pressed]=\"c.done\" (click)=\"toggleCardItem(item, c)\">\n <hm-icon [shape]=\"c.done ? 'check-circle' : 'circle-empty'\" /><span [class.as-card__done]=\"c.done\">{{ c.text }}</span>\n </button>\n </li>\n }\n </ul>\n }\n @if (item.due_date) {\n <span class=\"as-card__due\" [attr.data-due]=\"dueOf(item) ?? 'closed'\"><hm-icon [shape]=\"dueIcon(item)\" />{{ dueText(item) }}</span>\n }\n <div class=\"as-card__foot\">\n <span class=\"as-card__count\" [class.as-card__count--zero]=\"!item.comments_count\" [attr.title]=\"commentsText(item.comments_count)\">\n <hm-icon shape=\"comment-message\" /><span aria-hidden=\"true\">{{ item.comments_count }}</span><span class=\"as-sr-only\">{{ commentsText(item.comments_count) }}</span>\n </span>\n @if (item.checklist.total) {\n <span class=\"as-card__count\" [class.as-card__count--full]=\"item.checklist.done === item.checklist.total\" [attr.title]=\"checklistText(item)\">\n <hm-icon shape=\"list-check\" /><span aria-hidden=\"true\">{{ item.checklist.done }}/{{ item.checklist.total }}</span><span class=\"as-sr-only\">{{ checklistText(item) }}</span>\n </span>\n }\n <span class=\"as-card__count\" [class.as-card__count--on]=\"item.votes > 0\" [class.as-card__count--zero]=\"!item.votes\" [attr.title]=\"votesText(item.votes)\">\n <hm-icon shape=\"thumbs-up\" /><span aria-hidden=\"true\">{{ item.votes }}</span><span class=\"as-sr-only\">{{ votesText(item.votes) }}</span>\n </span>\n <span class=\"as-card__date\" [attr.title]=\"createdText(item)\">\n <hm-icon shape=\"clock\" /><span aria-hidden=\"true\">{{ date(item.created_at) }}</span><span class=\"as-sr-only\">{{ createdText(item) }}</span>\n </span>\n <span class=\"as-card__faces\">\n @for (p of faces(item); track p.user_id) {\n <hm-account-avatar size=\"small\" [name]=\"personName(p)\" [imageUrl]=\"p.picture\" [attr.title]=\"personName(p)\" />\n } @empty {\n <span class=\"as-card__nobody\" [attr.title]=\"text.assigneeNone\">\n <hm-icon shape=\"user-circle\" /><span class=\"as-sr-only\">{{ text.assigneeNone }}</span>\n </span>\n }\n @if (moreFaces(item); as more) { <span class=\"as-card__more\">{{ more }}</span> }\n </span>\n </div>\n </div>\n </div>\n </ng-template>\n </hm-kanban>\n }\n } @else {\n <!-- Arquivo: arquivadas, recusadas e conclu\u00EDdas fora da janela do quadro, paginado no servidor. -->\n <div class=\"as-suggestions__filters\">\n <hm-search-field class=\"as-suggestions__search\" [label]=\"text.archiveSearch\" labelHidden [placeholder]=\"text.archiveSearch\"\n [landmark]=\"false\" [debounce]=\"300\" [value]=\"archiveSearch()\" (search)=\"onArchiveSearch($event)\" />\n <hm-select class=\"as-suggestions__select\" [label]=\"text.statusFilter\" labelHidden [value]=\"archiveStatus()\"\n (valueChange)=\"onArchiveFilter('status', $any($event))\">\n <option value=\"\">{{ text.statusFilter }}: {{ text.allStatuses }}</option>\n @for (st of statuses; track st) {\n <option [value]=\"st\">{{ text.statuses[st] }}</option>\n }\n </hm-select>\n <hm-select class=\"as-suggestions__select\" [label]=\"text.kindFilter\" labelHidden [value]=\"archiveKind()\"\n (valueChange)=\"onArchiveFilter('kind', $any($event))\">\n <option value=\"\">{{ text.allKinds }}</option>\n @for (k of kinds; track k) {\n <option [value]=\"k\">{{ text.kinds[k] }}</option>\n }\n </hm-select>\n @if (archiveTotal()) {\n <span class=\"as-suggestions__count\" aria-live=\"polite\">{{ archiveCountText() }}</span>\n }\n </div>\n\n @if (archiveError()) {\n <hm-alert variant=\"error\">{{ archiveError() }}</hm-alert>\n } @else if (!archiveLoading() && archived().length === 0) {\n <hm-empty icon=\"archive\" [heading]=\"text.archiveEmpty\" [description]=\"archiveEmptyHint()\" />\n } @else {\n <ul class=\"as-archive\" [attr.aria-label]=\"text.archiveLabel\" [attr.aria-busy]=\"archiveLoading() ? 'true' : null\">\n @for (s of archived(); track s.id) {\n <li class=\"as-archive__item\">\n <div class=\"as-archive__main\">\n <div class=\"as-archive__tags\">\n <span class=\"as-card__key\">{{ s.key }}</span>\n <hm-tag [name]=\"text.statuses[s.status]\" [color]=\"statusColors[s.status]\" />\n <hm-tag [name]=\"text.kinds[s.kind]\" [color]=\"kindColors[s.kind]\" />\n @if (s.merged_into; as into) {\n <!-- Agrupada: no arquivo, com o destino; os coment\u00E1rios ficam nela. -->\n <hm-tag [name]=\"text.mergedTag\" color=\"info\" />\n <button type=\"button\" class=\"as-archive__link\" (click)=\"openTask(into.id)\">{{ mergedIntoText(into.key) }}</button>\n } @else {\n <span class=\"as-archive__date\">{{ closedOn(s) }}</span>\n }\n </div>\n <button type=\"button\" class=\"as-card__title\" (click)=\"openDetail(s)\">{{ s.title }}</button>\n <div class=\"as-archive__meta\">\n <hm-account-avatar size=\"small\" [name]=\"personName(s.author)\" [imageUrl]=\"s.author?.picture\" />\n <span>{{ personName(s.author) }}</span>\n <span aria-hidden=\"true\">\u00B7</span>\n <span>{{ s.screen.label || text.noScreen }}</span>\n @if (s.votes) {\n <span aria-hidden=\"true\">\u00B7</span>\n <span>{{ votesText(s.votes) }}</span>\n }\n </div>\n </div>\n @if (s.merged_into) {\n <hm-button size=\"small\" emphasis=\"medium\" icon=\"unlink\" [loading]=\"reopening() === s.id\" (clicked)=\"unmergeFromArchive(s)\">\n {{ text.unmerge }}\n </hm-button>\n } @else if (s.archived_at) {\n <hm-button size=\"small\" emphasis=\"medium\" icon=\"unarchive\" [loading]=\"reopening() === s.id\" (clicked)=\"reopen(s)\">\n {{ text.menuRestore }}\n </hm-button>\n } @else if (s.status === 'recusada') {\n <hm-button size=\"small\" emphasis=\"medium\" icon=\"unarchive\" [loading]=\"reopening() === s.id\" (clicked)=\"reopen(s)\">\n {{ text.reopen }}\n </hm-button>\n }\n </li>\n }\n </ul>\n @if (archiveTotal() > archivePage().size) {\n <hm-paginator variant=\"simple\" [label]=\"text.pagination\" [state]=\"archivePage()\" [totalItems]=\"archiveTotal()\"\n [sizes]=\"[20, 50]\" (stateChanged)=\"onArchivePage($event)\" />\n }\n }\n }\n</div>\n\n<!-- Capa pelo \"\u22EF\" do cart\u00E3o: o popover abre ancorado no bot\u00E3o do cart\u00E3o escolhido. -->\n<hm-popover #coverPop [label]=\"text.cover\">\n <spine-suggestion-cover-picker [value]=\"coverTarget()?.cover ?? null\" (picked)=\"pickCover($event)\" />\n</hm-popover>\n\n<spine-suggestion-task-detail [clientId]=\"clientId()\" [task]=\"selected()\" [draft]=\"drafting()\" [draftStatus]=\"draftStatus()\"\n [labels]=\"catalog()\" [canOpenScreen]=\"canOpenScreen()\"\n (changed)=\"replace($event)\" (created)=\"onCreated($event)\" (removed)=\"onRemoved($event)\"\n (closed)=\"closeDetail()\" (labelCreated)=\"onLabelCreated($event)\"\n (mergeRequested)=\"startMerge($event)\" (openTask)=\"openTask($event)\" (unmerged)=\"onUnmerged()\" />\n\n<!-- \"Agrupar em\u2026\" (do \"\u22EF\" do cart\u00E3o ou do detalhe): as abertas do quadro, com a pr\u00E9via do servidor. -->\n<spine-suggestion-merge-picker [clientId]=\"clientId()\" [source]=\"mergeSource()\" [candidates]=\"all()\"\n (merged)=\"onMerged($event)\" (closed)=\"mergeSource.set(null)\" />\n", styles: [".as-stack{display:flex;flex-direction:column;gap:var(--hm-space-md);min-inline-size:0}.as-actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--hm-space-xs)}.as-field-row{display:flex;flex-wrap:wrap;align-items:flex-end;gap:var(--hm-space-sm)}.as-field-row>hm-combobox{flex:1 1 16rem;min-inline-size:0}.as-summary{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-sm)}.as-summary__info{display:flex;flex:1 1 16rem;flex-direction:column;gap:var(--hm-space-micro);min-inline-size:0}.as-summary__title{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs);margin:0;color:var(--hm-text);font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-semibold)}.as-summary__detail{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.muted{color:var(--hm-muted)}.as-empty{margin:var(--hm-space-xs) 0 0;font-size:var(--hm-font-size-sm)}.as-secret{display:grid;gap:var(--hm-space-micro)}.as-secret__label{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.as-secret__row{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.as-secret__value{flex:1 1 240px;min-width:0;border:var(--hm-border-hairline) solid var(--hm-divider);border-radius:var(--hm-radius-control);padding:var(--hm-space-xs) var(--hm-space-sm);background:var(--hm-surface-muted);color:var(--hm-text);font-family:var(--hm-font-mono);font-size:var(--hm-font-size-sm);overflow-wrap:anywhere;-webkit-user-select:all;user-select:all}.as-tok--cat-1{--as-tok: var(--hm-data-categorical-1);background:var(--as-tok)}.as-tok--cat-2{--as-tok: var(--hm-data-categorical-2);background:var(--as-tok)}.as-tok--cat-3{--as-tok: var(--hm-data-categorical-3);background:var(--as-tok)}.as-tok--cat-4{--as-tok: var(--hm-data-categorical-4);background:var(--as-tok)}.as-tok--cat-5{--as-tok: var(--hm-data-categorical-5);background:var(--as-tok)}.as-tok--cat-6{--as-tok: var(--hm-data-categorical-6);background:var(--as-tok)}.as-tok--cat-7{--as-tok: var(--hm-data-categorical-7);background:var(--as-tok)}.as-tok--cat-8{--as-tok: var(--hm-data-categorical-8);background:var(--as-tok)}.as-tok--grad-1{--as-tok: var(--hm-data-categorical-1);background:linear-gradient(135deg,var(--hm-data-categorical-1),var(--hm-data-categorical-2))}.as-tok--grad-2{--as-tok: var(--hm-data-categorical-3);background:linear-gradient(135deg,var(--hm-data-categorical-3),var(--hm-data-categorical-4))}.as-tok--grad-3{--as-tok: var(--hm-data-categorical-5);background:linear-gradient(135deg,var(--hm-data-categorical-5),var(--hm-data-categorical-6))}.as-tok--grad-4{--as-tok: var(--hm-data-categorical-7);background:linear-gradient(135deg,var(--hm-data-categorical-7),var(--hm-data-categorical-8))}.as-tok--grad-5{--as-tok: var(--hm-data-categorical-1);background:linear-gradient(135deg,var(--hm-data-categorical-1),var(--hm-data-categorical-5))}.as-tok--grad-6{--as-tok: var(--hm-data-categorical-2);background:linear-gradient(135deg,var(--hm-data-categorical-2),var(--hm-data-categorical-6))}.as-label{display:inline-flex;align-items:center;gap:var(--hm-space-micro);max-inline-size:100%;padding:0 var(--hm-space-xxxs);border-radius:var(--hm-radius-pill);background:color-mix(in srgb,var(--as-tok, var(--hm-border)) 18%,var(--hm-surface-raised));color:var(--hm-text);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold);line-height:1.6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.as-label:before{flex:none;inline-size:.5rem;block-size:.5rem;border-radius:50%;background:var(--as-tok, var(--hm-border));content:\"\"}@media(forced-colors:active){.as-label{border:var(--hm-border-hairline) solid CanvasText}}:host{display:block;container-type:inline-size}.as-sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}.as-suggestions__bar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-sm)}.as-suggestions__spacer{flex:1}.as-split{display:inline-flex;align-items:stretch}.as-split ::ng-deep hm-button .hm-btn{border-start-end-radius:0;border-end-end-radius:0}.as-split ::ng-deep .as-split__more .hm-menu-button{--hm-icon-btn-color: var(--hm-on-action);--hm-icon-btn-hover: var(--hm-action-hover);block-size:var(--hm-control-height);border-inline-start:var(--hm-border-hairline) solid color-mix(in srgb,var(--hm-on-action) 40%,transparent);border-start-start-radius:0;border-end-start-radius:0;background:var(--hm-action)}.as-kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(12rem,1fr));gap:var(--hm-space-sm)}.as-kpis[aria-busy=true] .as-kpi__value{color:var(--hm-muted)}.as-kpi{--as-tone: var(--hm-muted);--as-tone-bg: var(--hm-hover);--as-tint: 0%;display:flex;align-items:flex-start;gap:var(--hm-space-sm);box-sizing:border-box;min-inline-size:0;margin:0;padding:var(--hm-space-sm);border:var(--hm-border-hairline) solid color-mix(in srgb,var(--as-tone) 35%,var(--hm-border));border-radius:var(--hm-radius);background:color-mix(in srgb,var(--as-tone) var(--as-tint),var(--hm-surface-raised));color:var(--hm-text);font:inherit;text-align:start}.as-kpi[data-tone=danger]{--as-tone: var(--hm-danger);--as-tone-bg: var(--hm-danger-bg);--as-tint: 7%}.as-kpi[data-tone=warning]{--as-tone: var(--hm-warning);--as-tone-bg: var(--hm-warning-bg);--as-tint: 7%}.as-kpi[data-tone=info]{--as-tone: var(--hm-info);--as-tone-bg: var(--hm-info-bg);--as-tint: 7%}.as-kpi[data-tone=success]{--as-tone: var(--hm-success);--as-tone-bg: var(--hm-success-bg);--as-tint: 7%}.as-kpi[data-tone=accent]{--as-tone: var(--hm-accent);--as-tone-bg: var(--hm-accent-subtle);--as-tint: 7%}.as-kpi--wide{grid-column:span 2}.as-kpi__icon{display:inline-flex;flex:none;align-items:center;justify-content:center;inline-size:var(--hm-control-height-large);block-size:var(--hm-control-height-large);border-radius:var(--hm-radius);background:var(--as-tone-bg);color:var(--as-tone)}.as-kpi__icon hm-icon{--hm-icon-size: calc(var(--hm-control-height-large) * .55)}.as-kpi__body{display:flex;flex:1;flex-direction:column;gap:var(--hm-space-micro);min-inline-size:0}.as-kpi__label{color:var(--hm-muted);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-medium)}.as-kpi__value{color:var(--hm-text);font-size:var(--hm-font-size-xxl);font-weight:var(--hm-font-weight-bold);font-variant-numeric:tabular-nums;line-height:var(--hm-line-height-tight)}.as-kpi__note{display:inline-flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-micro);color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.as-kpi__note--up{color:var(--hm-danger);font-weight:var(--hm-font-weight-semibold)}.as-kpi__note--good{color:var(--hm-success);font-weight:var(--hm-font-weight-semibold)}.as-kpi__parts,.as-kpi__legend{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-micro) var(--hm-space-xs);font-size:var(--hm-font-size-xs)}.as-kpi__part{display:inline-flex;align-items:center;gap:var(--hm-space-micro);color:var(--hm-muted);white-space:nowrap}.as-kpi__dot{flex:none;inline-size:.5rem;block-size:.5rem;border-radius:50%;background:var(--as-dot, var(--hm-muted))}.as-kpi__part[data-status=em_analise]{--as-dot: var(--hm-info)}.as-kpi__part[data-status=planejada]{--as-dot: var(--hm-accent)}.as-kpi__part[data-status=em_desenvolvimento]{--as-dot: var(--hm-warning)}.as-kpi__bar{display:flex;gap:var(--hm-border-thin);block-size:var(--hm-space-xs);margin-block:var(--hm-space-micro);overflow:hidden;border-radius:var(--hm-radius-pill);background:var(--hm-hover)}.as-kpi__seg{flex:none;min-inline-size:var(--hm-space-micro);background:var(--as-dot, var(--hm-muted))}.as-kpi [data-priority=urgente]{--as-dot: var(--hm-danger)}.as-kpi [data-priority=alta]{--as-dot: var(--hm-warning)}.as-kpi [data-priority=media]{--as-dot: var(--hm-info)}.as-kpi [data-priority=baixa]{--as-dot: var(--hm-muted)}.as-kpi__chip{display:inline-flex;align-items:center;gap:var(--hm-space-micro);padding:0 var(--hm-space-xxxs);border:var(--hm-border-hairline) solid transparent;border-radius:var(--hm-radius-pill);background:none;color:var(--hm-text);font:inherit;font-variant-numeric:tabular-nums;white-space:nowrap;cursor:pointer}.as-kpi__chip:hover:not(:disabled){background:var(--hm-hover)}.as-kpi__chip:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color)}.as-kpi__chip[aria-pressed=true]{border-color:var(--hm-text);background:var(--hm-hover);font-weight:var(--hm-font-weight-semibold)}.as-kpi__chip:disabled{color:var(--hm-muted);cursor:default}button.as-kpi{cursor:pointer;transition:border-color var(--hm-duration-quick) var(--hm-ease-standard),background var(--hm-duration-quick) var(--hm-ease-standard)}button.as-kpi:hover{border-color:var(--as-tone)}button.as-kpi:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color);outline-offset:var(--hm-border-hairline)}button.as-kpi[aria-pressed=true]{--as-tint: 16%;border-color:var(--as-tone);box-shadow:inset 0 0 0 var(--hm-border-thin) var(--as-tone)}.as-filters{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.as-filters__search{flex:3 1 16rem;min-inline-size:0}.as-filters__select{flex:1 1 9rem;min-inline-size:8rem;max-inline-size:13rem}.as-filters__spacer{flex:1 0 0}.as-filters__more{display:inline-flex;flex:none;align-items:center;gap:var(--hm-space-nano);box-sizing:border-box;block-size:var(--hm-control-height);padding-inline:var(--hm-space-xs);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface-raised);color:var(--hm-text);font:inherit;font-size:var(--hm-font-size-sm);white-space:nowrap;cursor:pointer}.as-filters__more:hover{background:var(--hm-hover)}.as-filters__more:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color)}.as-filters__more[aria-expanded=true],.as-filters__more--on{border-color:var(--hm-action);color:var(--hm-action);font-weight:var(--hm-font-weight-semibold)}.as-suggestions__status{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xxxs) var(--hm-space-sm)}.as-suggestions__hint{flex:1 1 20rem;margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.as-suggestions__count{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.as-suggestions__filters{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs) var(--hm-space-sm)}.as-suggestions__search{flex:1 1 16rem;min-inline-size:0}.as-suggestions__select{flex:0 1 11rem;min-inline-size:9rem}.as-board{--hm-kanban-column-width: 17.5rem}.as-board ::ng-deep .hm-kanban__board{min-block-size:max(26rem,100dvh - 24rem)}.as-board ::ng-deep .hm-kanban-column{--as-status: var(--hm-border);flex:1 0 var(--hm-kanban-column-width);max-inline-size:36rem;gap:var(--hm-space-xs);border-block-start:var(--hm-border-thin) solid var(--as-status);background:color-mix(in srgb,var(--as-status) 9%,var(--hm-kanban-column-bg))}.as-board ::ng-deep .hm-kanban-column--collapsed{flex:0 0 var(--hm-kanban-collapsed-width)}.as-board ::ng-deep .hm-kanban-column[data-column=em_analise]{--as-status: var(--hm-info)}.as-board ::ng-deep .hm-kanban-column[data-column=planejada]{--as-status: var(--hm-accent)}.as-board ::ng-deep .hm-kanban-column[data-column=em_desenvolvimento]{--as-status: var(--hm-warning)}.as-board ::ng-deep .hm-kanban-column[data-column=concluida]{--as-status: var(--hm-success)}.as-board ::ng-deep .hm-kanban-column__title:before{display:inline-block;inline-size:.625rem;block-size:.625rem;margin-inline-end:var(--hm-space-xs);border-radius:50%;background:var(--as-status);content:\"\"}.as-board ::ng-deep .hm-kanban-column__count{padding-inline:var(--hm-space-xs);font-weight:var(--hm-font-weight-semibold)}.as-board ::ng-deep .hm-kanban-column__summary{padding-inline:var(--hm-space-micro)}.as-board ::ng-deep .hm-kanban-column__list{gap:var(--hm-space-xs)}.as-board ::ng-deep .hm-kanban-card{padding:0;overflow:hidden}.as-board ::ng-deep .hm-kanban-card:has(.as-card--top){border-color:var(--hm-warning)}.as-colsum{display:inline-flex;align-items:center;gap:var(--hm-space-micro);white-space:nowrap}.as-colsum--late{color:var(--hm-danger);font-weight:var(--hm-font-weight-semibold)}.as-colempty{display:flex;flex-direction:column;align-items:center;gap:var(--hm-space-xxxs);padding:var(--hm-space-lg) var(--hm-space-sm);text-align:center}.as-colempty__icon{--hm-icon-size: var(--hm-control-height-large);color:var(--hm-muted);opacity:.6}.as-colempty__title{color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.as-colempty__hint{max-inline-size:16rem;color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.as-colfoot{display:flex;align-items:center;gap:var(--hm-space-nano);box-sizing:border-box;inline-size:100%;block-size:var(--hm-control-height-dense);padding-inline:var(--hm-space-xs);border:0;border-radius:var(--hm-radius-control);background:transparent;color:var(--hm-muted);font:inherit;font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold);cursor:pointer}.as-colfoot:hover{background:var(--hm-hover);color:var(--hm-text)}.as-colfoot:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color)}.as-card{display:flex;flex-direction:column;min-inline-size:0}.as-card__cover{block-size:2.5rem}.as-card__body{display:flex;flex-direction:column;gap:var(--hm-space-xxxs);padding:var(--hm-space-sm);min-inline-size:0}.as-card__top{display:flex;align-items:center;gap:var(--hm-space-xxxs) var(--hm-space-xs);min-inline-size:0}.as-card__key{flex:none;color:var(--hm-muted);font-family:var(--hm-font-mono, monospace);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold)}.as-card__menu{margin-inline-start:auto}.as-card__priority{display:inline-flex;align-items:center;gap:var(--hm-space-micro);padding:0 var(--hm-space-xxxs);border-radius:var(--hm-radius-pill);background:var(--hm-hover);color:var(--hm-text);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold);line-height:1.6;white-space:nowrap}.as-card__priority hm-icon{--hm-icon-size: .875rem;color:var(--hm-muted)}.as-card__priority[data-priority=urgente]{background:var(--hm-danger-bg);color:var(--hm-danger)}.as-card__priority[data-priority=urgente] hm-icon{color:var(--hm-danger)}.as-card__priority[data-priority=alta]{background:var(--hm-warning-bg)}.as-card__priority[data-priority=alta] hm-icon{color:var(--hm-warning)}.as-card__priority[data-priority=media]{background:var(--hm-info-bg)}.as-card__priority[data-priority=media] hm-icon{color:var(--hm-info)}.as-card__badge{display:inline-flex;align-items:center;gap:var(--hm-space-micro);padding:0 var(--hm-space-xxxs);border-radius:var(--hm-radius-pill);background:var(--hm-warning-bg);color:var(--hm-text);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold);white-space:nowrap}.as-card__title{margin-block-start:var(--hm-space-micro);padding:0;border:0;background:none;color:var(--hm-text);font:inherit;font-weight:var(--hm-font-weight-bold);line-height:var(--hm-line-height-snug);text-align:start;overflow-wrap:anywhere;cursor:pointer}.as-card__title:hover,.as-card__title:focus-visible{color:var(--hm-accent);text-decoration:underline}.as-card__context{display:inline-flex;align-items:center;gap:var(--hm-space-micro);min-inline-size:0;color:var(--hm-action);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-medium);overflow-wrap:anywhere}.as-card__context hm-icon{flex:none}.as-card__context[data-kind=melhoria] hm-icon{color:var(--hm-accent)}.as-card__context[data-kind=nova_funcionalidade] hm-icon{color:var(--hm-info)}.as-card__context[data-kind=problema] hm-icon{color:var(--hm-danger)}.as-card__labels{display:flex;flex-wrap:wrap;gap:var(--hm-space-micro)}.as-card__excerpt{display:-webkit-box;margin:0;overflow:hidden;color:var(--hm-muted);font-size:var(--hm-font-size-sm);-webkit-box-orient:vertical;-webkit-line-clamp:2;line-clamp:2}.as-card__checklist{display:flex;flex-direction:column;gap:1px;margin:var(--hm-space-micro) 0 0;padding:var(--hm-space-micro);border-radius:var(--hm-radius-control);background:var(--hm-hover);list-style:none}.as-card__check{display:flex;align-items:flex-start;gap:var(--hm-space-micro);inline-size:100%;padding:1px var(--hm-space-micro);border:0;border-radius:var(--hm-radius-control);background:none;color:var(--hm-text);font:inherit;font-size:var(--hm-font-size-sm);text-align:start;cursor:pointer}.as-card__check[aria-pressed=true] hm-icon{color:var(--hm-success)}.as-card__check:hover{background:var(--hm-hover)}.as-card__check:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color)}.as-card__done{color:var(--hm-muted);text-decoration:line-through}.as-card__due{display:inline-flex;align-self:flex-start;align-items:center;gap:var(--hm-space-micro);margin-block-start:var(--hm-space-micro);padding:0 var(--hm-space-xxxs);border-radius:var(--hm-radius-pill);background:var(--hm-hover);color:var(--hm-muted);font-size:var(--hm-font-size-xs);line-height:1.6;white-space:nowrap}.as-card__due[data-due=overdue]{background:var(--hm-danger-bg);color:var(--hm-danger);font-weight:var(--hm-font-weight-semibold)}.as-card__due[data-due=today],.as-card__due[data-due=tomorrow]{background:var(--hm-warning-bg);color:var(--hm-text);font-weight:var(--hm-font-weight-semibold)}.as-card__due[data-due=today] hm-icon,.as-card__due[data-due=tomorrow] hm-icon{color:var(--hm-warning)}.as-card__foot{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xxxs) var(--hm-space-sm);margin-block-start:var(--hm-space-xxxs);padding-block-start:var(--hm-space-xs);border-block-start:var(--hm-border-hairline) solid var(--hm-border)}.as-card__count,.as-card__date{display:inline-flex;align-items:center;gap:var(--hm-space-micro);color:var(--hm-muted);font-size:var(--hm-font-size-xs);font-variant-numeric:tabular-nums;white-space:nowrap}.as-card__count--on{color:var(--hm-text);font-weight:var(--hm-font-weight-semibold)}.as-card__count--full{color:var(--hm-success)}.as-card__count--zero{opacity:.75}.as-card__faces{display:inline-flex;align-items:center;margin-inline-start:auto}.as-card__faces>hm-account-avatar{border-radius:50%;box-shadow:0 0 0 var(--hm-border-thin) var(--hm-kanban-card-bg, var(--hm-surface-raised))}.as-card__faces>hm-account-avatar+hm-account-avatar{margin-inline-start:calc(var(--hm-space-xs) * -1)}.as-card__more{margin-inline-start:var(--hm-space-micro);color:var(--hm-muted);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold)}.as-card__nobody{display:inline-flex;color:var(--hm-muted);opacity:.7}.as-archive{display:flex;flex-direction:column;gap:var(--hm-space-xs);margin:0;padding:0;list-style:none}.as-archive[aria-busy=true]{opacity:.6}.as-archive__item{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-sm);padding:var(--hm-space-sm);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.as-archive__main{display:flex;flex:1 1 20rem;flex-direction:column;gap:var(--hm-space-xxxs);min-inline-size:0}.as-archive__tags,.as-archive__meta{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xxxs) var(--hm-space-xs)}.as-archive__meta,.as-archive__date{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.as-archive__link{padding:0;border:0;background:none;color:var(--hm-accent);font:inherit;font-size:var(--hm-font-size-sm);cursor:pointer}.as-archive__link:hover{text-decoration:underline}.as-archive__link:focus-visible{outline:var(--hm-border-thin) solid var(--hm-accent);outline-offset:var(--hm-border-thin)}@media(forced-colors:active){.as-kpi,.as-card__priority,.as-card__due{border:var(--hm-border-hairline) solid CanvasText}button.as-kpi[aria-pressed=true],.as-kpi__chip[aria-pressed=true]{outline:var(--hm-border-thin) solid Highlight}}@container (max-width: 52rem){.as-kpi--wide{grid-column:1/-1}}@container (max-width: 40rem){.as-kpis{grid-template-columns:repeat(auto-fit,minmax(9.5rem,1fr))}.as-kpi{gap:var(--hm-space-xs);padding:var(--hm-space-xs)}.as-kpi__icon{inline-size:var(--hm-control-height);block-size:var(--hm-control-height)}.as-kpi__icon hm-icon{--hm-icon-size: calc(var(--hm-control-height) * .55)}.as-kpi__value{font-size:var(--hm-font-size-xl)}.as-board{--hm-kanban-column-width: min(18rem, 85cqi)}.as-board ::ng-deep .hm-kanban__board{min-block-size:0;scroll-snap-type:x proximity;scroll-padding-inline:var(--hm-space-xs)}.as-board ::ng-deep .hm-kanban-column{flex:0 0 var(--hm-kanban-column-width);scroll-snap-align:start}.as-board ::ng-deep .hm-kanban-column--collapsed{flex:0 0 var(--hm-kanban-collapsed-width)}.as-filters__select,.as-suggestions__select{flex:1 1 9rem;max-inline-size:none}.as-filters__spacer{display:none}}\n"] }]
2089
+ }], ctorParameters: () => [], propDecorators: { clientId: [{ type: i0.Input, args: [{ isSignal: true, alias: "clientId", required: true }] }], coverPop: [{ type: i0.ViewChild, args: ['coverPop', { isSignal: true }] }] } });
2090
+
2091
+ /**
2092
+ * Tela "Sugestões" do auto-serviço (router `AUTO_SERVICE_SUGGESTIONS_TEMPLATE_ROUTER_ID`, na raiz do menu; migration
2093
+ * 0036 do argos): o seletor de client + o quadro de sugestões que era a aba Cliente › Sugestões. Carregada só por
2094
+ * `AUTO_SERVICE_SCREENS.suggestions` -- nunca exportada pelo ponto de entrada (ver `public-api.ts`).
2095
+ */
2096
+ class AutoServiceSuggestionsPageComponent {
2097
+ constructor() {
2098
+ this.client = signal(null, ...(ngDevMode ? [{ debugName: "client" }] : /* istanbul ignore next */ []));
2099
+ }
2100
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AutoServiceSuggestionsPageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2101
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: AutoServiceSuggestionsPageComponent, isStandalone: true, selector: "spine-auto-service-suggestions-page", ngImport: i0, template: `
2102
+ <div class="as-stack">
2103
+ <spine-auto-service-client-picker (clientChange)="client.set($event)" />
2104
+ @if (client(); as c) {
2105
+ <spine-auto-service-suggestions [clientId]="c.id" />
2106
+ }
2107
+ </div>
2108
+ `, isInline: true, styles: [".as-stack{display:flex;flex-direction:column;gap:var(--hm-space-md);min-inline-size:0}.as-actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--hm-space-xs)}.as-field-row{display:flex;flex-wrap:wrap;align-items:flex-end;gap:var(--hm-space-sm)}.as-field-row>hm-combobox{flex:1 1 16rem;min-inline-size:0}.as-summary{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-sm)}.as-summary__info{display:flex;flex:1 1 16rem;flex-direction:column;gap:var(--hm-space-micro);min-inline-size:0}.as-summary__title{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs);margin:0;color:var(--hm-text);font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-semibold)}.as-summary__detail{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.muted{color:var(--hm-muted)}.as-empty{margin:var(--hm-space-xs) 0 0;font-size:var(--hm-font-size-sm)}.as-secret{display:grid;gap:var(--hm-space-micro)}.as-secret__label{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.as-secret__row{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.as-secret__value{flex:1 1 240px;min-width:0;border:var(--hm-border-hairline) solid var(--hm-divider);border-radius:var(--hm-radius-control);padding:var(--hm-space-xs) var(--hm-space-sm);background:var(--hm-surface-muted);color:var(--hm-text);font-family:var(--hm-font-mono);font-size:var(--hm-font-size-sm);overflow-wrap:anywhere;-webkit-user-select:all;user-select:all}:host{display:block;min-inline-size:0}\n"], dependencies: [{ kind: "component", type: AutoServiceClientPickerComponent, selector: "spine-auto-service-client-picker", outputs: ["clientChange"] }, { kind: "component", type: AutoServiceSuggestionsComponent, selector: "spine-auto-service-suggestions", inputs: ["clientId"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
2109
+ }
2110
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AutoServiceSuggestionsPageComponent, decorators: [{
2111
+ type: Component,
2112
+ args: [{ selector: 'spine-auto-service-suggestions-page', imports: [AutoServiceClientPickerComponent, AutoServiceSuggestionsComponent], template: `
2113
+ <div class="as-stack">
2114
+ <spine-auto-service-client-picker (clientChange)="client.set($event)" />
2115
+ @if (client(); as c) {
2116
+ <spine-auto-service-suggestions [clientId]="c.id" />
2117
+ }
2118
+ </div>
2119
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".as-stack{display:flex;flex-direction:column;gap:var(--hm-space-md);min-inline-size:0}.as-actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--hm-space-xs)}.as-field-row{display:flex;flex-wrap:wrap;align-items:flex-end;gap:var(--hm-space-sm)}.as-field-row>hm-combobox{flex:1 1 16rem;min-inline-size:0}.as-summary{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-sm)}.as-summary__info{display:flex;flex:1 1 16rem;flex-direction:column;gap:var(--hm-space-micro);min-inline-size:0}.as-summary__title{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs);margin:0;color:var(--hm-text);font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-semibold)}.as-summary__detail{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.muted{color:var(--hm-muted)}.as-empty{margin:var(--hm-space-xs) 0 0;font-size:var(--hm-font-size-sm)}.as-secret{display:grid;gap:var(--hm-space-micro)}.as-secret__label{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.as-secret__row{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.as-secret__value{flex:1 1 240px;min-width:0;border:var(--hm-border-hairline) solid var(--hm-divider);border-radius:var(--hm-radius-control);padding:var(--hm-space-xs) var(--hm-space-sm);background:var(--hm-surface-muted);color:var(--hm-text);font-family:var(--hm-font-mono);font-size:var(--hm-font-size-sm);overflow-wrap:anywhere;-webkit-user-select:all;user-select:all}:host{display:block;min-inline-size:0}\n"] }]
2120
+ }] });
2121
+
2122
+ export { AutoServiceSuggestionsPageComponent };
2123
+ //# sourceMappingURL=trismegisto-spine-auto-service-auto-service-suggestions-page.component-DIeunpQ9.mjs.map