@trismegisto/spine 3.3.2 → 3.3.3

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 (31) hide show
  1. package/fesm2022/trismegisto-spine-auto-service-auto-service-suggestions-page.component-mMjE3JOL.mjs +2179 -0
  2. package/fesm2022/trismegisto-spine-auto-service-auto-service-suggestions-page.component-mMjE3JOL.mjs.map +1 -0
  3. package/fesm2022/{trismegisto-spine-auto-service-home.component-rQ3dIGIE.mjs → trismegisto-spine-auto-service-home.component-9WvXOH3l.mjs} +7 -4
  4. package/fesm2022/trismegisto-spine-auto-service-home.component-9WvXOH3l.mjs.map +1 -0
  5. package/fesm2022/trismegisto-spine-auto-service.mjs +2 -2
  6. package/fesm2022/trismegisto-spine-graphql.mjs +3 -1
  7. package/fesm2022/trismegisto-spine-graphql.mjs.map +1 -1
  8. package/fesm2022/trismegisto-spine-layout-suggestions-page.component-xI2gDMok.mjs +532 -0
  9. package/fesm2022/trismegisto-spine-layout-suggestions-page.component-xI2gDMok.mjs.map +1 -0
  10. package/fesm2022/trismegisto-spine-layout-trismegisto-spine-layout-Clphrsgc.mjs +8429 -0
  11. package/fesm2022/trismegisto-spine-layout-trismegisto-spine-layout-Clphrsgc.mjs.map +1 -0
  12. package/fesm2022/trismegisto-spine-layout.mjs +1 -7662
  13. package/fesm2022/trismegisto-spine-layout.mjs.map +1 -1
  14. package/fesm2022/trismegisto-spine-notification.mjs +1 -1
  15. package/fesm2022/trismegisto-spine-notification.mjs.map +1 -1
  16. package/fesm2022/trismegisto-spine-routing.mjs +16 -3
  17. package/fesm2022/trismegisto-spine-routing.mjs.map +1 -1
  18. package/fesm2022/trismegisto-spine-shell.mjs +4 -2
  19. package/fesm2022/trismegisto-spine-shell.mjs.map +1 -1
  20. package/fesm2022/trismegisto-spine.mjs +286 -29
  21. package/fesm2022/trismegisto-spine.mjs.map +1 -1
  22. package/package.json +6 -6
  23. package/types/trismegisto-spine-layout.d.ts +879 -17
  24. package/types/trismegisto-spine-layout.d.ts.map +1 -1
  25. package/types/trismegisto-spine-routing.d.ts.map +1 -1
  26. package/types/trismegisto-spine-shell.d.ts.map +1 -1
  27. package/types/trismegisto-spine.d.ts +130 -12
  28. package/types/trismegisto-spine.d.ts.map +1 -1
  29. package/fesm2022/trismegisto-spine-auto-service-auto-service-suggestions-page.component-DIeunpQ9.mjs +0 -2123
  30. package/fesm2022/trismegisto-spine-auto-service-auto-service-suggestions-page.component-DIeunpQ9.mjs.map +0 -1
  31. package/fesm2022/trismegisto-spine-auto-service-home.component-rQ3dIGIE.mjs.map +0 -1
@@ -0,0 +1,532 @@
1
+ import * as i0 from '@angular/core';
2
+ import { inject, viewChild, computed, signal, effect, untracked, ChangeDetectionStrategy, Component } from '@angular/core';
3
+ import { Router } from '@angular/router';
4
+ import { ViewService } from '@orfeu/angular/core';
5
+ import { HermesToastService, AccountAvatarComponent, AlertComponent, ButtonComponent, DrawerComponent, EmptyComponent, FilterBarComponent, IconComponent, MenuButtonComponent, PaginatorComponent, RichTextComponent, SearchFieldComponent, SegmentedControlComponent, TabComponent, TabsComponent, TagComponent } from '@trismegisto/angular';
6
+ import { pluralMessage, formatMessage } from '@trismegisto/primitives';
7
+ import { SPINE_MESSAGES, SpinePreferencesService, SpineUserStorage, spineRoutesOf } from '@trismegisto/spine';
8
+ import { S as SpineSuggestionsService, a as SpineInboxService, b as SpineSuggestionChatComponent, c as SPINE_SUGGESTION_STATUS_COLORS, d as SPINE_SUGGESTION_KIND_COLORS, e as SPINE_SUGGESTION_KIND_ICONS, f as SPINE_SUGGESTION_WAITING_COLORS, N as NEW_SCREEN_VALUE, g as SPINE_SUGGESTION_VISIBLE_SORTS, s as suggestionScreens, h as SPINE_SUGGESTION_REASONS, i as SPINE_SUGGESTION_STATUSES, j as SPINE_SUGGESTION_KINDS, k as SPINE_SUGGESTION_WAITING, l as SPINE_TAB, m as SPINE_SUGGESTION_PAGE_PARAM, n as SPINE_SUGGESTION_TAB_PARAM, o as SPINE_SUGGESTION_TAB_CONVERSATION, p as screenName, q as screenModule, r as SPINE_SUGGESTIONS_PAGE_PATH, t as SpineSuggestionsDialogComponent, u as SpineTagRowComponent } from './trismegisto-spine-layout-trismegisto-spine-layout-Clphrsgc.mjs';
9
+
10
+ const PAGE = 20;
11
+ /** O recorte "Precisam da sua resposta" no seletor (não é um motivo: é a vez de quem pede). */
12
+ const NEEDS_YOU = 'precisam-de-voce';
13
+ const MESSAGES_PAGE = 20;
14
+ const CLOSED = ['concluida', 'recusada'];
15
+ /** Intervalos do "há 3 dias" (segundos por unidade), do maior para o menor. */
16
+ const AGES = [
17
+ ['year', 31_536_000], ['month', 2_592_000], ['week', 604_800], ['day', 86_400], ['hour', 3_600], ['minute', 60],
18
+ ];
19
+ /**
20
+ * A página "Sugestões do app" de quem pede (página do próprio spine, em `/sugestoes` — ver `provideSpineSuggestionsPage`).
21
+ *
22
+ * - **O que aparece** é o que o servidor deixa a pessoa ver: as dela, as agrupadas (um pedido dela virou outro), as em
23
+ * que ela está envolvida (mesmo de outro grupo) e as pedidas por alguém do grupo dela. Um grupo não vê o do outro,
24
+ * a não ser por agrupamento. Cada item diz POR QUE aparece (os filtros rápidos no topo, com as contagens do servidor).
25
+ * - **Filtros**: busca (título, descrição, tela, "SUG-12"), o recorte (`hm-segmented-control`: Todas, os motivos e
26
+ * "Precisam da sua resposta"), e no `hm-filter-bar` status, tipo, tela (as do menu e "Nova tela") e com quem está a
27
+ * vez; mais a ordem. Tudo no servidor, paginado.
28
+ * - Apoiar direto no cartão, sem abrir a gaveta (a própria só mostra a contagem).
29
+ * - **Gaveta** (`hm-drawer`): clicar abre a sugestão em **Detalhes** (descrição, tela, quem pediu, os grupos em selos,
30
+ * os responsáveis), com a aba **Conversa** ao lado (o chat: o que a equipe escreveu de público e o que quem pede
31
+ * respondeu; quem pediu e os envolvidos escrevem, a vez passa para os responsáveis; quem só é do grupo não tem a aba).
32
+ * O apoio fica no topo, fora das abas. Abrir a Conversa marca como lida.
33
+ * - "Sugerir melhoria" abre o mesmo modal da barra (sem tela escolhida); a nova entra na lista e abre no painel.
34
+ * - "?sugestao=SUG-12" abre direto na sugestão (o link do aviso de envio).
35
+ */
36
+ class SpineSuggestionsPageComponent {
37
+ constructor() {
38
+ this.messages = inject(SPINE_MESSAGES);
39
+ this.text = this.messages.suggestions;
40
+ this.page = this.text.page;
41
+ this.service = inject(SpineSuggestionsService);
42
+ this.inbox = inject(SpineInboxService);
43
+ this.toast = inject(HermesToastService);
44
+ this.preferences = inject(SpinePreferencesService);
45
+ this.userStorage = inject(SpineUserStorage, { optional: true });
46
+ this.views = inject(ViewService, { optional: true });
47
+ this.router = inject(Router, { optional: true });
48
+ this.chat = viewChild(SpineSuggestionChatComponent, ...(ngDevMode ? [{ debugName: "chat" }] : /* istanbul ignore next */ []));
49
+ this.statusColors = SPINE_SUGGESTION_STATUS_COLORS;
50
+ this.kindColors = SPINE_SUGGESTION_KIND_COLORS;
51
+ this.kindIcons = SPINE_SUGGESTION_KIND_ICONS;
52
+ /** O "⋯" de cada cartão: copiar o link (abre a página já na sugestão) e a chave. */
53
+ this.itemMenu = [
54
+ { id: 'copy-link', label: this.text.menuCopyLink, icon: 'link' },
55
+ { id: 'copy-key', label: this.text.menuCopyKey, icon: 'copy' },
56
+ ];
57
+ this.waitingColors = SPINE_SUGGESTION_WAITING_COLORS;
58
+ this.newScreenValue = NEW_SCREEN_VALUE;
59
+ this.sortOptions = SPINE_SUGGESTION_VISIBLE_SORTS.map((s) => ({ value: s, label: this.page.sorts[s] }));
60
+ /** As telas do menu (o filtro de tela), da árvore que a pessoa já recebe. */
61
+ this.screens = suggestionScreens(spineRoutesOf(this.views?.currentView));
62
+ this.me = computed(() => this.userStorage?.userId() ?? '', ...(ngDevMode ? [{ debugName: "me" }] : /* istanbul ignore next */ []));
63
+ // --- filtros ---
64
+ this.search = signal('', ...(ngDevMode ? [{ debugName: "search" }] : /* istanbul ignore next */ []));
65
+ this.reason = signal('', ...(ngDevMode ? [{ debugName: "reason" }] : /* istanbul ignore next */ []));
66
+ this.status = signal('', ...(ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []));
67
+ this.kind = signal('', ...(ngDevMode ? [{ debugName: "kind" }] : /* istanbul ignore next */ []));
68
+ /** Uma rota, `NEW_SCREEN_VALUE` ou ''. */
69
+ this.screen = signal('', ...(ngDevMode ? [{ debugName: "screen" }] : /* istanbul ignore next */ []));
70
+ this.waiting = signal('', ...(ngDevMode ? [{ debugName: "waiting" }] : /* istanbul ignore next */ []));
71
+ this.sort = signal('activity', ...(ngDevMode ? [{ debugName: "sort" }] : /* istanbul ignore next */ []));
72
+ this.filtered = computed(() => !!(this.search().trim() || this.reason() || this.status() || this.kind()
73
+ || this.screen() || this.waiting()), ...(ngDevMode ? [{ debugName: "filtered" }] : /* istanbul ignore next */ []));
74
+ /** O recorte no seletor: um motivo, "Precisam da sua resposta" (a vez de quem pede, sem motivo) ou '' (Todas). */
75
+ this.scope = computed(() => this.reason() || (this.waiting() === 'solicitante' ? NEEDS_YOU : ''), ...(ngDevMode ? [{ debugName: "scope" }] : /* istanbul ignore next */ []));
76
+ /** As opções do recorte com a contagem ("Minhas · 2"); motivo sem nenhuma sugestão fica de fora (a não ser o escolhido). */
77
+ this.scopeOptions = computed(() => {
78
+ const counted = (label, count) => (count === null || count === undefined ? label : `${label} · ${count}`);
79
+ const counts = this.counts();
80
+ const options = [{ value: '', label: counted(this.page.allReasons, this.allTotal()) }];
81
+ for (const r of SPINE_SUGGESTION_REASONS) {
82
+ if (counts?.[r] !== 0 || this.reason() === r)
83
+ options.push({ value: r, label: counted(this.page.reasons[r], counts?.[r]) });
84
+ }
85
+ if (counts?.waiting_me || this.scope() === NEEDS_YOU) {
86
+ options.push({ value: NEEDS_YOU, label: counted(this.page.needsYou, counts?.waiting_me), icon: 'chat-bubble' });
87
+ }
88
+ return options;
89
+ }, ...(ngDevMode ? [{ debugName: "scopeOptions" }] : /* istanbul ignore next */ []));
90
+ /** Os campos do `hm-filter-bar` (o servidor filtra): status, tipo, tela (as do menu e "Nova tela") e com quem está a vez. */
91
+ this.filterFields = computed(() => [
92
+ { key: 'status', label: this.text.statusFilter, type: 'select', options: SPINE_SUGGESTION_STATUSES.map((v) => ({ value: v, label: this.text.statuses[v] })) },
93
+ { key: 'kind', label: this.text.kindFilter, type: 'select', options: SPINE_SUGGESTION_KINDS.map((v) => ({ value: v, label: this.text.kinds[v] })) },
94
+ {
95
+ key: 'screen', label: this.text.screenFilter, type: 'select',
96
+ options: [...this.screens.map((sc) => ({ value: sc.path, label: sc.label })), { value: NEW_SCREEN_VALUE, label: this.text.newScreenTag }],
97
+ },
98
+ { key: 'waiting', label: this.text.waitingFilter, type: 'select', options: SPINE_SUGGESTION_WAITING.map((v) => ({ value: v, label: this.text.waiting[v] })) },
99
+ ], ...(ngDevMode ? [{ debugName: "filterFields" }] : /* istanbul ignore next */ []));
100
+ this.filterValue = computed(() => {
101
+ const value = {};
102
+ for (const key of ['status', 'kind', 'screen', 'waiting'])
103
+ if (this[key]())
104
+ value[key] = this[key]();
105
+ return value;
106
+ }, ...(ngDevMode ? [{ debugName: "filterValue" }] : /* istanbul ignore next */ []));
107
+ // --- lista ---
108
+ this.list = signal([], ...(ngDevMode ? [{ debugName: "list" }] : /* istanbul ignore next */ []));
109
+ this.total = signal(0, ...(ngDevMode ? [{ debugName: "total" }] : /* istanbul ignore next */ []));
110
+ this.counts = signal(null, ...(ngDevMode ? [{ debugName: "counts" }] : /* istanbul ignore next */ []));
111
+ this.pagination = signal({ page: 1, size: PAGE }, ...(ngDevMode ? [{ debugName: "pagination" }] : /* istanbul ignore next */ []));
112
+ this.loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
113
+ this.error = signal('', ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
114
+ this.request = 0;
115
+ this.countText = computed(() => pluralMessage(this.page.count, this.total(), this.messages.meta.locale), ...(ngDevMode ? [{ debugName: "countText" }] : /* istanbul ignore next */ []));
116
+ /** "Todas": o total da última consulta sem motivo escolhido (null antes dela). */
117
+ this.allTotal = signal(null, ...(ngDevMode ? [{ debugName: "allTotal" }] : /* istanbul ignore next */ []));
118
+ /** O título do cabeçalho: o recorte escolhido ("Todas as sugestões", "Minhas"...). */
119
+ this.heading = computed(() => {
120
+ const r = this.reason();
121
+ if (this.waiting() === 'solicitante' && !r)
122
+ return this.page.needsYou;
123
+ return r ? this.page.reasons[r] : this.page.allTitle;
124
+ }, ...(ngDevMode ? [{ debugName: "heading" }] : /* istanbul ignore next */ []));
125
+ /** "1 em desenvolvimento · 1 planejada": quantas em cada status, do que está na tela. */
126
+ this.statusParts = computed(() => {
127
+ const locale = this.messages.meta.locale;
128
+ return SPINE_SUGGESTION_STATUSES.map((status) => ({ status, count: this.list().filter((x) => x.status === status).length }))
129
+ .filter((p) => p.count)
130
+ .map((p) => ({ status: p.status, text: formatMessage(this.page.statusPart, { count: p.count, status: this.text.statuses[p.status].toLocaleLowerCase(locale) }) }));
131
+ }, ...(ngDevMode ? [{ debugName: "statusParts" }] : /* istanbul ignore next */ []));
132
+ // --- gaveta ---
133
+ this.selected = signal(null, ...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
134
+ /** A gaveta abre em Detalhes; a conversa é lida (e marcada como lida) quando a pessoa vai até ela. */
135
+ this.tab = signal('details', ...(ngDevMode ? [{ debugName: "tab" }] : /* istanbul ignore next */ []));
136
+ this.drawerHeading = computed(() => {
137
+ const s = this.selected();
138
+ return s ? `${s.key} · ${s.title}` : this.page.details;
139
+ }, ...(ngDevMode ? [{ debugName: "drawerHeading" }] : /* istanbul ignore next */ []));
140
+ this.detailLoading = signal(false, ...(ngDevMode ? [{ debugName: "detailLoading" }] : /* istanbul ignore next */ []));
141
+ this.detailError = signal('', ...(ngDevMode ? [{ debugName: "detailError" }] : /* istanbul ignore next */ []));
142
+ this.conversation = signal(null, ...(ngDevMode ? [{ debugName: "conversation" }] : /* istanbul ignore next */ []));
143
+ this.reply = signal('', ...(ngDevMode ? [{ debugName: "reply" }] : /* istanbul ignore next */ []));
144
+ /** A conversa no formato do chat (do mais antigo ao mais novo). */
145
+ this.chatItems = computed(() => (this.conversation()?.messages ?? []).map((m) => ({
146
+ id: m.id, text: m.comment, created_at: m.created_at, author: m.author, from_requester: !!m.from_requester, status: m.status,
147
+ })), ...(ngDevMode ? [{ debugName: "chatItems" }] : /* istanbul ignore next */ []));
148
+ this.sending = signal(false, ...(ngDevMode ? [{ debugName: "sending" }] : /* istanbul ignore next */ []));
149
+ /** As sugestões com o apoio indo para o servidor (cada uma trava só o próprio botão). */
150
+ this.voting = signal(new Set(), ...(ngDevMode ? [{ debugName: "voting" }] : /* istanbul ignore next */ []));
151
+ this.panelOpen = computed(() => !!this.selected() || this.detailLoading() || !!this.detailError(), ...(ngDevMode ? [{ debugName: "panelOpen" }] : /* istanbul ignore next */ []));
152
+ /** A vez é de quem pede e a pessoa pode responder: o destaque no topo do painel. */
153
+ this.waitingOnMe = computed(() => {
154
+ const s = this.selected();
155
+ return !!s && s.can_comment && s.waiting_on === 'solicitante' && !CLOSED.includes(s.status);
156
+ }, ...(ngDevMode ? [{ debugName: "waitingOnMe" }] : /* istanbul ignore next */ []));
157
+ this.dialog = signal(null, ...(ngDevMode ? [{ debugName: "dialog" }] : /* istanbul ignore next */ []));
158
+ void this.load(1);
159
+ const tab = inject(SPINE_TAB, { optional: true });
160
+ if (!tab) {
161
+ const params = this.router?.parseUrl(this.router.url).queryParams ?? {};
162
+ const key = params[SPINE_SUGGESTION_PAGE_PARAM];
163
+ if (typeof key === 'string' && key)
164
+ void this.openByKey(key, params[SPINE_SUGGESTION_TAB_PARAM] === SPINE_SUGGESTION_TAB_CONVERSATION);
165
+ return;
166
+ }
167
+ // No workspace, a página fica viva na aba: "?sugestao=SUG-12" (um link, a central de notificações) pode chegar com
168
+ // ela já aberta. Abre a gaveta e tira o parâmetro da aba, para o mesmo link funcionar de novo.
169
+ effect(() => {
170
+ const params = tab.params();
171
+ const key = params[SPINE_SUGGESTION_PAGE_PARAM];
172
+ if (!key)
173
+ return;
174
+ const conversation = params[SPINE_SUGGESTION_TAB_PARAM] === SPINE_SUGGESTION_TAB_CONVERSATION;
175
+ untracked(() => {
176
+ void this.openByKey(key, conversation);
177
+ tab.navigate({ queryParams: { [SPINE_SUGGESTION_PAGE_PARAM]: null, [SPINE_SUGGESTION_TAB_PARAM]: null } });
178
+ });
179
+ });
180
+ }
181
+ // --- filtros ---
182
+ onSearch(term) {
183
+ this.search.set(term);
184
+ void this.load(1);
185
+ }
186
+ /**
187
+ * O recorte: um motivo, "Precisam da sua resposta" (a vez de quem pede, sem motivo) ou Todas. Sair de "Precisam da
188
+ * sua resposta" tira o "Aguardando solicitante" que ele tinha posto.
189
+ */
190
+ pickScope(value) {
191
+ if (value === this.scope())
192
+ return;
193
+ const leavingNeedsYou = this.scope() === NEEDS_YOU;
194
+ if (value === NEEDS_YOU) {
195
+ this.reason.set('');
196
+ this.waiting.set('solicitante');
197
+ }
198
+ else {
199
+ this.reason.set(value);
200
+ if (leavingNeedsYou)
201
+ this.waiting.set('');
202
+ }
203
+ void this.load(1);
204
+ }
205
+ /** O `hm-filter-bar` aplicou, limpou um campo ou limpou tudo. */
206
+ onFilters(value) {
207
+ const read = (key) => (typeof value[key] === 'string' ? value[key] : '');
208
+ this.status.set(read('status'));
209
+ this.kind.set(read('kind'));
210
+ this.screen.set(read('screen'));
211
+ this.waiting.set(read('waiting'));
212
+ void this.load(1);
213
+ }
214
+ pickTab(tab) {
215
+ const s = this.selected();
216
+ const conversation = tab === 'conversation' && !!s?.can_comment;
217
+ this.tab.set(conversation ? 'conversation' : 'details');
218
+ if (conversation && this.conversation()?.id !== s.id)
219
+ void this.loadConversation(s.id, true);
220
+ }
221
+ onSort(sort) {
222
+ if (sort === this.sort())
223
+ return;
224
+ this.sort.set(sort);
225
+ void this.load(1);
226
+ }
227
+ onPage(state) {
228
+ this.pagination.set({ page: state.page, size: state.size });
229
+ void this.load(state.page);
230
+ }
231
+ async load(page) {
232
+ const request = ++this.request;
233
+ const size = this.pagination().size;
234
+ this.loading.set(true);
235
+ this.error.set('');
236
+ const screen = this.screen();
237
+ try {
238
+ const res = await this.service.visible({
239
+ limit: size, offset: (page - 1) * size,
240
+ reason: this.reason(),
241
+ status: this.status(),
242
+ kind: this.kind(),
243
+ waiting: this.waiting(),
244
+ screen: screen && screen !== NEW_SCREEN_VALUE ? screen : undefined,
245
+ newScreen: screen === NEW_SCREEN_VALUE,
246
+ q: this.search(),
247
+ sort: this.sort(),
248
+ });
249
+ if (request !== this.request)
250
+ return;
251
+ this.list.set(res.suggestions);
252
+ this.total.set(res.total);
253
+ if (!this.reason())
254
+ this.allTotal.set(res.total);
255
+ if (res.counts)
256
+ this.counts.set(res.counts);
257
+ this.pagination.set({ page, size });
258
+ }
259
+ catch {
260
+ if (request === this.request)
261
+ this.error.set(this.page.error);
262
+ }
263
+ finally {
264
+ if (request === this.request)
265
+ this.loading.set(false);
266
+ }
267
+ }
268
+ // --- painel ---
269
+ open(s) {
270
+ if (this.selected()?.id === s.id)
271
+ return;
272
+ this.selected.set(s);
273
+ // Abre em Detalhes; a conversa vem ao abrir a aba (quem só vê pelo grupo nem tem a aba: o servidor recusa).
274
+ this.tab.set('details');
275
+ this.detailError.set('');
276
+ this.reply.set('');
277
+ this.conversation.set(null);
278
+ void this.refreshSelected(s.id);
279
+ }
280
+ /** Abre pela chave (link): `conversation` = já na aba Conversa. */
281
+ async openByKey(key, conversation = false) {
282
+ this.detailLoading.set(true);
283
+ try {
284
+ const s = await this.service.lookup(key);
285
+ this.detailLoading.set(false);
286
+ this.open(s);
287
+ if (conversation)
288
+ this.pickTab('conversation');
289
+ }
290
+ catch {
291
+ this.detailLoading.set(false);
292
+ this.detailError.set(this.page.notFound);
293
+ }
294
+ }
295
+ close() {
296
+ this.selected.set(null);
297
+ this.conversation.set(null);
298
+ this.detailError.set('');
299
+ }
300
+ /** Relê a sugestão aberta (status, a vez e os apoios mudam do outro lado) e a deixa igual na lista. */
301
+ async refreshSelected(id) {
302
+ try {
303
+ const fresh = await this.service.get(id);
304
+ if (this.selected()?.id !== id)
305
+ return;
306
+ this.selected.set(fresh);
307
+ this.patch(id, fresh);
308
+ }
309
+ catch {
310
+ /* fica o que veio da lista */
311
+ }
312
+ }
313
+ patch(id, change) {
314
+ this.list.update((list) => list.map((x) => (x.id === id ? { ...x, ...change } : x)));
315
+ const current = this.selected();
316
+ if (current?.id === id)
317
+ this.selected.set({ ...current, ...change });
318
+ }
319
+ loadOlder() {
320
+ const c = this.conversation();
321
+ if (c && !c.loading && c.messages.length < c.total)
322
+ void this.loadConversation(c.id, false);
323
+ }
324
+ async loadConversation(id, reset) {
325
+ const current = this.conversation();
326
+ const offset = reset || current?.id !== id ? 0 : current.messages.length;
327
+ this.conversation.set(reset || current?.id !== id
328
+ ? { id, messages: [], total: 0, loading: true, error: false }
329
+ : { ...current, loading: true, error: false });
330
+ try {
331
+ const res = await this.service.comments(id, { limit: MESSAGES_PAGE, offset });
332
+ const older = [...res.comments].reverse();
333
+ this.conversation.update((c) => (c?.id === id
334
+ ? { ...c, messages: reset ? older : [...older, ...c.messages], total: res.total, loading: false } : c));
335
+ if (reset)
336
+ this.markRead(id);
337
+ }
338
+ catch {
339
+ this.conversation.update((c) => (c?.id === id ? { ...c, loading: false, error: true } : c));
340
+ }
341
+ }
342
+ /**
343
+ * Abrir a conversa a marca como lida (o número some da lista na hora; falha não atrapalha a leitura). Vai sempre ao
344
+ * servidor: a central de notificações também conta a mudança de status, que o `unread` (só comentários) não vê.
345
+ */
346
+ markRead(id) {
347
+ const s = this.list().find((x) => x.id === id) ?? this.selected();
348
+ if (s?.unread) {
349
+ this.patch(id, { unread: 0 });
350
+ this.counts.update((c) => (c ? { ...c, unread: Math.max(0, c.unread - 1) } : c));
351
+ }
352
+ this.service.markRead(id).then(() => this.inbox.refresh(), () => undefined);
353
+ }
354
+ async send(text) {
355
+ const s = this.selected();
356
+ const comment = (text ?? this.reply()).trim();
357
+ if (!s || !comment || this.sending())
358
+ return;
359
+ this.sending.set(true);
360
+ try {
361
+ const message = await this.service.reply(s.id, comment);
362
+ this.reply.set('');
363
+ void this.inbox.refresh();
364
+ this.conversation.update((c) => (c?.id === s.id
365
+ ? { ...c, messages: [...c.messages, { ...message, from_requester: true }], total: c.total + 1 } : c));
366
+ const now = new Date().toISOString();
367
+ const wasWaitingOnMe = s.waiting_on === 'solicitante';
368
+ this.patch(s.id, {
369
+ comments_count: s.comments_count + 1,
370
+ last_comment: { comment, created_at: message.created_at ?? now },
371
+ ...(CLOSED.includes(s.status) ? {} : { waiting_on: 'responsaveis', waiting_since: now }),
372
+ last_activity_at: now,
373
+ });
374
+ if (wasWaitingOnMe)
375
+ this.counts.update((c) => (c ? { ...c, waiting_me: Math.max(0, c.waiting_me - 1) } : c));
376
+ this.chat()?.scrollToEnd();
377
+ }
378
+ catch {
379
+ this.toast.error(this.page.replyError);
380
+ }
381
+ finally {
382
+ this.sending.set(false);
383
+ }
384
+ }
385
+ /** Apoiar (quem não pediu): muda na hora e volta se o servidor falhar. */
386
+ isVoting(id) {
387
+ return this.voting().has(id);
388
+ }
389
+ /** Apoia ou tira o apoio (do cartão ou da gaveta): já na tela, e desfaz se o servidor recusar. */
390
+ async toggleSupport(s) {
391
+ if (s.mine || this.isVoting(s.id))
392
+ return;
393
+ const voted = !s.voted;
394
+ this.patch(s.id, { voted, votes: Math.max(0, s.votes + (voted ? 1 : -1)) });
395
+ this.voting.update((ids) => new Set(ids).add(s.id));
396
+ try {
397
+ if (voted)
398
+ await this.service.vote(s.id);
399
+ else
400
+ await this.service.unvote(s.id);
401
+ }
402
+ catch {
403
+ this.patch(s.id, { voted: s.voted, votes: s.votes });
404
+ this.toast.error(this.text.voteError);
405
+ }
406
+ finally {
407
+ this.voting.update((ids) => {
408
+ const next = new Set(ids);
409
+ next.delete(s.id);
410
+ return next;
411
+ });
412
+ }
413
+ }
414
+ suggest() {
415
+ this.dialog.set('new');
416
+ }
417
+ /** A nova entra no topo (relendo a lista) e abre no painel. */
418
+ async onCreated(created) {
419
+ await this.load(1);
420
+ const fresh = this.list().find((s) => s.id === created.id);
421
+ if (fresh)
422
+ this.open(fresh);
423
+ else if (created.key)
424
+ void this.openByKey(created.key);
425
+ }
426
+ // --- texto ---
427
+ reasonCount(r) {
428
+ return this.counts()?.[r] ?? null;
429
+ }
430
+ screenText(s) {
431
+ if (s.screen.new)
432
+ return s.screen.label || this.text.newScreenTag;
433
+ return s.screen.label || this.text.noScreen;
434
+ }
435
+ screenShort(s) {
436
+ return s.screen.new ? (s.screen.label || this.text.newScreenTag) : (screenName(s.screen.label) || this.text.noScreen);
437
+ }
438
+ /** O módulo da tela ("Vendas" de "Vendas › Pedidos"); vazio na nova, sem tela ou quando o módulo é a própria tela. */
439
+ screenModule(s) {
440
+ if (s.screen.new)
441
+ return '';
442
+ const module = screenModule(s.screen.label);
443
+ return module && module !== screenName(s.screen.label) ? module : '';
444
+ }
445
+ /** O trecho do cartão: a última mensagem pública (a conversa anda) ou, sem ela, a descrição. */
446
+ excerptOf(s) {
447
+ return (s.last_comment?.comment || s.description || '').replace(/\s+/g, ' ').trim();
448
+ }
449
+ commentsText(count) {
450
+ return pluralMessage(this.text.comments, count, this.messages.meta.locale);
451
+ }
452
+ menuLabel(s) {
453
+ return formatMessage(this.text.cardMenu, { key: s.key });
454
+ }
455
+ async onMenu(s, id) {
456
+ const value = id === 'copy-key' ? s.key : this.linkOf(s);
457
+ try {
458
+ await navigator.clipboard.writeText(value);
459
+ this.toast.success(id === 'copy-key' ? this.text.keyCopied : this.text.linkCopied);
460
+ }
461
+ catch {
462
+ this.toast.error(this.text.copyError);
463
+ }
464
+ }
465
+ /** O link que abre a página já na sugestão ("…/sugestoes?sugestao=SUG-12"), respeitando o `<base href>` do app. */
466
+ linkOf(s) {
467
+ const path = `${SPINE_SUGGESTIONS_PAGE_PATH.slice(1)}?${SPINE_SUGGESTION_PAGE_PARAM}=${encodeURIComponent(s.key)}`;
468
+ try {
469
+ return new URL(path, document.baseURI).toString();
470
+ }
471
+ catch {
472
+ return `/${path}`;
473
+ }
474
+ }
475
+ personName(p) {
476
+ return p?.name || p?.username || this.text.removedAuthor;
477
+ }
478
+ votesText(count) {
479
+ return pluralMessage(this.text.votes, count, this.messages.meta.locale);
480
+ }
481
+ unreadText(count) {
482
+ return pluralMessage(this.text.unread, count, this.messages.meta.locale);
483
+ }
484
+ supportText(s) {
485
+ if (s.mine)
486
+ return this.votesText(s.votes);
487
+ return formatMessage(s.voted ? this.text.supported : this.text.support, { count: s.votes });
488
+ }
489
+ mergedFromText(key, target) {
490
+ return formatMessage(this.text.mergedFrom, { key, target });
491
+ }
492
+ /** "há 3 dias", "ontem", "há 2 horas" (Intl.RelativeTimeFormat no idioma do catálogo). */
493
+ age(iso) {
494
+ if (!iso)
495
+ return '';
496
+ const seconds = Math.round((Date.now() - new Date(iso).getTime()) / 1000);
497
+ try {
498
+ const rtf = new Intl.RelativeTimeFormat(this.messages.meta.locale, { numeric: 'auto' });
499
+ for (const [unit, size] of AGES)
500
+ if (Math.abs(seconds) >= size)
501
+ return rtf.format(-Math.floor(seconds / size), unit);
502
+ return rtf.format(0, 'minute');
503
+ }
504
+ catch {
505
+ return this.date(iso);
506
+ }
507
+ }
508
+ date(iso) {
509
+ try {
510
+ return new Intl.DateTimeFormat(this.messages.meta.locale, {
511
+ dateStyle: 'short', timeStyle: 'short', timeZone: this.preferences.effectiveTimeZone(),
512
+ }).format(new Date(iso));
513
+ }
514
+ catch {
515
+ return iso.slice(0, 16).replace('T', ' ');
516
+ }
517
+ }
518
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineSuggestionsPageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
519
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: SpineSuggestionsPageComponent, isStandalone: true, selector: "spine-suggestions-page", viewQueries: [{ propertyName: "chat", first: true, predicate: SpineSuggestionChatComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<section class=\"ssp\" [attr.aria-label]=\"page.listLabel\">\n <!-- Cabe\u00E7alho numa linha: o recorte, quantas (e quantas em cada status), a ordem e o \"Sugerir melhoria\". -->\n <header class=\"ssp__head\">\n <h2 class=\"ssp__heading\">{{ heading() }}</h2>\n <span class=\"ssp__count\" aria-live=\"polite\">{{ countText() }}</span>\n @for (part of statusParts(); track part.status) {\n <span class=\"ssp__status-part\" [attr.data-status]=\"part.status\"><span class=\"ssp__dot\" aria-hidden=\"true\"></span>{{ part.text }}</span>\n }\n <span class=\"ssp__spacer\"></span>\n <hm-segmented-control class=\"ssp__sort\" size=\"small\" [label]=\"text.sort\" [options]=\"sortOptions\" [value]=\"sort()\"\n (valueChange)=\"onSort($event)\" />\n <hm-button size=\"small\" icon=\"plus\" (clicked)=\"suggest()\">{{ text.button }}</hm-button>\n </header>\n\n <!-- Busca e o recorte (por que a pessoa v\u00EA, com as contagens do servidor) na mesma linha. -->\n <div class=\"ssp__bar\">\n <hm-search-field class=\"ssp__search\" [label]=\"text.appSearch\" labelHidden [placeholder]=\"text.appSearch\"\n [landmark]=\"false\" [debounce]=\"300\" [value]=\"search()\" (search)=\"onSearch($event)\" />\n <hm-segmented-control class=\"ssp__scope\" size=\"small\" emphasis=\"action\" [label]=\"page.reasonFilter\"\n [options]=\"scopeOptions()\" [value]=\"scope()\" (valueChange)=\"pickScope($event)\" />\n </div>\n\n <!-- Filtros do Trismegisto: um bot\u00E3o por campo e, aplicados, as fichas remov\u00EDveis com \"Limpar tudo\". -->\n <hm-filter-bar class=\"ssp__filters\" [label]=\"page.filtersLabel\" [filters]=\"filterFields()\" [value]=\"filterValue()\"\n (valueChange)=\"onFilters($event)\" />\n\n @if (error()) {\n <hm-alert variant=\"error\">{{ error() }}</hm-alert>\n } @else if (!loading() && list().length === 0) {\n @if (filtered()) {\n <hm-empty icon=\"filter\" [heading]=\"page.emptyFiltered\" />\n } @else {\n <hm-empty icon=\"lightbulb\" [heading]=\"page.empty\" [description]=\"page.emptyHint\" />\n }\n }\n\n <ul class=\"ssp__list\" [attr.aria-busy]=\"loading() ? 'true' : null\">\n @for (s of list(); track s.id) {\n <li class=\"ssp__item\" [class.ssp__item--open]=\"selected()?.id === s.id\" [class.ssp__item--unread]=\"s.unread > 0\">\n <div class=\"ssp__item-meta\">\n <span class=\"ssp__key\">{{ s.key }}</span>\n <span class=\"ssp__status\" [attr.data-status]=\"s.status\"><span class=\"ssp__dot\" aria-hidden=\"true\"></span>{{ text.statuses[s.status] }}</span>\n <span class=\"ssp__kind\" [attr.data-kind]=\"s.kind\"><hm-icon [shape]=\"kindIcons[s.kind]\" />{{ text.kinds[s.kind] }}</span>\n @if (s.unread) { <span class=\"ssp__unread\">{{ unreadText(s.unread) }}</span> }\n <span class=\"ssp__menu\">\n <hm-menu-button size=\"small\" icon=\"ellipsis-vertical\" [showArrow]=\"false\" align=\"end\" [label]=\"menuLabel(s)\"\n [items]=\"itemMenu\" (chosen)=\"onMenu(s, $event)\" />\n </span>\n </div>\n <!-- O t\u00EDtulo \u00E9 o bot\u00E3o que abre a sugest\u00E3o na gaveta. -->\n <button type=\"button\" class=\"ssp__item-title\" aria-haspopup=\"dialog\" (click)=\"open(s)\">{{ s.title }}</button>\n <div class=\"ssp__item-screen\">\n <hm-icon [shape]=\"s.screen.new ? 'plus' : 'application'\" />\n @if (screenModule(s); as module) {\n <span class=\"ssp__screen-module\">{{ module }}</span><hm-icon class=\"ssp__screen-sep\" shape=\"angle-right\" />\n }\n <strong class=\"ssp__screen-name\">{{ screenShort(s) }}</strong>\n @if (s.screen.new) { <span class=\"ssp__new-screen\">{{ text.newScreenTag }}</span> }\n @if (s.waiting_on && s.status !== 'concluida' && s.status !== 'recusada') {\n <hm-tag [name]=\"text.waiting[s.waiting_on]\" [color]=\"waitingColors[s.waiting_on]\" />\n }\n </div>\n @if (excerptOf(s); as ex) {\n <p class=\"ssp__excerpt\">{{ ex }}</p>\n }\n <div class=\"ssp__item-foot\">\n <hm-account-avatar size=\"small\" [name]=\"personName(s.author)\" [imageUrl]=\"s.author?.picture\" />\n <span class=\"ssp__byline\">\n <span class=\"ssp__byline-name\">{{ personName(s.author) }}</span>\n <time [attr.datetime]=\"s.last_activity_at || s.updated_at\">{{ age(s.last_activity_at || s.updated_at) }}</time>\n </span>\n <span class=\"ssp__counts\">\n @if (s.can_comment) {\n <span class=\"ssp__count-item\" [attr.title]=\"commentsText(s.comments_count)\">\n <hm-icon shape=\"comment-message\" /><span aria-hidden=\"true\">{{ s.comments_count }}</span><span class=\"ssp__sr\">{{ commentsText(s.comments_count) }}</span>\n </span>\n }\n <!-- Apoiar direto no cart\u00E3o (a pr\u00F3pria s\u00F3 mostra a contagem: ningu\u00E9m apoia o que pediu). -->\n @if (s.mine) {\n <span class=\"ssp__count-item\" [attr.title]=\"votesText(s.votes)\">\n <hm-icon shape=\"thumbs-up\" /><span aria-hidden=\"true\">{{ s.votes }}</span><span class=\"ssp__sr\">{{ votesText(s.votes) }}</span>\n </span>\n } @else {\n <hm-button class=\"ssp__support\" size=\"small\" emphasis=\"minimal\" icon=\"thumbs-up\" toggle [pressed]=\"s.voted\"\n [disabled]=\"isVoting(s.id)\" [attr.title]=\"supportText(s)\" (clicked)=\"toggleSupport(s)\">\n <span aria-hidden=\"true\">{{ s.votes }}</span><span class=\"ssp__sr\">{{ supportText(s) }}</span>\n </hm-button>\n }\n </span>\n </div>\n </li>\n }\n </ul>\n\n @if (total() > pagination().size) {\n <hm-paginator variant=\"simple\" [label]=\"text.appPagination\" [state]=\"pagination()\" [totalItems]=\"total()\"\n [sizes]=\"[20, 50]\" (stateChanged)=\"onPage($event)\" />\n }\n</section>\n\n<!-- A sugest\u00E3o aberta: gaveta com as abas Conversa (o chat) e Detalhes. -->\n<hm-drawer [heading]=\"drawerHeading()\" size=\"large\" [open]=\"panelOpen()\" (openChange)=\"$event || close()\">\n @if (detailError()) {\n <hm-alert variant=\"error\">{{ detailError() }}</hm-alert>\n } @else if (selected(); as s) {\n <div class=\"ssp-detail\" [class.ssp-detail--alert]=\"waitingOnMe()\">\n <div class=\"ssp__item-meta\">\n <span class=\"ssp__key\">{{ s.key }}</span>\n <span class=\"ssp__status\" [attr.data-status]=\"s.status\"><span class=\"ssp__dot\" aria-hidden=\"true\"></span>{{ text.statuses[s.status] }}</span>\n <span class=\"ssp__kind\" [attr.data-kind]=\"s.kind\"><hm-icon [shape]=\"kindIcons[s.kind]\" />{{ text.kinds[s.kind] }}</span>\n @if (s.archived) { <hm-tag [name]=\"text.archivedTag\" /> }\n @if (s.waiting_on && s.status !== 'concluida' && s.status !== 'recusada') {\n <hm-tag [name]=\"text.waiting[s.waiting_on]\" [color]=\"waitingColors[s.waiting_on]\" />\n }\n <!-- O apoio fica fora das abas: vale para as duas. -->\n <hm-button class=\"ssp-detail__support\" size=\"small\" emphasis=\"medium\" icon=\"thumbs-up\" toggle [pressed]=\"s.voted\"\n [disabled]=\"s.mine || isVoting(s.id)\" (clicked)=\"toggleSupport(s)\">{{ supportText(s) }}</hm-button>\n </div>\n @if (waitingOnMe()) {\n <hm-alert variant=\"warning\">{{ page.waitingYou }}</hm-alert>\n }\n\n <hm-tabs [label]=\"page.details\" [active]=\"tab()\" (activeChange)=\"pickTab($event)\">\n <hm-tab id=\"details\" [label]=\"page.details\">\n <div class=\"ssp-detail__body\">\n @if (!s.can_comment) {\n <hm-alert variant=\"info\">{{ page.readOnly }}</hm-alert>\n }\n @if (s.description_html) {\n <hm-rich-text class=\"ssp-detail__description\" [label]=\"text.description\" labelHidden readonly hideToolbar [value]=\"s.description_html\" />\n } @else if (s.description) {\n <p class=\"ssp-detail__description\">{{ s.description }}</p>\n } @else {\n <p class=\"ssp__hint\">{{ text.noDescription }}</p>\n }\n\n <dl class=\"ssp-detail__facts\">\n <dt>{{ text.screenOrigin }}</dt>\n <dd>\n @if (s.screen.new) { <span class=\"ssp__new-screen\">{{ text.newScreenTag }}</span> }\n {{ screenText(s) }}\n </dd>\n <dt>{{ text.requestedBy }}</dt>\n <dd>\n <hm-account-avatar size=\"small\" [name]=\"personName(s.author)\" [imageUrl]=\"s.author?.picture\" />\n {{ personName(s.author) }} \u00B7 {{ age(s.created_at) }}\n </dd>\n <!-- Os grupos em linha, no modelo dos r\u00F3tulos (o que n\u00E3o cabe vira \"+N\"), como no quadro da equipe. -->\n <dt>{{ page.groups }}</dt>\n <dd class=\"ssp-detail__groups\"><spine-tag-row [label]=\"page.groups\" [items]=\"s.groups\" [empty]=\"text.noGroups\" /></dd>\n <dt>{{ text.assignees }}</dt>\n <dd>\n @for (p of s.assignees; track p.user_id) {\n <hm-account-avatar size=\"small\" [name]=\"personName(p)\" [imageUrl]=\"p.picture\" />\n <span>{{ personName(p) }}</span>\n } @empty { <span class=\"ssp__hint\">{{ text.nobody }}</span> }\n </dd>\n <dt>{{ text.createdAt }}</dt>\n <dd>{{ date(s.created_at) }}</dd>\n </dl>\n\n @for (r of s.reasons; track r) {\n <p class=\"ssp__hint\">{{ page.reasonHints[r] }}</p>\n }\n @for (m of s.merged_from; track m.id) {\n <p class=\"ssp-detail__merged\">{{ mergedFromText(m.key, s.key) }}</p>\n }\n\n </div>\n </hm-tab>\n <!-- A conversa \u00E9 de quem pediu, foi agrupado ou participa (can_comment); quem s\u00F3 v\u00EA pelo grupo fica com os Detalhes. -->\n @if (s.can_comment) {\n <hm-tab id=\"conversation\" [label]=\"page.conversation\" [count]=\"s.comments_count || null\">\n @if (conversation()?.error) {\n <hm-alert variant=\"error\">{{ page.conversationError }}</hm-alert>\n }\n <!-- A conversa em chat: \u00E0 direita o que a pessoa escreveu como quem pede; o resto (inclusive ela mesma como\n equipe) \u00E0 esquerda, com o papel no cabe\u00E7alho. -->\n <spine-suggestion-chat class=\"ssp-detail__chat\" side=\"solicitante\" [me]=\"me()\" [items]=\"chatItems()\"\n [label]=\"page.conversation\" [empty]=\"page.conversationEmpty\"\n [hasMore]=\"(conversation()?.messages?.length ?? 0) < (conversation()?.total ?? 0)\"\n [loading]=\"!!conversation()?.loading\"\n [composerLabel]=\"page.reply\" [placeholder]=\"page.replyPlaceholder\" [sendLabel]=\"page.replySend\"\n [sending]=\"sending()\" [(draft)]=\"reply\" (loadHistory)=\"loadOlder()\" (send)=\"send($event)\" />\n </hm-tab>\n }\n\n </hm-tabs>\n </div>\n } @else {\n <p class=\"ssp__hint\">{{ page.pickOne }}</p>\n }\n</hm-drawer>\n\n<spine-suggestions-dialog [(view)]=\"dialog\" [onPage]=\"true\" (created)=\"onCreated($event)\" />\n", styles: [":host{display:block;min-block-size:100%}.ssp{display:flex;flex-direction:column;gap:var(--hm-space-xs);padding:var(--hm-space-sm) var(--hm-space-md);min-inline-size:0}.ssp__head{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xxxs) var(--hm-space-sm)}.ssp__heading{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-bold)}.ssp__count{padding:0 var(--hm-space-xs);border-radius:var(--hm-radius-pill);background:var(--hm-hover);color:var(--hm-muted);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold);line-height:1.8}.ssp__status-part{display:inline-flex;align-items:center;gap:var(--hm-space-micro);color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.ssp__spacer{flex:1 1 auto}.ssp__bar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs) var(--hm-space-sm)}.ssp__search{flex:1 1 18rem;min-inline-size:0}.ssp__scope{flex:none;max-inline-size:100%}.ssp__dot{inline-size:.5rem;block-size:.5rem;border-radius:50%;background:var(--ssp-status, var(--hm-border))}[data-status=em_analise]{--ssp-status: var(--hm-info)}[data-status=planejada]{--ssp-status: var(--hm-accent)}[data-status=em_desenvolvimento]{--ssp-status: var(--hm-warning)}[data-status=concluida]{--ssp-status: var(--hm-success)}[data-status=recusada]{--ssp-status: var(--hm-muted)}.ssp__list{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,24rem),1fr));gap:var(--hm-space-sm);margin:0;padding:0;list-style:none}.ssp__item{display:flex;flex-direction:column;gap:var(--hm-space-xxxs);padding:var(--hm-space-sm);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.ssp__item--open{border-color:var(--hm-accent)}.ssp__item--unread{box-shadow:inset var(--hm-border-thin) 0 0 var(--hm-accent)}.ssp__item-meta{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xxxs)}.ssp__key{color:var(--hm-muted);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold)}.ssp__menu{margin-inline-start:auto}.ssp__status,.ssp__kind{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}.ssp__status{background:color-mix(in srgb,var(--ssp-status, var(--hm-border)) 14%,transparent)}.ssp__kind{color:var(--hm-action)}.ssp__kind hm-icon{--hm-icon-size: .875rem}.ssp__kind[data-kind=melhoria] hm-icon{color:var(--hm-accent)}.ssp__kind[data-kind=nova_funcionalidade] hm-icon{color:var(--hm-info)}.ssp__kind[data-kind=problema] hm-icon{color:var(--hm-danger)}.ssp__unread{color:var(--hm-accent);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold)}.ssp__item-title{padding:0;border:0;background:none;color:var(--hm-text);font:inherit;font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-bold);text-align:start;overflow-wrap:anywhere;cursor:pointer}.ssp__item-title:hover{text-decoration:underline}.ssp__item-title:focus-visible{outline:var(--hm-border-thin) solid var(--hm-accent);outline-offset:var(--hm-border-thin);border-radius:var(--hm-radius-micro)}.ssp__item-screen{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-micro) var(--hm-space-xxxs);color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.ssp__item-screen>hm-icon{--hm-icon-size: 1rem}.ssp__screen-sep{--hm-icon-size: .75rem}.ssp__screen-name{color:var(--hm-text);font-weight:var(--hm-font-weight-semibold)}.ssp__new-screen{padding:0 var(--hm-space-xxxs);border-radius:var(--hm-radius-pill);background:var(--hm-info);color:var(--hm-on-status);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold);line-height:1.6}.ssp__excerpt{margin:0;overflow:hidden;color:var(--hm-muted);font-size:var(--hm-font-size-sm);white-space:nowrap;text-overflow:ellipsis}.ssp__item-foot{display:flex;align-items:center;gap:var(--hm-space-xs);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)}.ssp__byline{display:flex;flex:1 1 auto;flex-direction:column;min-inline-size:0;color:var(--hm-muted);font-size:var(--hm-font-size-xs);line-height:1.3}.ssp__byline-name{overflow:hidden;color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold);white-space:nowrap;text-overflow:ellipsis}.ssp__counts{display:inline-flex;align-items:center;gap:var(--hm-space-sm)}.ssp__count-item{display:inline-flex;align-items:center;gap:var(--hm-space-micro);color:var(--hm-muted);font-size:var(--hm-font-size-sm);font-variant-numeric:tabular-nums}.ssp__count-item+.ssp__count-item{padding-inline-start:var(--hm-space-sm);border-inline-start:var(--hm-border-hairline) solid var(--hm-border)}.ssp__support{font-variant-numeric:tabular-nums}.ssp-detail__support{margin-inline-start:auto}.ssp__sr{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}.ssp__hint{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.ssp-detail{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-inline-size:0}.ssp-detail{--ssp-chat-offset: 8rem}.ssp-detail--alert{--ssp-chat-offset: 11.5rem}.ssp-detail__chat{--spine-chat-height: calc(100dvh - var(--ssp-chat-offset));margin-block-start:var(--hm-space-xxxs)}.ssp-detail__groups{min-inline-size:0}.ssp-detail__body{display:flex;flex-direction:column;gap:var(--hm-space-sm);padding-block-start:var(--hm-space-sm)}.ssp-detail__description{margin:0;color:var(--hm-text);white-space:pre-line;overflow-wrap:anywhere}.ssp-detail__facts{display:flex;flex-direction:column;margin:0;font-size:var(--hm-font-size-sm)}.ssp-detail__facts dt{color:var(--hm-text);font-weight:var(--hm-font-weight-semibold)}.ssp-detail__facts dt:not(:first-child){margin-block-start:var(--hm-space-sm)}.ssp-detail__facts dd{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xxxs);margin:var(--hm-space-micro) 0 0;color:var(--hm-text)}.ssp-detail__merged{margin:0;padding:var(--hm-space-xxxs) var(--hm-space-xs);border-inline-start:var(--hm-border-thin) solid var(--hm-info);border-radius:var(--hm-radius-micro);background:var(--hm-info-bg);color:var(--hm-text);font-size:var(--hm-font-size-sm)}\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: DrawerComponent, selector: "hm-drawer", inputs: ["open", "heading", "size", "position", "pinned", "closeLabel", "closable", "beforeClose"], outputs: ["openChange", "closed"] }, { kind: "component", type: EmptyComponent, selector: "hm-empty", inputs: ["heading", "description", "icon", "appearance", "size", "headingLevel"] }, { kind: "component", type: FilterBarComponent, selector: "hm-filter-bar", inputs: ["filters", "value", "label", "savedFilters", "storageKey", "storage", "applyDefault"], outputs: ["valueChange", "savedChange", "savedApplied"] }, { kind: "component", type: IconComponent, selector: "hm-icon", inputs: ["shape", "direction", "size", "solid", "badge", "status", "flip", "inverse", "label"] }, { 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: 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: 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: SpineSuggestionChatComponent, selector: "spine-suggestion-chat", inputs: ["items", "side", "me", "label", "empty", "hasMore", "loading", "canWrite", "composerLabel", "placeholder", "sendLabel", "sending", "error", "draft"], outputs: ["draftChange", "loadHistory", "send"] }, { kind: "component", type: SpineSuggestionsDialogComponent, selector: "spine-suggestions-dialog", inputs: ["view", "screenPath", "screenLabel", "onPage"], outputs: ["viewChange", "created"] }, { kind: "component", type: SpineTagRowComponent, selector: "spine-tag-row", inputs: ["items", "label", "empty"] }, { 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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
520
+ }
521
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineSuggestionsPageComponent, decorators: [{
522
+ type: Component,
523
+ args: [{ selector: 'spine-suggestions-page', imports: [
524
+ AccountAvatarComponent, AlertComponent, ButtonComponent, DrawerComponent, EmptyComponent, FilterBarComponent,
525
+ IconComponent, MenuButtonComponent, PaginatorComponent, RichTextComponent, SearchFieldComponent,
526
+ SegmentedControlComponent, SpineSuggestionChatComponent, SpineSuggestionsDialogComponent, SpineTagRowComponent, TabComponent, TabsComponent,
527
+ TagComponent,
528
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<section class=\"ssp\" [attr.aria-label]=\"page.listLabel\">\n <!-- Cabe\u00E7alho numa linha: o recorte, quantas (e quantas em cada status), a ordem e o \"Sugerir melhoria\". -->\n <header class=\"ssp__head\">\n <h2 class=\"ssp__heading\">{{ heading() }}</h2>\n <span class=\"ssp__count\" aria-live=\"polite\">{{ countText() }}</span>\n @for (part of statusParts(); track part.status) {\n <span class=\"ssp__status-part\" [attr.data-status]=\"part.status\"><span class=\"ssp__dot\" aria-hidden=\"true\"></span>{{ part.text }}</span>\n }\n <span class=\"ssp__spacer\"></span>\n <hm-segmented-control class=\"ssp__sort\" size=\"small\" [label]=\"text.sort\" [options]=\"sortOptions\" [value]=\"sort()\"\n (valueChange)=\"onSort($event)\" />\n <hm-button size=\"small\" icon=\"plus\" (clicked)=\"suggest()\">{{ text.button }}</hm-button>\n </header>\n\n <!-- Busca e o recorte (por que a pessoa v\u00EA, com as contagens do servidor) na mesma linha. -->\n <div class=\"ssp__bar\">\n <hm-search-field class=\"ssp__search\" [label]=\"text.appSearch\" labelHidden [placeholder]=\"text.appSearch\"\n [landmark]=\"false\" [debounce]=\"300\" [value]=\"search()\" (search)=\"onSearch($event)\" />\n <hm-segmented-control class=\"ssp__scope\" size=\"small\" emphasis=\"action\" [label]=\"page.reasonFilter\"\n [options]=\"scopeOptions()\" [value]=\"scope()\" (valueChange)=\"pickScope($event)\" />\n </div>\n\n <!-- Filtros do Trismegisto: um bot\u00E3o por campo e, aplicados, as fichas remov\u00EDveis com \"Limpar tudo\". -->\n <hm-filter-bar class=\"ssp__filters\" [label]=\"page.filtersLabel\" [filters]=\"filterFields()\" [value]=\"filterValue()\"\n (valueChange)=\"onFilters($event)\" />\n\n @if (error()) {\n <hm-alert variant=\"error\">{{ error() }}</hm-alert>\n } @else if (!loading() && list().length === 0) {\n @if (filtered()) {\n <hm-empty icon=\"filter\" [heading]=\"page.emptyFiltered\" />\n } @else {\n <hm-empty icon=\"lightbulb\" [heading]=\"page.empty\" [description]=\"page.emptyHint\" />\n }\n }\n\n <ul class=\"ssp__list\" [attr.aria-busy]=\"loading() ? 'true' : null\">\n @for (s of list(); track s.id) {\n <li class=\"ssp__item\" [class.ssp__item--open]=\"selected()?.id === s.id\" [class.ssp__item--unread]=\"s.unread > 0\">\n <div class=\"ssp__item-meta\">\n <span class=\"ssp__key\">{{ s.key }}</span>\n <span class=\"ssp__status\" [attr.data-status]=\"s.status\"><span class=\"ssp__dot\" aria-hidden=\"true\"></span>{{ text.statuses[s.status] }}</span>\n <span class=\"ssp__kind\" [attr.data-kind]=\"s.kind\"><hm-icon [shape]=\"kindIcons[s.kind]\" />{{ text.kinds[s.kind] }}</span>\n @if (s.unread) { <span class=\"ssp__unread\">{{ unreadText(s.unread) }}</span> }\n <span class=\"ssp__menu\">\n <hm-menu-button size=\"small\" icon=\"ellipsis-vertical\" [showArrow]=\"false\" align=\"end\" [label]=\"menuLabel(s)\"\n [items]=\"itemMenu\" (chosen)=\"onMenu(s, $event)\" />\n </span>\n </div>\n <!-- O t\u00EDtulo \u00E9 o bot\u00E3o que abre a sugest\u00E3o na gaveta. -->\n <button type=\"button\" class=\"ssp__item-title\" aria-haspopup=\"dialog\" (click)=\"open(s)\">{{ s.title }}</button>\n <div class=\"ssp__item-screen\">\n <hm-icon [shape]=\"s.screen.new ? 'plus' : 'application'\" />\n @if (screenModule(s); as module) {\n <span class=\"ssp__screen-module\">{{ module }}</span><hm-icon class=\"ssp__screen-sep\" shape=\"angle-right\" />\n }\n <strong class=\"ssp__screen-name\">{{ screenShort(s) }}</strong>\n @if (s.screen.new) { <span class=\"ssp__new-screen\">{{ text.newScreenTag }}</span> }\n @if (s.waiting_on && s.status !== 'concluida' && s.status !== 'recusada') {\n <hm-tag [name]=\"text.waiting[s.waiting_on]\" [color]=\"waitingColors[s.waiting_on]\" />\n }\n </div>\n @if (excerptOf(s); as ex) {\n <p class=\"ssp__excerpt\">{{ ex }}</p>\n }\n <div class=\"ssp__item-foot\">\n <hm-account-avatar size=\"small\" [name]=\"personName(s.author)\" [imageUrl]=\"s.author?.picture\" />\n <span class=\"ssp__byline\">\n <span class=\"ssp__byline-name\">{{ personName(s.author) }}</span>\n <time [attr.datetime]=\"s.last_activity_at || s.updated_at\">{{ age(s.last_activity_at || s.updated_at) }}</time>\n </span>\n <span class=\"ssp__counts\">\n @if (s.can_comment) {\n <span class=\"ssp__count-item\" [attr.title]=\"commentsText(s.comments_count)\">\n <hm-icon shape=\"comment-message\" /><span aria-hidden=\"true\">{{ s.comments_count }}</span><span class=\"ssp__sr\">{{ commentsText(s.comments_count) }}</span>\n </span>\n }\n <!-- Apoiar direto no cart\u00E3o (a pr\u00F3pria s\u00F3 mostra a contagem: ningu\u00E9m apoia o que pediu). -->\n @if (s.mine) {\n <span class=\"ssp__count-item\" [attr.title]=\"votesText(s.votes)\">\n <hm-icon shape=\"thumbs-up\" /><span aria-hidden=\"true\">{{ s.votes }}</span><span class=\"ssp__sr\">{{ votesText(s.votes) }}</span>\n </span>\n } @else {\n <hm-button class=\"ssp__support\" size=\"small\" emphasis=\"minimal\" icon=\"thumbs-up\" toggle [pressed]=\"s.voted\"\n [disabled]=\"isVoting(s.id)\" [attr.title]=\"supportText(s)\" (clicked)=\"toggleSupport(s)\">\n <span aria-hidden=\"true\">{{ s.votes }}</span><span class=\"ssp__sr\">{{ supportText(s) }}</span>\n </hm-button>\n }\n </span>\n </div>\n </li>\n }\n </ul>\n\n @if (total() > pagination().size) {\n <hm-paginator variant=\"simple\" [label]=\"text.appPagination\" [state]=\"pagination()\" [totalItems]=\"total()\"\n [sizes]=\"[20, 50]\" (stateChanged)=\"onPage($event)\" />\n }\n</section>\n\n<!-- A sugest\u00E3o aberta: gaveta com as abas Conversa (o chat) e Detalhes. -->\n<hm-drawer [heading]=\"drawerHeading()\" size=\"large\" [open]=\"panelOpen()\" (openChange)=\"$event || close()\">\n @if (detailError()) {\n <hm-alert variant=\"error\">{{ detailError() }}</hm-alert>\n } @else if (selected(); as s) {\n <div class=\"ssp-detail\" [class.ssp-detail--alert]=\"waitingOnMe()\">\n <div class=\"ssp__item-meta\">\n <span class=\"ssp__key\">{{ s.key }}</span>\n <span class=\"ssp__status\" [attr.data-status]=\"s.status\"><span class=\"ssp__dot\" aria-hidden=\"true\"></span>{{ text.statuses[s.status] }}</span>\n <span class=\"ssp__kind\" [attr.data-kind]=\"s.kind\"><hm-icon [shape]=\"kindIcons[s.kind]\" />{{ text.kinds[s.kind] }}</span>\n @if (s.archived) { <hm-tag [name]=\"text.archivedTag\" /> }\n @if (s.waiting_on && s.status !== 'concluida' && s.status !== 'recusada') {\n <hm-tag [name]=\"text.waiting[s.waiting_on]\" [color]=\"waitingColors[s.waiting_on]\" />\n }\n <!-- O apoio fica fora das abas: vale para as duas. -->\n <hm-button class=\"ssp-detail__support\" size=\"small\" emphasis=\"medium\" icon=\"thumbs-up\" toggle [pressed]=\"s.voted\"\n [disabled]=\"s.mine || isVoting(s.id)\" (clicked)=\"toggleSupport(s)\">{{ supportText(s) }}</hm-button>\n </div>\n @if (waitingOnMe()) {\n <hm-alert variant=\"warning\">{{ page.waitingYou }}</hm-alert>\n }\n\n <hm-tabs [label]=\"page.details\" [active]=\"tab()\" (activeChange)=\"pickTab($event)\">\n <hm-tab id=\"details\" [label]=\"page.details\">\n <div class=\"ssp-detail__body\">\n @if (!s.can_comment) {\n <hm-alert variant=\"info\">{{ page.readOnly }}</hm-alert>\n }\n @if (s.description_html) {\n <hm-rich-text class=\"ssp-detail__description\" [label]=\"text.description\" labelHidden readonly hideToolbar [value]=\"s.description_html\" />\n } @else if (s.description) {\n <p class=\"ssp-detail__description\">{{ s.description }}</p>\n } @else {\n <p class=\"ssp__hint\">{{ text.noDescription }}</p>\n }\n\n <dl class=\"ssp-detail__facts\">\n <dt>{{ text.screenOrigin }}</dt>\n <dd>\n @if (s.screen.new) { <span class=\"ssp__new-screen\">{{ text.newScreenTag }}</span> }\n {{ screenText(s) }}\n </dd>\n <dt>{{ text.requestedBy }}</dt>\n <dd>\n <hm-account-avatar size=\"small\" [name]=\"personName(s.author)\" [imageUrl]=\"s.author?.picture\" />\n {{ personName(s.author) }} \u00B7 {{ age(s.created_at) }}\n </dd>\n <!-- Os grupos em linha, no modelo dos r\u00F3tulos (o que n\u00E3o cabe vira \"+N\"), como no quadro da equipe. -->\n <dt>{{ page.groups }}</dt>\n <dd class=\"ssp-detail__groups\"><spine-tag-row [label]=\"page.groups\" [items]=\"s.groups\" [empty]=\"text.noGroups\" /></dd>\n <dt>{{ text.assignees }}</dt>\n <dd>\n @for (p of s.assignees; track p.user_id) {\n <hm-account-avatar size=\"small\" [name]=\"personName(p)\" [imageUrl]=\"p.picture\" />\n <span>{{ personName(p) }}</span>\n } @empty { <span class=\"ssp__hint\">{{ text.nobody }}</span> }\n </dd>\n <dt>{{ text.createdAt }}</dt>\n <dd>{{ date(s.created_at) }}</dd>\n </dl>\n\n @for (r of s.reasons; track r) {\n <p class=\"ssp__hint\">{{ page.reasonHints[r] }}</p>\n }\n @for (m of s.merged_from; track m.id) {\n <p class=\"ssp-detail__merged\">{{ mergedFromText(m.key, s.key) }}</p>\n }\n\n </div>\n </hm-tab>\n <!-- A conversa \u00E9 de quem pediu, foi agrupado ou participa (can_comment); quem s\u00F3 v\u00EA pelo grupo fica com os Detalhes. -->\n @if (s.can_comment) {\n <hm-tab id=\"conversation\" [label]=\"page.conversation\" [count]=\"s.comments_count || null\">\n @if (conversation()?.error) {\n <hm-alert variant=\"error\">{{ page.conversationError }}</hm-alert>\n }\n <!-- A conversa em chat: \u00E0 direita o que a pessoa escreveu como quem pede; o resto (inclusive ela mesma como\n equipe) \u00E0 esquerda, com o papel no cabe\u00E7alho. -->\n <spine-suggestion-chat class=\"ssp-detail__chat\" side=\"solicitante\" [me]=\"me()\" [items]=\"chatItems()\"\n [label]=\"page.conversation\" [empty]=\"page.conversationEmpty\"\n [hasMore]=\"(conversation()?.messages?.length ?? 0) < (conversation()?.total ?? 0)\"\n [loading]=\"!!conversation()?.loading\"\n [composerLabel]=\"page.reply\" [placeholder]=\"page.replyPlaceholder\" [sendLabel]=\"page.replySend\"\n [sending]=\"sending()\" [(draft)]=\"reply\" (loadHistory)=\"loadOlder()\" (send)=\"send($event)\" />\n </hm-tab>\n }\n\n </hm-tabs>\n </div>\n } @else {\n <p class=\"ssp__hint\">{{ page.pickOne }}</p>\n }\n</hm-drawer>\n\n<spine-suggestions-dialog [(view)]=\"dialog\" [onPage]=\"true\" (created)=\"onCreated($event)\" />\n", styles: [":host{display:block;min-block-size:100%}.ssp{display:flex;flex-direction:column;gap:var(--hm-space-xs);padding:var(--hm-space-sm) var(--hm-space-md);min-inline-size:0}.ssp__head{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xxxs) var(--hm-space-sm)}.ssp__heading{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-bold)}.ssp__count{padding:0 var(--hm-space-xs);border-radius:var(--hm-radius-pill);background:var(--hm-hover);color:var(--hm-muted);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold);line-height:1.8}.ssp__status-part{display:inline-flex;align-items:center;gap:var(--hm-space-micro);color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.ssp__spacer{flex:1 1 auto}.ssp__bar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs) var(--hm-space-sm)}.ssp__search{flex:1 1 18rem;min-inline-size:0}.ssp__scope{flex:none;max-inline-size:100%}.ssp__dot{inline-size:.5rem;block-size:.5rem;border-radius:50%;background:var(--ssp-status, var(--hm-border))}[data-status=em_analise]{--ssp-status: var(--hm-info)}[data-status=planejada]{--ssp-status: var(--hm-accent)}[data-status=em_desenvolvimento]{--ssp-status: var(--hm-warning)}[data-status=concluida]{--ssp-status: var(--hm-success)}[data-status=recusada]{--ssp-status: var(--hm-muted)}.ssp__list{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,24rem),1fr));gap:var(--hm-space-sm);margin:0;padding:0;list-style:none}.ssp__item{display:flex;flex-direction:column;gap:var(--hm-space-xxxs);padding:var(--hm-space-sm);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.ssp__item--open{border-color:var(--hm-accent)}.ssp__item--unread{box-shadow:inset var(--hm-border-thin) 0 0 var(--hm-accent)}.ssp__item-meta{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xxxs)}.ssp__key{color:var(--hm-muted);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold)}.ssp__menu{margin-inline-start:auto}.ssp__status,.ssp__kind{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}.ssp__status{background:color-mix(in srgb,var(--ssp-status, var(--hm-border)) 14%,transparent)}.ssp__kind{color:var(--hm-action)}.ssp__kind hm-icon{--hm-icon-size: .875rem}.ssp__kind[data-kind=melhoria] hm-icon{color:var(--hm-accent)}.ssp__kind[data-kind=nova_funcionalidade] hm-icon{color:var(--hm-info)}.ssp__kind[data-kind=problema] hm-icon{color:var(--hm-danger)}.ssp__unread{color:var(--hm-accent);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold)}.ssp__item-title{padding:0;border:0;background:none;color:var(--hm-text);font:inherit;font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-bold);text-align:start;overflow-wrap:anywhere;cursor:pointer}.ssp__item-title:hover{text-decoration:underline}.ssp__item-title:focus-visible{outline:var(--hm-border-thin) solid var(--hm-accent);outline-offset:var(--hm-border-thin);border-radius:var(--hm-radius-micro)}.ssp__item-screen{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-micro) var(--hm-space-xxxs);color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.ssp__item-screen>hm-icon{--hm-icon-size: 1rem}.ssp__screen-sep{--hm-icon-size: .75rem}.ssp__screen-name{color:var(--hm-text);font-weight:var(--hm-font-weight-semibold)}.ssp__new-screen{padding:0 var(--hm-space-xxxs);border-radius:var(--hm-radius-pill);background:var(--hm-info);color:var(--hm-on-status);font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold);line-height:1.6}.ssp__excerpt{margin:0;overflow:hidden;color:var(--hm-muted);font-size:var(--hm-font-size-sm);white-space:nowrap;text-overflow:ellipsis}.ssp__item-foot{display:flex;align-items:center;gap:var(--hm-space-xs);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)}.ssp__byline{display:flex;flex:1 1 auto;flex-direction:column;min-inline-size:0;color:var(--hm-muted);font-size:var(--hm-font-size-xs);line-height:1.3}.ssp__byline-name{overflow:hidden;color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold);white-space:nowrap;text-overflow:ellipsis}.ssp__counts{display:inline-flex;align-items:center;gap:var(--hm-space-sm)}.ssp__count-item{display:inline-flex;align-items:center;gap:var(--hm-space-micro);color:var(--hm-muted);font-size:var(--hm-font-size-sm);font-variant-numeric:tabular-nums}.ssp__count-item+.ssp__count-item{padding-inline-start:var(--hm-space-sm);border-inline-start:var(--hm-border-hairline) solid var(--hm-border)}.ssp__support{font-variant-numeric:tabular-nums}.ssp-detail__support{margin-inline-start:auto}.ssp__sr{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}.ssp__hint{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.ssp-detail{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-inline-size:0}.ssp-detail{--ssp-chat-offset: 8rem}.ssp-detail--alert{--ssp-chat-offset: 11.5rem}.ssp-detail__chat{--spine-chat-height: calc(100dvh - var(--ssp-chat-offset));margin-block-start:var(--hm-space-xxxs)}.ssp-detail__groups{min-inline-size:0}.ssp-detail__body{display:flex;flex-direction:column;gap:var(--hm-space-sm);padding-block-start:var(--hm-space-sm)}.ssp-detail__description{margin:0;color:var(--hm-text);white-space:pre-line;overflow-wrap:anywhere}.ssp-detail__facts{display:flex;flex-direction:column;margin:0;font-size:var(--hm-font-size-sm)}.ssp-detail__facts dt{color:var(--hm-text);font-weight:var(--hm-font-weight-semibold)}.ssp-detail__facts dt:not(:first-child){margin-block-start:var(--hm-space-sm)}.ssp-detail__facts dd{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xxxs);margin:var(--hm-space-micro) 0 0;color:var(--hm-text)}.ssp-detail__merged{margin:0;padding:var(--hm-space-xxxs) var(--hm-space-xs);border-inline-start:var(--hm-border-thin) solid var(--hm-info);border-radius:var(--hm-radius-micro);background:var(--hm-info-bg);color:var(--hm-text);font-size:var(--hm-font-size-sm)}\n"] }]
529
+ }], ctorParameters: () => [], propDecorators: { chat: [{ type: i0.ViewChild, args: [i0.forwardRef(() => SpineSuggestionChatComponent), { isSignal: true }] }] } });
530
+
531
+ export { SpineSuggestionsPageComponent };
532
+ //# sourceMappingURL=trismegisto-spine-layout-suggestions-page.component-xI2gDMok.mjs.map