@trismegisto/spine 3.3.2 → 3.3.4

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 (60) hide show
  1. package/README.md +15 -2
  2. package/fesm2022/trismegisto-spine-audit.mjs +6 -6
  3. package/fesm2022/trismegisto-spine-audit.mjs.map +1 -1
  4. package/fesm2022/{trismegisto-spine-auto-service-auto-service-client.component-BAfeLyTB.mjs → trismegisto-spine-auto-service-auto-service-client.component-kAXaCM5W.mjs} +8 -8
  5. package/fesm2022/{trismegisto-spine-auto-service-auto-service-client.component-BAfeLyTB.mjs.map → trismegisto-spine-auto-service-auto-service-client.component-kAXaCM5W.mjs.map} +1 -1
  6. package/fesm2022/{trismegisto-spine-auto-service-auto-service-groups.component-BYjIQ9ID.mjs → trismegisto-spine-auto-service-auto-service-groups.component-DE2QIGRH.mjs} +251 -85
  7. package/fesm2022/trismegisto-spine-auto-service-auto-service-groups.component-DE2QIGRH.mjs.map +1 -0
  8. package/fesm2022/{trismegisto-spine-auto-service-auto-service-news-page.component-CBemH4hr.mjs → trismegisto-spine-auto-service-auto-service-news-page.component-BW7BJ0oD.mjs} +2 -2
  9. package/fesm2022/{trismegisto-spine-auto-service-auto-service-news-page.component-CBemH4hr.mjs.map → trismegisto-spine-auto-service-auto-service-news-page.component-BW7BJ0oD.mjs.map} +1 -1
  10. package/fesm2022/{trismegisto-spine-auto-service-auto-service-ratings-page.component-C_THGTkw.mjs → trismegisto-spine-auto-service-auto-service-ratings-page.component-CHPU-QLJ.mjs} +2 -2
  11. package/fesm2022/{trismegisto-spine-auto-service-auto-service-ratings-page.component-C_THGTkw.mjs.map → trismegisto-spine-auto-service-auto-service-ratings-page.component-CHPU-QLJ.mjs.map} +1 -1
  12. package/fesm2022/trismegisto-spine-auto-service-auto-service-suggestions-page.component-CkiCTmXD.mjs +3733 -0
  13. package/fesm2022/trismegisto-spine-auto-service-auto-service-suggestions-page.component-CkiCTmXD.mjs.map +1 -0
  14. package/fesm2022/{trismegisto-spine-auto-service-auto-service.component-QsIQ61w2.mjs → trismegisto-spine-auto-service-auto-service.component-D58Ht9PH.mjs} +7 -7
  15. package/fesm2022/{trismegisto-spine-auto-service-auto-service.component-QsIQ61w2.mjs.map → trismegisto-spine-auto-service-auto-service.component-D58Ht9PH.mjs.map} +1 -1
  16. package/fesm2022/{trismegisto-spine-auto-service-groups.technical-_jQrg1Qh.mjs → trismegisto-spine-auto-service-groups.technical-CB1D3ANC.mjs} +12 -3
  17. package/fesm2022/trismegisto-spine-auto-service-groups.technical-CB1D3ANC.mjs.map +1 -0
  18. package/fesm2022/{trismegisto-spine-auto-service-groups.usage-OvWjNeeT.mjs → trismegisto-spine-auto-service-groups.usage-De2MhlE7.mjs} +7 -2
  19. package/fesm2022/{trismegisto-spine-auto-service-groups.usage-OvWjNeeT.mjs.map → trismegisto-spine-auto-service-groups.usage-De2MhlE7.mjs.map} +1 -1
  20. package/fesm2022/{trismegisto-spine-auto-service-home.component-rQ3dIGIE.mjs → trismegisto-spine-auto-service-home.component-9WvXOH3l.mjs} +7 -4
  21. package/fesm2022/trismegisto-spine-auto-service-home.component-9WvXOH3l.mjs.map +1 -0
  22. package/fesm2022/{trismegisto-spine-auto-service-suggestions.technical-BtUK6sKD.mjs → trismegisto-spine-auto-service-suggestions.technical-CGBBzAR8.mjs} +14 -1
  23. package/fesm2022/{trismegisto-spine-auto-service-suggestions.technical-BtUK6sKD.mjs.map → trismegisto-spine-auto-service-suggestions.technical-CGBBzAR8.mjs.map} +1 -1
  24. package/fesm2022/{trismegisto-spine-auto-service-suggestions.usage-BwO9Hjsx.mjs → trismegisto-spine-auto-service-suggestions.usage-DnAJS-Z0.mjs} +26 -3
  25. package/fesm2022/trismegisto-spine-auto-service-suggestions.usage-DnAJS-Z0.mjs.map +1 -0
  26. package/fesm2022/trismegisto-spine-auto-service.mjs +25 -12
  27. package/fesm2022/trismegisto-spine-auto-service.mjs.map +1 -1
  28. package/fesm2022/trismegisto-spine-graphql.mjs +3 -1
  29. package/fesm2022/trismegisto-spine-graphql.mjs.map +1 -1
  30. package/fesm2022/trismegisto-spine-layout-suggestions-page.component-CEKMqera.mjs +532 -0
  31. package/fesm2022/trismegisto-spine-layout-suggestions-page.component-CEKMqera.mjs.map +1 -0
  32. package/fesm2022/trismegisto-spine-layout-trismegisto-spine-layout-DiAfxIGP.mjs +8764 -0
  33. package/fesm2022/trismegisto-spine-layout-trismegisto-spine-layout-DiAfxIGP.mjs.map +1 -0
  34. package/fesm2022/trismegisto-spine-layout.mjs +1 -7662
  35. package/fesm2022/trismegisto-spine-layout.mjs.map +1 -1
  36. package/fesm2022/trismegisto-spine-notification.mjs +1 -1
  37. package/fesm2022/trismegisto-spine-notification.mjs.map +1 -1
  38. package/fesm2022/trismegisto-spine-routing.mjs +16 -3
  39. package/fesm2022/trismegisto-spine-routing.mjs.map +1 -1
  40. package/fesm2022/trismegisto-spine-shell.mjs +4 -2
  41. package/fesm2022/trismegisto-spine-shell.mjs.map +1 -1
  42. package/fesm2022/trismegisto-spine-side-panel.mjs +1 -1
  43. package/fesm2022/trismegisto-spine-side-panel.mjs.map +1 -1
  44. package/fesm2022/trismegisto-spine.mjs +813 -38
  45. package/fesm2022/trismegisto-spine.mjs.map +1 -1
  46. package/package.json +6 -6
  47. package/types/trismegisto-spine-auto-service.d.ts +20 -4
  48. package/types/trismegisto-spine-auto-service.d.ts.map +1 -1
  49. package/types/trismegisto-spine-layout.d.ts +1200 -74
  50. package/types/trismegisto-spine-layout.d.ts.map +1 -1
  51. package/types/trismegisto-spine-routing.d.ts.map +1 -1
  52. package/types/trismegisto-spine-shell.d.ts.map +1 -1
  53. package/types/trismegisto-spine.d.ts +350 -14
  54. package/types/trismegisto-spine.d.ts.map +1 -1
  55. package/fesm2022/trismegisto-spine-auto-service-auto-service-groups.component-BYjIQ9ID.mjs.map +0 -1
  56. package/fesm2022/trismegisto-spine-auto-service-auto-service-suggestions-page.component-DIeunpQ9.mjs +0 -2123
  57. package/fesm2022/trismegisto-spine-auto-service-auto-service-suggestions-page.component-DIeunpQ9.mjs.map +0 -1
  58. package/fesm2022/trismegisto-spine-auto-service-groups.technical-_jQrg1Qh.mjs.map +0 -1
  59. package/fesm2022/trismegisto-spine-auto-service-home.component-rQ3dIGIE.mjs.map +0 -1
  60. package/fesm2022/trismegisto-spine-auto-service-suggestions.usage-BwO9Hjsx.mjs.map +0 -1
@@ -18,7 +18,7 @@ cabeçalho \`x-access-token\`. Peças: dois \`hm-combobox\` (nível de acesso e
18
18
  | Criar grupo | \`POST /auto-service/:clientId/groups\` (\`{ name, type_id, description? }\`) |
19
19
  | Editar grupo | \`PUT /auto-service/:clientId/groups/:groupId\` (\`{ name, type_id, description, enabled }\`) |
20
20
  | Excluir grupo | \`DELETE /auto-service/:clientId/groups/:groupId\` |
21
- | Membros do grupo | \`GET /auto-service/:clientId/groups/:groupId/members\` |
21
+ | Membros do grupo | \`GET /auto-service/:clientId/groups/:groupId/members?page=&limit=\` (\`{ members, total, page, limit }\`: uma página pelo login, \`limit\` padrão 50 e teto 200; a tela pede páginas de 200 até o fim) |
22
22
  | Busca de usuários | \`GET /auto-service/:clientId/users?search=&limit=\` |
23
23
  | Sugestões de membro | \`GET /auto-service/:clientId/groups/:groupId/member-suggestions?limit=8\` (quem está em outros grupos do projeto e não neste, com foto e os grupos) |
24
24
  | Adicionar membro | \`POST /auto-service/:clientId/groups/:groupId/members\` (\`{ user_id, role }\`) |
@@ -60,7 +60,16 @@ cabeçalho \`x-access-token\`. Peças: dois \`hm-combobox\` (nível de acesso e
60
60
  \`deleteError\`). O \`AutoServiceDeleteFeedback\` usa a mesma função em todas as exclusões do auto-serviço.
61
61
  - Sugestões: até 8, da rota própria (ordem do servidor: mais grupos do projeto primeiro), em chips; sem "ver mais".
62
62
  Falha na rota deixa sem sugestões. Busca: a partir de 2 caracteres, depois de 300 ms sem digitar, \`limit=10\`.
63
- Membros atuais são retirados dos dois.
63
+ Membros atuais são retirados dos dois (as sugestões já vêm sem eles do servidor; a busca, pelos membros já
64
+ carregados).
65
+ - **Carga progressiva** (desde a 3.3.4): a tabela de membros e o modal pedem o grupo inteiro em páginas de 200, uma
66
+ depois da outra (\`MembersLoader\`, \`groups/members-loader.ts\`), e funcionam desde a primeira; enquanto carrega
67
+ aparece \`membersLoadingProgress\` ("Carregando membros… 400 de 1.250") com \`membersPartialResults\`. Sem paginador:
68
+ a grade usa rolagem virtual. A busca é local e instantânea (nome, login, e-mail). Falha numa página mostra
69
+ \`membersLoadIncomplete\` com \`retryLoad\`, que continua da página que falhou. Fechar o modal, trocar de grupo ou
70
+ destruir a tela cancela a página pendente. \`countChange\` emite o total do servidor. No modal, um **Novo** que
71
+ aparece numa página que chega depois sai das inclusões (já era membro); o número ao lado de "Membros do grupo" é o
72
+ total do grupo mais os novos.
64
73
  - Funções oferecidas pela tela: Administrador e Editor; vazio grava \`role: null\` (membro sem função, que só vê os outros\n membros nas configurações do app). Outro valor o servidor recusa (\`400 invalid_role\`).
65
74
  - **Salvar alterações** grava só a diferença, em sequência: adições, depois remoções, depois trocas de função (dos
66
75
  membros que ficaram). Ao terminar, emite \`saved\` e a tabela de membros recarrega. Se uma chamada falhar, o modal mostra o
@@ -68,4 +77,4 @@ cabeçalho \`x-access-token\`. Peças: dois \`hm-combobox\` (nível de acesso e
68
77
  `;
69
78
 
70
79
  export { groups_technical as default };
71
- //# sourceMappingURL=trismegisto-spine-auto-service-groups.technical-_jQrg1Qh.mjs.map
80
+ //# sourceMappingURL=trismegisto-spine-auto-service-groups.technical-CB1D3ANC.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trismegisto-spine-auto-service-groups.technical-CB1D3ANC.mjs","sources":["../../../projects/ui/auto-service/src/docs/groups.technical.ts"],"sourcesContent":["// Especificação técnica da tela Grupos do auto-serviço (`RouteDocument.loadTechnical`), em Markdown. Módulo próprio,\n// carregado só pelo `import()` de `AUTO_SERVICE_DOCUMENTS`: vira um chunk à parte no FESM e fica fora do\n// bundle inicial. Não importe estaticamente nem reexporte no `public-api.ts`.\nexport default `# Grupos — especificação técnica\n\nTela \\`AutoServiceGroupsComponent\\`, ligada ao router de template \\`AUTO_SERVICE_GROUPS_TEMPLATE_ROUTER_ID\\`\n(\\`ad100001-…\\`). Fala com o argos-admin-server em \\`{adminApi}/auto-service\\` (\\`AutoServiceService\\`), com o token no\ncabeçalho \\`x-access-token\\`. Peças: dois \\`hm-combobox\\` (nível de acesso e grupo) + Consultar, \\`AutoServiceGroupModalComponent\\` (criar/editar),\n\\`AutoServiceGroupMembersComponent\\` (membros do grupo aberto) e \\`AutoServiceGroupMembersModalComponent\\`.\n\n## Endpoints\n\n| Ação | Endpoint |\n| --- | --- |\n| Client administrado (só o do token) | \\`GET /auto-service\\` |\n| Grupos do projeto do client | \\`GET /auto-service/:clientId/groups\\` (\\`{ id, name, description, enabled, type }\\`, sem o grupo de administração) |\n| Tipos de grupo do realm | \\`GET /auto-service/:clientId/group-types\\` (\\`{ id, name, level, assignable }\\`) |\n| Criar grupo | \\`POST /auto-service/:clientId/groups\\` (\\`{ name, type_id, description? }\\`) |\n| Editar grupo | \\`PUT /auto-service/:clientId/groups/:groupId\\` (\\`{ name, type_id, description, enabled }\\`) |\n| Excluir grupo | \\`DELETE /auto-service/:clientId/groups/:groupId\\` |\n| Membros do grupo | \\`GET /auto-service/:clientId/groups/:groupId/members?page=&limit=\\` (\\`{ members, total, page, limit }\\`: uma página pelo login, \\`limit\\` padrão 50 e teto 200; a tela pede páginas de 200 até o fim) |\n| Busca de usuários | \\`GET /auto-service/:clientId/users?search=&limit=\\` |\n| Sugestões de membro | \\`GET /auto-service/:clientId/groups/:groupId/member-suggestions?limit=8\\` (quem está em outros grupos do projeto e não neste, com foto e os grupos) |\n| Adicionar membro | \\`POST /auto-service/:clientId/groups/:groupId/members\\` (\\`{ user_id, role }\\`) |\n| Trocar a função | \\`PUT /auto-service/:clientId/groups/:groupId/members/:userId\\` (\\`{ role }\\`) |\n| Remover membro | \\`DELETE /auto-service/:clientId/groups/:groupId/members/:userId\\` |\n\n## Regras\n\n- **Autorização** (\\`clientAdminGuard\\` no servidor): o \\`sub\\` é membro do grupo de administração (\\`is_admin\\`,\n habilitado) do projeto do client **e** está com o crachá Administrador ativo. Todo token só administra o client em\n que foi emitido: \\`GET /auto-service\\` devolve só ele. O seletor de client\n (\\`AutoServiceClientPickerComponent\\`) mostra \\`loading\\` enquanto busca; com um client, escolhe sozinho e não desenha\n mais nada; com nenhum, mostra\n \\`autoService.noClients\\`; a combobox só aparece se a API devolver mais de um.\n- Grupo de outro projeto responde \\`404\\` (\\`assertGroupBelongsToClient\\`).\n- Seletores: grupos e tipos carregados juntos ao escolher o client. **Nível de acesso** (\\`levelOptions\\`): os tipos\n de \\`group-types\\` com \\`assignable !== false\\` ou com algum grupo na lista (o Administrador fica de fora), mais os tipos\n que só aparecem nos grupos (sem a rota de tipos) e \"Sem nível\" (\\`NO_LEVEL\\`) se algum grupo não tiver tipo; do\n \\`level\\` maior para o menor. **Grupo** (\\`groupOptions\\`): só os do nível escolhido, em ordem alfabética; trocar o\n nível limpa o grupo. **Consultar** abre o grupo (resumo com Editar/Excluir + membros); abrir um grupo aponta os dois\n seletores para ele. Depois de criar/editar/excluir a lista recarrega; o grupo aberto é trocado pela versão nova (ou\n fecha, se sumiu); o grupo recém-criado já abre. (Na 3.0.0 a lista era um \\`hm-data-grid\\`; voltou ao seletor a\n pedido do usuário.)\n- Modal: nome obrigatório (1–100, aparado), tipo obrigatório, descrição opcional; na criação a descrição vazia não\n vai no corpo; na edição vai como \\`null\\` (apaga). Tipos oferecidos: só \\`assignable !== false\\`. A falha aparece no\n modal (\\`hm-alert\\`) e o modal continua aberto.\n- **Grupo de administração** (\\`is_admin\\`): gerido só no painel central (Argos), não aparece no auto-serviço. O\n servidor não o devolve em \\`GET /groups\\` e responde \\`404\\` a editar, excluir e mexer nos membros dele (como grupo\n de outro projeto). Servidor antigo que ainda o devolva (\\`is_admin: true\\`): a tela descarta na carga da lista\n (\\`load\\`) e na lista de grupos do novo acesso (**Cliente › Acesso**). O Administrador (\\`assignable: false\\`)\n nunca entra nos tipos do modal. Os textos \\`adminGroupTag\\`, \\`adminGroupNote\\`, \\`adminGroupEditNote\\` e\n \\`adminGroupLastMemberWarning\\` saíram do catálogo na 3.3.1.\n- **Erros** (\\`autoServiceErrorText\\`, em \\`auto-service-errors.ts\\`): o código \\`error\\` da resposta vira o texto de\n \\`autoService.groupErrors\\` no idioma ativo — \\`invalid_type\\`, \\`admin_type_reserved\\`, \\`empty_update\\`,\n \\`group_project_immutable\\` (400); \\`group_name_taken\\`, \\`admin_group_immutable\\`, \\`admin_group_protected\\`,\n \\`admin_group_last_member\\`, \\`admin_group_access_protected\\` (409; de servidor antigo — o atual responde 404 ao\n grupo de administração); \\`404\\` de grupo → \\`groupNotFound\\`. Código\n desconhecido usa o \\`detail\\` do servidor e, por último, o texto genérico (\\`groupSaveError\\`, \\`membersSaveError\\` ou\n \\`deleteError\\`). O \\`AutoServiceDeleteFeedback\\` usa a mesma função em todas as exclusões do auto-serviço.\n- Sugestões: até 8, da rota própria (ordem do servidor: mais grupos do projeto primeiro), em chips; sem \"ver mais\".\n Falha na rota deixa sem sugestões. Busca: a partir de 2 caracteres, depois de 300 ms sem digitar, \\`limit=10\\`.\n Membros atuais são retirados dos dois (as sugestões já vêm sem eles do servidor; a busca, pelos membros já\n carregados).\n- **Carga progressiva** (desde a 3.3.4): a tabela de membros e o modal pedem o grupo inteiro em páginas de 200, uma\n depois da outra (\\`MembersLoader\\`, \\`groups/members-loader.ts\\`), e funcionam desde a primeira; enquanto carrega\n aparece \\`membersLoadingProgress\\` (\"Carregando membros… 400 de 1.250\") com \\`membersPartialResults\\`. Sem paginador:\n a grade usa rolagem virtual. A busca é local e instantânea (nome, login, e-mail). Falha numa página mostra\n \\`membersLoadIncomplete\\` com \\`retryLoad\\`, que continua da página que falhou. Fechar o modal, trocar de grupo ou\n destruir a tela cancela a página pendente. \\`countChange\\` emite o total do servidor. No modal, um **Novo** que\n aparece numa página que chega depois sai das inclusões (já era membro); o número ao lado de \"Membros do grupo\" é o\n total do grupo mais os novos.\n- Funções oferecidas pela tela: Administrador e Editor; vazio grava \\`role: null\\` (membro sem função, que só vê os outros\\n membros nas configurações do app). Outro valor o servidor recusa (\\`400 invalid_role\\`).\n- **Salvar alterações** grava só a diferença, em sequência: adições, depois remoções, depois trocas de função (dos\n membros que ficaram). Ao terminar, emite \\`saved\\` e a tabela de membros recarrega. Se uma chamada falhar, o modal mostra o\n erro, emite \\`saved\\` (parte pode ter sido gravada) e recarrega os membros.\n`;\n"],"names":[],"mappings":"AAAA;AACA;AACA;AACA,uBAAe,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyEd;;"}
@@ -67,7 +67,12 @@ grupo e só grava quando você confirma.
67
67
  (ou fechar o modal) descarta o que não foi salvo. Se uma parte falhar, o modal mostra o motivo e recarrega a
68
68
  lista com o que de fato ficou gravado.
69
69
 
70
- Depois de salvar, a tabela **Membros do grupo** é recarregada.
70
+ Em grupos grandes, a lista chega aos poucos: ela já aparece com os primeiros membros e mostra "Carregando membros… 400
71
+ de 1.250" até terminar. **Buscar membro** filtra na hora, por nome, login ou e-mail; enquanto a lista carrega, mais
72
+ gente pode aparecer na busca. Se a carga falhar no meio, **Tentar de novo** continua de onde parou.
73
+
74
+ Depois de salvar, a tabela **Membros do grupo** é recarregada. Ela também mostra o grupo inteiro, com a busca no
75
+ campo acima dela.
71
76
 
72
77
  ## O que esta tela não faz
73
78
 
@@ -78,4 +83,4 @@ Depois de salvar, a tabela **Membros do grupo** é recarregada.
78
83
  `;
79
84
 
80
85
  export { groups_usage as default };
81
- //# sourceMappingURL=trismegisto-spine-auto-service-groups.usage-OvWjNeeT.mjs.map
86
+ //# sourceMappingURL=trismegisto-spine-auto-service-groups.usage-De2MhlE7.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"trismegisto-spine-auto-service-groups.usage-OvWjNeeT.mjs","sources":["../../../projects/ui/auto-service/src/docs/groups.usage.ts"],"sourcesContent":["// Manual de uso da tela Grupos do auto-serviço (`RouteDocument.load`), em Markdown. Módulo próprio,\n// carregado só pelo `import()` de `AUTO_SERVICE_DOCUMENTS`: vira um chunk à parte no FESM e fica fora do\n// bundle inicial. Não importe estaticamente nem reexporte no `public-api.ts`.\nexport default `# Grupos\n\nMantenha os grupos do projeto do seu app sem depender do painel central: crie, edite e exclua grupos, veja os\nmembros de cada um, adicione e remova pessoas e defina a função de cada uma.\n\n## Antes de começar\n\nO auto-serviço administra o app em que você está logado: não há client para escolher. É preciso estar no grupo de\nadministração do projeto desse app e com o crachá **Administrador** ativo. Se aparecer o aviso de que você não\nadministra este app, falta uma dessas duas coisas: troque de crachá no menu do seu perfil, se disponível, ou peça\nacesso a quem administra o projeto.\n\n## A lista de grupos\n\nA tela abre com o botão **Novo grupo** e um cartão com dois seletores (o grupo de administração do projeto não\nentra; veja abaixo):\n\n1. **Nível de acesso**: o tipo do grupo (o crachá), do nível mais alto para o mais baixo.\n2. **Grupo**: só os grupos do nível escolhido. Trocar o nível limpa o grupo escolhido.\n\nClique em **Consultar** para abrir o grupo embaixo: um cartão com o nome, os selos do tipo (a cor acompanha o nível)\ne da situação (**Ativo** ou **Inativo**), a descrição e os botões **Editar** e **Excluir**, e a tabela **Membros do\ngrupo**.\n\n## Criar e editar\n\n- **Novo grupo** (acima dos seletores) abre o modal com **Nome** (obrigatório, até 100 caracteres), **Tipo**\n (obrigatório) e **Descrição** (opcional). **Criar** grava e já abre o grupo novo, para incluir os membros.\n- **Editar** abre o mesmo modal preenchido, com a caixa **Ativo** a mais: desmarcada, o grupo fica inativo.\n **Salvar** grava; apagar a descrição a remove.\n- O tipo **Administrador** não aparece na lista de tipos: ele é só do grupo de administração, que não se gere aqui.\n- Se o servidor recusar, a mensagem aparece no próprio modal e nada é gravado. Os casos mais comuns: já existe um\n grupo com esse nome no projeto, ou o tipo escolhido deixou de existir.\n\n## Excluir\n\n**Excluir** pede confirmação (**Excluir grupo?**, com o nome do grupo). Excluir um grupo apaga também os membros\ndele, os acessos a telas (**Cliente › Acesso**) e as permissões em aplicações (**Aplicações › Acessos**), e não\npode ser desfeito. Depois aparece **Exclusão concluída.**; se o servidor recusar, a mensagem diz o motivo e o grupo\ncontinua aberto.\n\n## O grupo de administração\n\nTodo projeto tem um, e só um, grupo de administração: quem está nele administra o app no auto-serviço. Ele é gerido\nsó no painel central (Argos) e não aparece no auto-serviço: não está nos seletores, não se edita nem se exclui aqui, e os\nmembros dele também não se gerenciam por esta tela. Para incluir ou tirar alguém da administração do app, peça a\nquem administra o Argos.\n\n## Gerenciar membros\n\nCom o grupo aberto, clique em **Gerenciar membros**. O modal **Membros — nome do grupo** junta tudo o que muda no\ngrupo e só grava quando você confirma.\n\n- **Adicionar:** em **Adicionar usuário**, digite pelo menos 2 letras do nome, do login ou do e-mail e escolha a\n pessoa na lista (quem já é membro não aparece). Se quiser, escolha a **Função** (Administrador ou Editor) e clique\n em **Adicionar**. A pessoa entra na lista com o selo **Novo**.\n- **Sugestões:** até 8 pessoas que já estão em outros grupos deste sistema e ainda não neste (primeiro quem está em\n mais grupos). Passe o mouse para ver em quais; clicar inclui direto, com a função escolhida ao lado.\n- **Mudar a função:** use a coluna **Função** de cada membro. **Membro (sem função)** deixa a pessoa como membro sem função: ela só vê os outros membros.\n- **Remover:** marque os membros na lista. Eles vão para o painel **Remoção**, ao lado (vazio, ele só mostra a dica), e a função deles fica\n travada. **Desfazer remoção** (o ícone ao lado do nome) devolve o membro. Marcar alguém com o selo **Novo**\n apenas o tira da lista, porque ele ainda não foi gravado.\n- **Salvar alterações** grava tudo de uma vez: as pessoas novas, as remoções e as funções trocadas. **Cancelar**\n (ou fechar o modal) descarta o que não foi salvo. Se uma parte falhar, o modal mostra o motivo e recarrega a\n lista com o que de fato ficou gravado.\n\nDepois de salvar, a tabela **Membros do grupo** é recarregada.\n\n## O que esta tela não faz\n\n- Não mexe em grupos de outros projetos: só aparecem, e só se alteram, os grupos do projeto deste app.\n- Não mexe no grupo de administração do projeto: ele fica só no painel central (Argos).\n- Não define o que o grupo pode acessar. As telas que cada grupo vê ficam em **Cliente › Acesso**; as operações que\n cada grupo pode chamar numa aplicação ficam em **Aplicações › Acessos**.\n`;\n"],"names":[],"mappings":"AAAA;AACA;AACA;AACA,mBAAe,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0Ed;;"}
1
+ {"version":3,"file":"trismegisto-spine-auto-service-groups.usage-De2MhlE7.mjs","sources":["../../../projects/ui/auto-service/src/docs/groups.usage.ts"],"sourcesContent":["// Manual de uso da tela Grupos do auto-serviço (`RouteDocument.load`), em Markdown. Módulo próprio,\n// carregado só pelo `import()` de `AUTO_SERVICE_DOCUMENTS`: vira um chunk à parte no FESM e fica fora do\n// bundle inicial. Não importe estaticamente nem reexporte no `public-api.ts`.\nexport default `# Grupos\n\nMantenha os grupos do projeto do seu app sem depender do painel central: crie, edite e exclua grupos, veja os\nmembros de cada um, adicione e remova pessoas e defina a função de cada uma.\n\n## Antes de começar\n\nO auto-serviço administra o app em que você está logado: não há client para escolher. É preciso estar no grupo de\nadministração do projeto desse app e com o crachá **Administrador** ativo. Se aparecer o aviso de que você não\nadministra este app, falta uma dessas duas coisas: troque de crachá no menu do seu perfil, se disponível, ou peça\nacesso a quem administra o projeto.\n\n## A lista de grupos\n\nA tela abre com o botão **Novo grupo** e um cartão com dois seletores (o grupo de administração do projeto não\nentra; veja abaixo):\n\n1. **Nível de acesso**: o tipo do grupo (o crachá), do nível mais alto para o mais baixo.\n2. **Grupo**: só os grupos do nível escolhido. Trocar o nível limpa o grupo escolhido.\n\nClique em **Consultar** para abrir o grupo embaixo: um cartão com o nome, os selos do tipo (a cor acompanha o nível)\ne da situação (**Ativo** ou **Inativo**), a descrição e os botões **Editar** e **Excluir**, e a tabela **Membros do\ngrupo**.\n\n## Criar e editar\n\n- **Novo grupo** (acima dos seletores) abre o modal com **Nome** (obrigatório, até 100 caracteres), **Tipo**\n (obrigatório) e **Descrição** (opcional). **Criar** grava e já abre o grupo novo, para incluir os membros.\n- **Editar** abre o mesmo modal preenchido, com a caixa **Ativo** a mais: desmarcada, o grupo fica inativo.\n **Salvar** grava; apagar a descrição a remove.\n- O tipo **Administrador** não aparece na lista de tipos: ele é só do grupo de administração, que não se gere aqui.\n- Se o servidor recusar, a mensagem aparece no próprio modal e nada é gravado. Os casos mais comuns: já existe um\n grupo com esse nome no projeto, ou o tipo escolhido deixou de existir.\n\n## Excluir\n\n**Excluir** pede confirmação (**Excluir grupo?**, com o nome do grupo). Excluir um grupo apaga também os membros\ndele, os acessos a telas (**Cliente › Acesso**) e as permissões em aplicações (**Aplicações › Acessos**), e não\npode ser desfeito. Depois aparece **Exclusão concluída.**; se o servidor recusar, a mensagem diz o motivo e o grupo\ncontinua aberto.\n\n## O grupo de administração\n\nTodo projeto tem um, e só um, grupo de administração: quem está nele administra o app no auto-serviço. Ele é gerido\nsó no painel central (Argos) e não aparece no auto-serviço: não está nos seletores, não se edita nem se exclui aqui, e os\nmembros dele também não se gerenciam por esta tela. Para incluir ou tirar alguém da administração do app, peça a\nquem administra o Argos.\n\n## Gerenciar membros\n\nCom o grupo aberto, clique em **Gerenciar membros**. O modal **Membros — nome do grupo** junta tudo o que muda no\ngrupo e só grava quando você confirma.\n\n- **Adicionar:** em **Adicionar usuário**, digite pelo menos 2 letras do nome, do login ou do e-mail e escolha a\n pessoa na lista (quem já é membro não aparece). Se quiser, escolha a **Função** (Administrador ou Editor) e clique\n em **Adicionar**. A pessoa entra na lista com o selo **Novo**.\n- **Sugestões:** até 8 pessoas que já estão em outros grupos deste sistema e ainda não neste (primeiro quem está em\n mais grupos). Passe o mouse para ver em quais; clicar inclui direto, com a função escolhida ao lado.\n- **Mudar a função:** use a coluna **Função** de cada membro. **Membro (sem função)** deixa a pessoa como membro sem função: ela só vê os outros membros.\n- **Remover:** marque os membros na lista. Eles vão para o painel **Remoção**, ao lado (vazio, ele só mostra a dica), e a função deles fica\n travada. **Desfazer remoção** (o ícone ao lado do nome) devolve o membro. Marcar alguém com o selo **Novo**\n apenas o tira da lista, porque ele ainda não foi gravado.\n- **Salvar alterações** grava tudo de uma vez: as pessoas novas, as remoções e as funções trocadas. **Cancelar**\n (ou fechar o modal) descarta o que não foi salvo. Se uma parte falhar, o modal mostra o motivo e recarrega a\n lista com o que de fato ficou gravado.\n\nEm grupos grandes, a lista chega aos poucos: ela já aparece com os primeiros membros e mostra \"Carregando membros… 400\nde 1.250\" até terminar. **Buscar membro** filtra na hora, por nome, login ou e-mail; enquanto a lista carrega, mais\ngente pode aparecer na busca. Se a carga falhar no meio, **Tentar de novo** continua de onde parou.\n\nDepois de salvar, a tabela **Membros do grupo** é recarregada. Ela também mostra o grupo inteiro, com a busca no\ncampo acima dela.\n\n## O que esta tela não faz\n\n- Não mexe em grupos de outros projetos: só aparecem, e só se alteram, os grupos do projeto deste app.\n- Não mexe no grupo de administração do projeto: ele fica só no painel central (Argos).\n- Não define o que o grupo pode acessar. As telas que cada grupo vê ficam em **Cliente › Acesso**; as operações que\n cada grupo pode chamar numa aplicação ficam em **Aplicações › Acessos**.\n`;\n"],"names":[],"mappings":"AAAA;AACA;AACA;AACA,mBAAe,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+Ed;;"}
@@ -7,7 +7,7 @@ import { ViewService, AuthorizationService } from '@orfeu/angular/core';
7
7
  import { IconButtonComponent, IconComponent, MenuButtonComponent, StatePageComponent } from '@trismegisto/angular';
8
8
  import { pluralMessage, formatMessage } from '@trismegisto/primitives';
9
9
  import { spineRoutesOf, SpineQuickAccessService, SPINE_MESSAGES } from '@trismegisto/spine';
10
- import { SpineDocsService, SpineSettingsService, SpineNewsService, ShortcutService, SpineInitialSetupService } from '@trismegisto/spine/layout';
10
+ import { SpineDocsService, SpineSettingsService, SpineNewsService, spineSuggestionsEnabled, SPINE_SUGGESTIONS_PAGE_PATH, ShortcutService, SpineInitialSetupService } from '@trismegisto/spine/layout';
11
11
  import { map } from 'rxjs';
12
12
  import { AUTO_SERVICE_GROUPS_TEMPLATE_ROUTER_ID, AUTO_SERVICE_APPS_TEMPLATE_ROUTER_ID, AUTO_SERVICE_CLIENT_TEMPLATE_ROUTER_ID, AUTO_SERVICE_AUDIT_TEMPLATE_ROUTER_ID, AUTO_SERVICE_LOGIN_ATTEMPTS_TEMPLATE_ROUTER_ID, AUTO_SERVICE_NEWS_TEMPLATE_ROUTER_ID, AUTO_SERVICE_SUGGESTIONS_TEMPLATE_ROUTER_ID, AUTO_SERVICE_RATINGS_TEMPLATE_ROUTER_ID, AUTO_SERVICE_HOME_TEMPLATE_ROUTER_ID } from './trismegisto-spine-auto-service.mjs';
13
13
 
@@ -159,6 +159,9 @@ class HomeComponent {
159
159
  this.autoRedirect = inject(HomeAutoRedirect);
160
160
  this.messages = inject(SPINE_MESSAGES);
161
161
  this.t = this.messages.home;
162
+ /** "Sugestões do app" em Ajuda e recursos, só com o recurso ligado (o mesmo nome da página). */
163
+ this.suggestions = spineSuggestionsEnabled() ? this.messages.suggestions.page.title : null;
164
+ this.suggestionsPath = SPINE_SUGGESTIONS_PAGE_PATH;
162
165
  this.uid = `home-${++instances}`;
163
166
  this.view = inject(ViewService);
164
167
  this.tree = toSignal(this.view.currentView$.pipe(map(cleanTree)), {
@@ -304,12 +307,12 @@ class HomeComponent {
304
307
  return text.charAt(0).toLocaleUpperCase(locale) + text.slice(1);
305
308
  }
306
309
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: HomeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
307
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: HomeComponent, isStandalone: true, selector: "home", ngImport: i0, template: "@if (noAccess()) {\n <hm-state-page kind=\"no-rights\" [heading]=\"t.noAccessTitle\" [description]=\"t.noAccessText\" action=\"\" />\n} @else {\n <div class=\"home\">\n <!-- Sauda\u00E7\u00E3o \u00E0 esquerda; as novidades, quando houver, ocupam o espa\u00E7o \u00E0 direita dela. -->\n <div class=\"hero\">\n <div class=\"hero__text\">\n <p class=\"hero__greeting\">{{ greeting }}</p>\n <p class=\"hero__intro\">{{ t.intro }}</p>\n </div>\n @if (newsRows().length) {\n <!-- Compacto: cabe\u00E7alho numa linha e cada novidade numa linha s\u00F3 (t\u00EDtulo e o come\u00E7o do texto). -->\n <section class=\"card card--compact hero__news\" [attr.aria-labelledby]=\"uid + '-news'\">\n <div class=\"card__head\">\n <span class=\"card__heading\">\n <h2 class=\"card__title\" [id]=\"uid + '-news'\">{{ t.news }}</h2>\n <span class=\"badge\">{{ t.newsBadge }}</span>\n </span>\n <button type=\"button\" class=\"more\" (click)=\"openNews()\">{{ t.newsAll }}</button>\n </div>\n <ul class=\"list\">\n @for (entry of newsRows(); track entry.id) {\n <li>\n <button type=\"button\" class=\"news\" [title]=\"entry.title + ' \u2014 ' + entry.text\" (click)=\"openNews(entry.id)\">\n <span class=\"news__dot\" [class.news__dot--seen]=\"!entry.unseen\" aria-hidden=\"true\"></span>\n <span class=\"news__title\">{{ entry.title }}</span>\n <span class=\"news__body\">{{ entry.text }}</span>\n </button>\n </li>\n }\n </ul>\n </section>\n }\n </div>\n\n <!-- Atalhos de trabalho e privilegiados dividem a mesma linha (cada grupo na propor\u00E7\u00E3o dos seus atalhos). -->\n @if (workTiles().length || adminTiles().length) {\n <div class=\"shortcuts\">\n @if (workTiles().length) {\n <section class=\"block shortcuts__group\" [style.--tiles]=\"workTiles().length\" [attr.aria-labelledby]=\"uid + '-work'\">\n <h2 class=\"block__title\" [id]=\"uid + '-work'\">{{ descended() ? t.yourScreens : t.modules }}</h2>\n <ng-container *ngTemplateOutlet=\"tileGrid; context: { $implicit: workTiles(), admin: false }\" />\n </section>\n }\n @if (adminTiles().length) {\n <section class=\"block shortcuts__group\" [style.--tiles]=\"adminTiles().length\" [attr.aria-labelledby]=\"uid + '-admin'\">\n <h2 class=\"block__title\" [id]=\"uid + '-admin'\">{{ t.privileged }}</h2>\n <ng-container *ngTemplateOutlet=\"tileGrid; context: { $implicit: adminTiles(), admin: true }\" />\n </section>\n }\n </div>\n }\n\n <!-- Linha 1: o que a pessoa usa (recentes | favoritos). Linha 2: o resto do sistema (m\u00F3dulos | ajuda).\n Sem um dos lados, o outro ocupa a linha inteira. -->\n <div class=\"pair\">\n @if (recentRows().length) {\n <section class=\"card\" [attr.aria-labelledby]=\"uid + '-recent'\">\n <div class=\"card__head\">\n <h2 class=\"card__title\" [id]=\"uid + '-recent'\">{{ t.recent }}</h2>\n @if (moreRecents()) {\n <button type=\"button\" class=\"more\" [attr.aria-expanded]=\"showAllRecents()\" (click)=\"showAllRecents.set(!showAllRecents())\">\n {{ showAllRecents() ? t.seeLess : t.seeAll }}\n </button>\n }\n </div>\n <ul class=\"list\" [attr.aria-label]=\"t.recentCaption\">\n @for (item of recentRows(); track item.id) {\n <li class=\"recent\">\n <button type=\"button\" class=\"recent__open\" (click)=\"go(item.redirect)\">\n <hm-icon class=\"recent__icon\" [shape]=\"item.icon || 'grid-layout'\" />\n <span class=\"recent__name\">{{ item.displayName }}</span>\n <span class=\"recent__where\">{{ item.where }}</span>\n @if (item.when) {\n <span class=\"recent__when\">{{ item.when }}</span>\n }\n </button>\n <hm-icon-button\n emphasis=\"subtle\"\n size=\"small\"\n icon=\"heart\"\n [solid]=\"isFavorite(item.id)\"\n [class.heart--on]=\"isFavorite(item.id)\"\n [label]=\"favoriteLabel(item)\"\n (clicked)=\"toggleFavorite(item)\"\n />\n <hm-menu-button\n class=\"recent__menu\"\n icon=\"ellipsis-vertical\"\n shape=\"circle\"\n size=\"small\"\n align=\"end\"\n [showArrow]=\"false\"\n [label]=\"recentMenuLabel(item)\"\n [items]=\"recentMenu\"\n (chosen)=\"onRecentMenu(item, $event)\"\n />\n </li>\n }\n </ul>\n </section>\n }\n\n <section class=\"card\" [attr.aria-labelledby]=\"uid + '-fav'\">\n <div class=\"card__head\">\n <h2 class=\"card__title\" [id]=\"uid + '-fav'\">{{ t.favorites }}</h2>\n @if (moreFavorites()) {\n <button type=\"button\" class=\"more\" [attr.aria-expanded]=\"showAllFavorites()\" (click)=\"showAllFavorites.set(!showAllFavorites())\">\n {{ showAllFavorites() ? t.seeLess : t.seeAll }}\n </button>\n }\n </div>\n @if (visibleFavorites().length) {\n <ul class=\"list\">\n @for (fav of visibleFavorites(); track fav.id) {\n <li>\n <button type=\"button\" class=\"row\" (click)=\"go(fav.redirect)\">\n <hm-icon class=\"row__icon\" [shape]=\"fav.icon || 'grid-layout'\" />\n <span class=\"row__name\">{{ fav.displayName }}</span>\n <span class=\"row__meta\">{{ whereOf(fav) }}</span>\n <hm-icon class=\"row__chevron\" shape=\"angle-right\" size=\"sm\" />\n </button>\n </li>\n }\n </ul>\n } @else {\n <p class=\"card__empty\">{{ t.favoritesEmpty }}</p>\n }\n </section>\n </div>\n\n <div class=\"pair\">\n @if (systems().length) {\n <section class=\"card\" [attr.aria-labelledby]=\"uid + '-systems'\">\n <div class=\"card__head card__head--stacked\">\n <h2 class=\"card__title\" [id]=\"uid + '-systems'\">{{ t.explore }}</h2>\n <p class=\"card__intro\">{{ t.exploreIntro }}</p>\n </div>\n <ul class=\"systems\">\n @for (system of systems(); track system.id) {\n <!-- O card inteiro abre o m\u00F3dulo (como os atalhos): uma seta no canto, sem link pr\u00F3prio. -->\n <li class=\"systems__cell\">\n <button type=\"button\" class=\"system\" (click)=\"go(system.redirect)\">\n <span class=\"system__head\">\n <hm-icon class=\"system__icon\" [shape]=\"system.icon\" size=\"lg\" />\n <span class=\"system__name\" [title]=\"system.label\">{{ system.label }}</span>\n <span class=\"badge badge--neutral\">{{ system.count }}</span>\n <hm-icon class=\"system__chevron\" shape=\"angle-right\" size=\"sm\" />\n </span>\n @if (system.description) {\n <span class=\"system__description\">{{ system.description }}</span>\n }\n @if (system.chips.length) {\n <span class=\"system__chips\">\n @for (chip of system.chips; track chip) {\n <span class=\"system__chip\">{{ chip }}</span>\n }\n </span>\n }\n </button>\n </li>\n }\n </ul>\n </section>\n }\n\n <section class=\"card\" [attr.aria-labelledby]=\"uid + '-help'\">\n <div class=\"card__head\">\n <h2 class=\"card__title\" [id]=\"uid + '-help'\">{{ t.help }}</h2>\n </div>\n <ul class=\"list\">\n @for (area of areas(); track area.routed_id) {\n <li>\n <button type=\"button\" class=\"row\" (click)=\"go(area.route_redirect)\">\n <hm-icon class=\"row__icon\" [shape]=\"area.route_icon || 'home'\" />\n <span class=\"row__name\">{{ area.route_display_name }}</span>\n <hm-icon class=\"row__chevron\" shape=\"angle-right\" size=\"sm\" />\n </button>\n </li>\n }\n <li>\n <button type=\"button\" class=\"row\" (click)=\"openDocs()\">\n <hm-icon class=\"row__icon\" shape=\"book\" />\n <span class=\"row__name\">{{ t.systemDocs }}</span>\n <hm-icon class=\"row__chevron\" shape=\"angle-right\" size=\"sm\" />\n </button>\n </li>\n <li>\n <button type=\"button\" class=\"row\" (click)=\"openShortcuts()\">\n <hm-icon class=\"row__icon\" shape=\"keyboard\" />\n <span class=\"row__name\">{{ t.shortcuts }}</span>\n <hm-icon class=\"row__chevron\" shape=\"angle-right\" size=\"sm\" />\n </button>\n </li>\n <li>\n <button type=\"button\" class=\"row\" (click)=\"openEssentials()\">\n <hm-icon class=\"row__icon\" shape=\"help\" />\n <span class=\"row__name\">{{ t.essentials }}</span>\n <hm-icon class=\"row__chevron\" shape=\"angle-right\" size=\"sm\" />\n </button>\n </li>\n <li>\n <button type=\"button\" class=\"row\" (click)=\"openSettings()\">\n <hm-icon class=\"row__icon\" shape=\"cog\" />\n <span class=\"row__name\">{{ t.settings }}</span>\n <hm-icon class=\"row__chevron\" shape=\"angle-right\" size=\"sm\" />\n </button>\n </li>\n </ul>\n </section>\n </div>\n </div>\n}\n\n<ng-template #tileGrid let-tiles let-admin=\"admin\">\n <ul class=\"tiles\" [class.tiles--admin]=\"admin\">\n @for (tile of tiles; track tile.id; let i = $index) {\n <li class=\"tiles__cell\" [style.--tile-order]=\"i\">\n <button type=\"button\" class=\"tile\" (click)=\"go(tile.redirect)\">\n <hm-icon class=\"tile__icon\" [shape]=\"tile.icon ?? 'grid-layout'\" size=\"lg\" />\n <span class=\"tile__text\">\n <span class=\"tile__label\">{{ tile.label }}</span>\n @if (caption(tile); as meta) {\n <span class=\"tile__meta\">{{ meta }}</span>\n }\n </span>\n <hm-icon class=\"tile__chevron\" shape=\"angle-right\" size=\"sm\" />\n </button>\n </li>\n }\n </ul>\n</ng-template>\n", styles: [":host{display:flex;flex:1 1 auto;flex-direction:column;min-block-size:100%;min-inline-size:0}hm-state-page{flex:1 1 auto}ul{margin:0;padding:0;list-style:none}p{margin:0}.home{container-type:inline-size;display:flex;flex-direction:column;gap:var(--hm-space-md);min-inline-size:0}.hero{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--hm-space-md)}.hero__text{display:flex;flex:1 1 20rem;flex-direction:column;gap:var(--hm-space-micro);min-inline-size:0}.hero__news{flex:0 1 32rem;min-inline-size:min(100%,20rem)}.hero__greeting{color:var(--hm-text);font-size:var(--hm-font-size-xxl);font-weight:var(--hm-font-weight-bold);line-height:var(--hm-line-height-tight)}.hero__intro{color:var(--hm-muted);font-size:var(--hm-font-size-md)}.block{display:flex;flex-direction:column;gap:var(--hm-space-sm);min-inline-size:0}.block__title{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-semibold)}.news,.row,.system,.recent__open,.more,.tile{display:flex;align-items:center;gap:var(--hm-space-sm);border:var(--hm-border-hairline) solid transparent;border-radius:var(--hm-radius-control);background:transparent;color:var(--hm-text);font:inherit;text-align:start;cursor:pointer;transition:background-color var(--hm-duration-quick) var(--hm-ease-standard),border-color var(--hm-duration-quick) var(--hm-ease-standard)}.news:hover,.row:hover,.system:hover,.recent__open:hover,.more:hover,.tile:hover{background:var(--hm-hover)}.news:focus-visible,.row:focus-visible,.system:focus-visible,.recent__open:focus-visible,.more:focus-visible,.tile:focus-visible{outline:var(--hm-border-thin) solid var(--hm-accent);outline-offset:2px}@media(forced-colors:active){.news,.row,.system,.recent__open,.more,.tile{border-color:ButtonText}.news:focus-visible,.row:focus-visible,.system:focus-visible,.recent__open:focus-visible,.more:focus-visible,.tile:focus-visible{outline-color:Highlight}}.shortcuts{display:flex;flex-wrap:wrap;gap:var(--hm-space-md)}.shortcuts__group{flex:var(--tiles, 1) 1 calc(var(--tiles, 1) * 13rem + (var(--tiles, 1) - 1) * var(--hm-space-sm))}.tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,13rem),1fr));gap:var(--hm-space-sm)}.tiles__cell{display:flex}.tile{flex:1 1 auto;gap:var(--hm-space-md);padding:var(--hm-space-md);border-color:var(--hm-divider);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.tile:hover{border-color:var(--hm-accent);background:var(--hm-surface-raised)}.tile__icon{flex:none;color:var(--hm-accent)}.tiles--admin .tile__icon{color:var(--hm-warning)}.tile__text{display:flex;flex:1 1 auto;flex-direction:column;gap:var(--hm-space-nano);min-inline-size:0}.tile__label{font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-semibold);overflow-wrap:anywhere}.tile__meta{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.tile__chevron,.row__chevron{flex:none;color:var(--hm-muted)}@media(prefers-reduced-motion:no-preference){.tiles__cell{animation:tile-in var(--hm-duration-standard) var(--hm-ease-decelerate) both;animation-delay:calc(min(var(--tile-order, 0),10)*35ms)}}:host-context([data-motion=full]) .tiles__cell{animation:tile-in var(--hm-duration-standard) var(--hm-ease-decelerate) both;animation-delay:calc(min(var(--tile-order, 0),10)*35ms)}:host-context([data-motion=reduce]) .tiles__cell{animation:none}@keyframes tile-in{0%{opacity:0;transform:translateY(var(--hm-space-xs))}}.pair{display:grid;grid-template-columns:minmax(0,2fr) minmax(0,1fr);gap:var(--hm-space-md);align-items:stretch}.pair>:only-child{grid-column:1/-1}.stack{display:flex;flex-direction:column;gap:var(--hm-space-md);min-inline-size:0}.stack>.card:last-child{flex:1 1 auto}@container (max-width: 56rem){.pair{grid-template-columns:minmax(0,1fr)}}.card{display:flex;flex-direction:column;gap:var(--hm-space-sm);min-inline-size:0;padding:var(--hm-space-md);border:var(--hm-border-hairline) solid var(--hm-divider);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.card__head{display:flex;align-items:center;justify-content:space-between;gap:var(--hm-space-sm)}.card__head--stacked{flex-direction:column;align-items:flex-start;gap:var(--hm-space-micro)}.card__heading{display:flex;align-items:center;gap:var(--hm-space-xs)}.card__title{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-semibold)}.card__intro,.card__empty{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.more{flex:none;padding:var(--hm-space-nano) var(--hm-space-xs);color:var(--hm-accent);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-medium)}.badge{flex:none;padding:var(--hm-space-nano) var(--hm-space-xs);border-radius:var(--hm-radius-micro);background:var(--hm-accent-subtle);color:var(--hm-accent);font-size:var(--hm-font-size-3xs);font-weight:var(--hm-font-weight-semibold);letter-spacing:.04em;text-transform:uppercase}.badge--neutral{background:var(--hm-surface-muted);color:var(--hm-muted);letter-spacing:normal;text-transform:none}.card--compact{gap:var(--hm-space-micro);padding:var(--hm-space-sm) var(--hm-space-md)}.recent{display:flex;align-items:center;gap:var(--hm-space-micro)}.recent__open{flex:1 1 auto;min-inline-size:0;padding:var(--hm-space-nano) var(--hm-space-xs)}.recent__icon{flex:none;color:var(--hm-accent)}.recent__name{flex:none;max-inline-size:50%;overflow:hidden;font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold);text-overflow:ellipsis;white-space:nowrap}.recent__where{flex:1 1 auto;min-inline-size:0;overflow:hidden;color:var(--hm-muted);font-size:var(--hm-font-size-xs);text-overflow:ellipsis;white-space:nowrap}.recent__when{flex:none;color:var(--hm-muted);font-size:var(--hm-font-size-xs);white-space:nowrap}.heart--on{color:var(--hm-danger)}.systems{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,14rem),1fr));gap:var(--hm-space-sm)}.systems__cell{display:flex}.system{flex:1 1 auto;flex-direction:column;align-items:stretch;gap:var(--hm-space-xs);padding:var(--hm-space-sm) var(--hm-space-md);border-color:var(--hm-divider);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.system:hover{border-color:var(--hm-accent);background:var(--hm-surface-raised)}.system__head{display:flex;align-items:center;gap:var(--hm-space-sm);min-inline-size:0}.system__icon{flex:none;color:var(--hm-accent)}.system__name{min-inline-size:0;overflow:hidden;font-weight:var(--hm-font-weight-semibold);text-overflow:ellipsis;white-space:nowrap}.system__chevron{flex:none;margin-inline-start:auto;color:var(--hm-muted)}.system__description{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.system__chips{display:flex;flex-wrap:wrap;gap:var(--hm-space-micro)}.system__chip{padding:var(--hm-space-nano) var(--hm-space-xs);border:var(--hm-border-hairline) solid var(--hm-divider);border-radius:var(--hm-radius-pill);background:var(--hm-surface-muted);color:var(--hm-text);font-size:var(--hm-font-size-xs)}.list{display:flex;flex-direction:column}.list>li+li{border-block-start:var(--hm-border-hairline) solid var(--hm-divider)}.row{inline-size:100%;padding:var(--hm-space-nano) var(--hm-space-xs)}.row__icon{flex:none;color:var(--hm-muted)}.row__name{flex:1 1 auto;min-inline-size:0;overflow:hidden;font-size:var(--hm-font-size-sm);text-overflow:ellipsis;white-space:nowrap}.row__meta{flex:0 1 auto;min-inline-size:0;max-inline-size:45%;overflow:hidden;color:var(--hm-muted);font-size:var(--hm-font-size-xs);text-overflow:ellipsis;white-space:nowrap}.news{inline-size:100%;gap:var(--hm-space-xs);padding:var(--hm-space-nano) var(--hm-space-xs)}.news__dot{flex:none;inline-size:var(--hm-space-xs);block-size:var(--hm-space-xs);border-radius:var(--hm-radius-pill);background:var(--hm-accent)}.news__dot--seen{background:var(--hm-divider)}.news__title{flex:none;max-inline-size:50%;overflow:hidden;font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold);text-overflow:ellipsis;white-space:nowrap}.news__body{flex:1 1 auto;min-inline-size:0;overflow:hidden;color:var(--hm-muted);font-size:var(--hm-font-size-xs);text-overflow:ellipsis;white-space:nowrap}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { 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: MenuButtonComponent, selector: "hm-menu-button", inputs: ["label", "icon", "items", "showArrow", "align", "side", "size", "shape", "layer", "closeOnCheck", "disabled"], outputs: ["chosen", "checkedChange"] }, { kind: "component", type: StatePageComponent, selector: "hm-state-page", inputs: ["kind", "heading", "description", "action", "actionIcon"], outputs: ["actionTriggered"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
310
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: HomeComponent, isStandalone: true, selector: "home", ngImport: i0, template: "@if (noAccess()) {\n <hm-state-page kind=\"no-rights\" [heading]=\"t.noAccessTitle\" [description]=\"t.noAccessText\" action=\"\" />\n} @else {\n <div class=\"home\">\n <!-- Sauda\u00E7\u00E3o \u00E0 esquerda; as novidades, quando houver, ocupam o espa\u00E7o \u00E0 direita dela. -->\n <div class=\"hero\">\n <div class=\"hero__text\">\n <p class=\"hero__greeting\">{{ greeting }}</p>\n <p class=\"hero__intro\">{{ t.intro }}</p>\n </div>\n @if (newsRows().length) {\n <!-- Compacto: cabe\u00E7alho numa linha e cada novidade numa linha s\u00F3 (t\u00EDtulo e o come\u00E7o do texto). -->\n <section class=\"card card--compact hero__news\" [attr.aria-labelledby]=\"uid + '-news'\">\n <div class=\"card__head\">\n <span class=\"card__heading\">\n <h2 class=\"card__title\" [id]=\"uid + '-news'\">{{ t.news }}</h2>\n <span class=\"badge\">{{ t.newsBadge }}</span>\n </span>\n <button type=\"button\" class=\"more\" (click)=\"openNews()\">{{ t.newsAll }}</button>\n </div>\n <ul class=\"list\">\n @for (entry of newsRows(); track entry.id) {\n <li>\n <button type=\"button\" class=\"news\" [title]=\"entry.title + ' \u2014 ' + entry.text\" (click)=\"openNews(entry.id)\">\n <span class=\"news__dot\" [class.news__dot--seen]=\"!entry.unseen\" aria-hidden=\"true\"></span>\n <span class=\"news__title\">{{ entry.title }}</span>\n <span class=\"news__body\">{{ entry.text }}</span>\n </button>\n </li>\n }\n </ul>\n </section>\n }\n </div>\n\n <!-- Atalhos de trabalho e privilegiados dividem a mesma linha (cada grupo na propor\u00E7\u00E3o dos seus atalhos). -->\n @if (workTiles().length || adminTiles().length) {\n <div class=\"shortcuts\">\n @if (workTiles().length) {\n <section class=\"block shortcuts__group\" [style.--tiles]=\"workTiles().length\" [attr.aria-labelledby]=\"uid + '-work'\">\n <h2 class=\"block__title\" [id]=\"uid + '-work'\">{{ descended() ? t.yourScreens : t.modules }}</h2>\n <ng-container *ngTemplateOutlet=\"tileGrid; context: { $implicit: workTiles(), admin: false }\" />\n </section>\n }\n @if (adminTiles().length) {\n <section class=\"block shortcuts__group\" [style.--tiles]=\"adminTiles().length\" [attr.aria-labelledby]=\"uid + '-admin'\">\n <h2 class=\"block__title\" [id]=\"uid + '-admin'\">{{ t.privileged }}</h2>\n <ng-container *ngTemplateOutlet=\"tileGrid; context: { $implicit: adminTiles(), admin: true }\" />\n </section>\n }\n </div>\n }\n\n <!-- Linha 1: o que a pessoa usa (recentes | favoritos). Linha 2: o resto do sistema (m\u00F3dulos | ajuda).\n Sem um dos lados, o outro ocupa a linha inteira. -->\n <div class=\"pair\">\n @if (recentRows().length) {\n <section class=\"card\" [attr.aria-labelledby]=\"uid + '-recent'\">\n <div class=\"card__head\">\n <h2 class=\"card__title\" [id]=\"uid + '-recent'\">{{ t.recent }}</h2>\n @if (moreRecents()) {\n <button type=\"button\" class=\"more\" [attr.aria-expanded]=\"showAllRecents()\" (click)=\"showAllRecents.set(!showAllRecents())\">\n {{ showAllRecents() ? t.seeLess : t.seeAll }}\n </button>\n }\n </div>\n <ul class=\"list\" [attr.aria-label]=\"t.recentCaption\">\n @for (item of recentRows(); track item.id) {\n <li class=\"recent\">\n <button type=\"button\" class=\"recent__open\" (click)=\"go(item.redirect)\">\n <hm-icon class=\"recent__icon\" [shape]=\"item.icon || 'grid-layout'\" />\n <span class=\"recent__name\">{{ item.displayName }}</span>\n <span class=\"recent__where\">{{ item.where }}</span>\n @if (item.when) {\n <span class=\"recent__when\">{{ item.when }}</span>\n }\n </button>\n <hm-icon-button\n emphasis=\"subtle\"\n size=\"small\"\n icon=\"heart\"\n [solid]=\"isFavorite(item.id)\"\n [class.heart--on]=\"isFavorite(item.id)\"\n [label]=\"favoriteLabel(item)\"\n (clicked)=\"toggleFavorite(item)\"\n />\n <hm-menu-button\n class=\"recent__menu\"\n icon=\"ellipsis-vertical\"\n shape=\"circle\"\n size=\"small\"\n align=\"end\"\n [showArrow]=\"false\"\n [label]=\"recentMenuLabel(item)\"\n [items]=\"recentMenu\"\n (chosen)=\"onRecentMenu(item, $event)\"\n />\n </li>\n }\n </ul>\n </section>\n }\n\n <section class=\"card\" [attr.aria-labelledby]=\"uid + '-fav'\">\n <div class=\"card__head\">\n <h2 class=\"card__title\" [id]=\"uid + '-fav'\">{{ t.favorites }}</h2>\n @if (moreFavorites()) {\n <button type=\"button\" class=\"more\" [attr.aria-expanded]=\"showAllFavorites()\" (click)=\"showAllFavorites.set(!showAllFavorites())\">\n {{ showAllFavorites() ? t.seeLess : t.seeAll }}\n </button>\n }\n </div>\n @if (visibleFavorites().length) {\n <ul class=\"list\">\n @for (fav of visibleFavorites(); track fav.id) {\n <li>\n <button type=\"button\" class=\"row\" (click)=\"go(fav.redirect)\">\n <hm-icon class=\"row__icon\" [shape]=\"fav.icon || 'grid-layout'\" />\n <span class=\"row__name\">{{ fav.displayName }}</span>\n <span class=\"row__meta\">{{ whereOf(fav) }}</span>\n <hm-icon class=\"row__chevron\" shape=\"angle-right\" size=\"sm\" />\n </button>\n </li>\n }\n </ul>\n } @else {\n <p class=\"card__empty\">{{ t.favoritesEmpty }}</p>\n }\n </section>\n </div>\n\n <div class=\"pair\">\n @if (systems().length) {\n <section class=\"card\" [attr.aria-labelledby]=\"uid + '-systems'\">\n <div class=\"card__head card__head--stacked\">\n <h2 class=\"card__title\" [id]=\"uid + '-systems'\">{{ t.explore }}</h2>\n <p class=\"card__intro\">{{ t.exploreIntro }}</p>\n </div>\n <ul class=\"systems\">\n @for (system of systems(); track system.id) {\n <!-- O card inteiro abre o m\u00F3dulo (como os atalhos): uma seta no canto, sem link pr\u00F3prio. -->\n <li class=\"systems__cell\">\n <button type=\"button\" class=\"system\" (click)=\"go(system.redirect)\">\n <span class=\"system__head\">\n <hm-icon class=\"system__icon\" [shape]=\"system.icon\" size=\"lg\" />\n <span class=\"system__name\" [title]=\"system.label\">{{ system.label }}</span>\n <span class=\"badge badge--neutral\">{{ system.count }}</span>\n <hm-icon class=\"system__chevron\" shape=\"angle-right\" size=\"sm\" />\n </span>\n @if (system.description) {\n <span class=\"system__description\">{{ system.description }}</span>\n }\n @if (system.chips.length) {\n <span class=\"system__chips\">\n @for (chip of system.chips; track chip) {\n <span class=\"system__chip\">{{ chip }}</span>\n }\n </span>\n }\n </button>\n </li>\n }\n </ul>\n </section>\n }\n\n <section class=\"card\" [attr.aria-labelledby]=\"uid + '-help'\">\n <div class=\"card__head\">\n <h2 class=\"card__title\" [id]=\"uid + '-help'\">{{ t.help }}</h2>\n </div>\n <ul class=\"list\">\n @for (area of areas(); track area.routed_id) {\n <li>\n <button type=\"button\" class=\"row\" (click)=\"go(area.route_redirect)\">\n <hm-icon class=\"row__icon\" [shape]=\"area.route_icon || 'home'\" />\n <span class=\"row__name\">{{ area.route_display_name }}</span>\n <hm-icon class=\"row__chevron\" shape=\"angle-right\" size=\"sm\" />\n </button>\n </li>\n }\n <li>\n <button type=\"button\" class=\"row\" (click)=\"openDocs()\">\n <hm-icon class=\"row__icon\" shape=\"book\" />\n <span class=\"row__name\">{{ t.systemDocs }}</span>\n <hm-icon class=\"row__chevron\" shape=\"angle-right\" size=\"sm\" />\n </button>\n </li>\n @if (suggestions) {\n <li>\n <button type=\"button\" class=\"row\" (click)=\"go(suggestionsPath)\">\n <hm-icon class=\"row__icon\" shape=\"lightbulb\" />\n <span class=\"row__name\">{{ suggestions }}</span>\n <hm-icon class=\"row__chevron\" shape=\"angle-right\" size=\"sm\" />\n </button>\n </li>\n }\n <li>\n <button type=\"button\" class=\"row\" (click)=\"openShortcuts()\">\n <hm-icon class=\"row__icon\" shape=\"keyboard\" />\n <span class=\"row__name\">{{ t.shortcuts }}</span>\n <hm-icon class=\"row__chevron\" shape=\"angle-right\" size=\"sm\" />\n </button>\n </li>\n <li>\n <button type=\"button\" class=\"row\" (click)=\"openEssentials()\">\n <hm-icon class=\"row__icon\" shape=\"help\" />\n <span class=\"row__name\">{{ t.essentials }}</span>\n <hm-icon class=\"row__chevron\" shape=\"angle-right\" size=\"sm\" />\n </button>\n </li>\n <li>\n <button type=\"button\" class=\"row\" (click)=\"openSettings()\">\n <hm-icon class=\"row__icon\" shape=\"cog\" />\n <span class=\"row__name\">{{ t.settings }}</span>\n <hm-icon class=\"row__chevron\" shape=\"angle-right\" size=\"sm\" />\n </button>\n </li>\n </ul>\n </section>\n </div>\n </div>\n}\n\n<ng-template #tileGrid let-tiles let-admin=\"admin\">\n <ul class=\"tiles\" [class.tiles--admin]=\"admin\">\n @for (tile of tiles; track tile.id; let i = $index) {\n <li class=\"tiles__cell\" [style.--tile-order]=\"i\">\n <button type=\"button\" class=\"tile\" (click)=\"go(tile.redirect)\">\n <hm-icon class=\"tile__icon\" [shape]=\"tile.icon ?? 'grid-layout'\" size=\"lg\" />\n <span class=\"tile__text\">\n <span class=\"tile__label\">{{ tile.label }}</span>\n @if (caption(tile); as meta) {\n <span class=\"tile__meta\">{{ meta }}</span>\n }\n </span>\n <hm-icon class=\"tile__chevron\" shape=\"angle-right\" size=\"sm\" />\n </button>\n </li>\n }\n </ul>\n</ng-template>\n", styles: [":host{display:flex;flex:1 1 auto;flex-direction:column;min-block-size:100%;min-inline-size:0}hm-state-page{flex:1 1 auto}ul{margin:0;padding:0;list-style:none}p{margin:0}.home{container-type:inline-size;display:flex;flex-direction:column;gap:var(--hm-space-md);min-inline-size:0}.hero{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--hm-space-md)}.hero__text{display:flex;flex:1 1 20rem;flex-direction:column;gap:var(--hm-space-micro);min-inline-size:0}.hero__news{flex:0 1 32rem;min-inline-size:min(100%,20rem)}.hero__greeting{color:var(--hm-text);font-size:var(--hm-font-size-xxl);font-weight:var(--hm-font-weight-bold);line-height:var(--hm-line-height-tight)}.hero__intro{color:var(--hm-muted);font-size:var(--hm-font-size-md)}.block{display:flex;flex-direction:column;gap:var(--hm-space-sm);min-inline-size:0}.block__title{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-semibold)}.news,.row,.system,.recent__open,.more,.tile{display:flex;align-items:center;gap:var(--hm-space-sm);border:var(--hm-border-hairline) solid transparent;border-radius:var(--hm-radius-control);background:transparent;color:var(--hm-text);font:inherit;text-align:start;cursor:pointer;transition:background-color var(--hm-duration-quick) var(--hm-ease-standard),border-color var(--hm-duration-quick) var(--hm-ease-standard)}.news:hover,.row:hover,.system:hover,.recent__open:hover,.more:hover,.tile:hover{background:var(--hm-hover)}.news:focus-visible,.row:focus-visible,.system:focus-visible,.recent__open:focus-visible,.more:focus-visible,.tile:focus-visible{outline:var(--hm-border-thin) solid var(--hm-accent);outline-offset:2px}@media(forced-colors:active){.news,.row,.system,.recent__open,.more,.tile{border-color:ButtonText}.news:focus-visible,.row:focus-visible,.system:focus-visible,.recent__open:focus-visible,.more:focus-visible,.tile:focus-visible{outline-color:Highlight}}.shortcuts{display:flex;flex-wrap:wrap;gap:var(--hm-space-md)}.shortcuts__group{flex:var(--tiles, 1) 1 calc(var(--tiles, 1) * 13rem + (var(--tiles, 1) - 1) * var(--hm-space-sm))}.tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,13rem),1fr));gap:var(--hm-space-sm)}.tiles__cell{display:flex}.tile{flex:1 1 auto;gap:var(--hm-space-md);padding:var(--hm-space-md);border-color:var(--hm-divider);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.tile:hover{border-color:var(--hm-accent);background:var(--hm-surface-raised)}.tile__icon{flex:none;color:var(--hm-accent)}.tiles--admin .tile__icon{color:var(--hm-warning)}.tile__text{display:flex;flex:1 1 auto;flex-direction:column;gap:var(--hm-space-nano);min-inline-size:0}.tile__label{font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-semibold);overflow-wrap:anywhere}.tile__meta{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.tile__chevron,.row__chevron{flex:none;color:var(--hm-muted)}@media(prefers-reduced-motion:no-preference){.tiles__cell{animation:tile-in var(--hm-duration-standard) var(--hm-ease-decelerate) both;animation-delay:calc(min(var(--tile-order, 0),10)*35ms)}}:host-context([data-motion=full]) .tiles__cell{animation:tile-in var(--hm-duration-standard) var(--hm-ease-decelerate) both;animation-delay:calc(min(var(--tile-order, 0),10)*35ms)}:host-context([data-motion=reduce]) .tiles__cell{animation:none}@keyframes tile-in{0%{opacity:0;transform:translateY(var(--hm-space-xs))}}.pair{display:grid;grid-template-columns:minmax(0,2fr) minmax(0,1fr);gap:var(--hm-space-md);align-items:stretch}.pair>:only-child{grid-column:1/-1}.stack{display:flex;flex-direction:column;gap:var(--hm-space-md);min-inline-size:0}.stack>.card:last-child{flex:1 1 auto}@container (max-width: 56rem){.pair{grid-template-columns:minmax(0,1fr)}}.card{display:flex;flex-direction:column;gap:var(--hm-space-sm);min-inline-size:0;padding:var(--hm-space-md);border:var(--hm-border-hairline) solid var(--hm-divider);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.card__head{display:flex;align-items:center;justify-content:space-between;gap:var(--hm-space-sm)}.card__head--stacked{flex-direction:column;align-items:flex-start;gap:var(--hm-space-micro)}.card__heading{display:flex;align-items:center;gap:var(--hm-space-xs)}.card__title{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-semibold)}.card__intro,.card__empty{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.more{flex:none;padding:var(--hm-space-nano) var(--hm-space-xs);color:var(--hm-accent);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-medium)}.badge{flex:none;padding:var(--hm-space-nano) var(--hm-space-xs);border-radius:var(--hm-radius-micro);background:var(--hm-accent-subtle);color:var(--hm-accent);font-size:var(--hm-font-size-3xs);font-weight:var(--hm-font-weight-semibold);letter-spacing:.04em;text-transform:uppercase}.badge--neutral{background:var(--hm-surface-muted);color:var(--hm-muted);letter-spacing:normal;text-transform:none}.card--compact{gap:var(--hm-space-micro);padding:var(--hm-space-sm) var(--hm-space-md)}.recent{display:flex;align-items:center;gap:var(--hm-space-micro)}.recent__open{flex:1 1 auto;min-inline-size:0;padding:var(--hm-space-nano) var(--hm-space-xs)}.recent__icon{flex:none;color:var(--hm-accent)}.recent__name{flex:none;max-inline-size:50%;overflow:hidden;font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold);text-overflow:ellipsis;white-space:nowrap}.recent__where{flex:1 1 auto;min-inline-size:0;overflow:hidden;color:var(--hm-muted);font-size:var(--hm-font-size-xs);text-overflow:ellipsis;white-space:nowrap}.recent__when{flex:none;color:var(--hm-muted);font-size:var(--hm-font-size-xs);white-space:nowrap}.heart--on{color:var(--hm-danger)}.systems{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,14rem),1fr));gap:var(--hm-space-sm)}.systems__cell{display:flex}.system{flex:1 1 auto;flex-direction:column;align-items:stretch;gap:var(--hm-space-xs);padding:var(--hm-space-sm) var(--hm-space-md);border-color:var(--hm-divider);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.system:hover{border-color:var(--hm-accent);background:var(--hm-surface-raised)}.system__head{display:flex;align-items:center;gap:var(--hm-space-sm);min-inline-size:0}.system__icon{flex:none;color:var(--hm-accent)}.system__name{min-inline-size:0;overflow:hidden;font-weight:var(--hm-font-weight-semibold);text-overflow:ellipsis;white-space:nowrap}.system__chevron{flex:none;margin-inline-start:auto;color:var(--hm-muted)}.system__description{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.system__chips{display:flex;flex-wrap:wrap;gap:var(--hm-space-micro)}.system__chip{padding:var(--hm-space-nano) var(--hm-space-xs);border:var(--hm-border-hairline) solid var(--hm-divider);border-radius:var(--hm-radius-pill);background:var(--hm-surface-muted);color:var(--hm-text);font-size:var(--hm-font-size-xs)}.list{display:flex;flex-direction:column}.list>li+li{border-block-start:var(--hm-border-hairline) solid var(--hm-divider)}.row{inline-size:100%;padding:var(--hm-space-nano) var(--hm-space-xs)}.row__icon{flex:none;color:var(--hm-muted)}.row__name{flex:1 1 auto;min-inline-size:0;overflow:hidden;font-size:var(--hm-font-size-sm);text-overflow:ellipsis;white-space:nowrap}.row__meta{flex:0 1 auto;min-inline-size:0;max-inline-size:45%;overflow:hidden;color:var(--hm-muted);font-size:var(--hm-font-size-xs);text-overflow:ellipsis;white-space:nowrap}.news{inline-size:100%;gap:var(--hm-space-xs);padding:var(--hm-space-nano) var(--hm-space-xs)}.news__dot{flex:none;inline-size:var(--hm-space-xs);block-size:var(--hm-space-xs);border-radius:var(--hm-radius-pill);background:var(--hm-accent)}.news__dot--seen{background:var(--hm-divider)}.news__title{flex:none;max-inline-size:50%;overflow:hidden;font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold);text-overflow:ellipsis;white-space:nowrap}.news__body{flex:1 1 auto;min-inline-size:0;overflow:hidden;color:var(--hm-muted);font-size:var(--hm-font-size-xs);text-overflow:ellipsis;white-space:nowrap}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { 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: MenuButtonComponent, selector: "hm-menu-button", inputs: ["label", "icon", "items", "showArrow", "align", "side", "size", "shape", "layer", "closeOnCheck", "disabled"], outputs: ["chosen", "checkedChange"] }, { kind: "component", type: StatePageComponent, selector: "hm-state-page", inputs: ["kind", "heading", "description", "action", "actionIcon"], outputs: ["actionTriggered"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
308
311
  }
309
312
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: HomeComponent, decorators: [{
310
313
  type: Component,
311
- args: [{ selector: 'home', imports: [NgTemplateOutlet, IconButtonComponent, IconComponent, MenuButtonComponent, StatePageComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (noAccess()) {\n <hm-state-page kind=\"no-rights\" [heading]=\"t.noAccessTitle\" [description]=\"t.noAccessText\" action=\"\" />\n} @else {\n <div class=\"home\">\n <!-- Sauda\u00E7\u00E3o \u00E0 esquerda; as novidades, quando houver, ocupam o espa\u00E7o \u00E0 direita dela. -->\n <div class=\"hero\">\n <div class=\"hero__text\">\n <p class=\"hero__greeting\">{{ greeting }}</p>\n <p class=\"hero__intro\">{{ t.intro }}</p>\n </div>\n @if (newsRows().length) {\n <!-- Compacto: cabe\u00E7alho numa linha e cada novidade numa linha s\u00F3 (t\u00EDtulo e o come\u00E7o do texto). -->\n <section class=\"card card--compact hero__news\" [attr.aria-labelledby]=\"uid + '-news'\">\n <div class=\"card__head\">\n <span class=\"card__heading\">\n <h2 class=\"card__title\" [id]=\"uid + '-news'\">{{ t.news }}</h2>\n <span class=\"badge\">{{ t.newsBadge }}</span>\n </span>\n <button type=\"button\" class=\"more\" (click)=\"openNews()\">{{ t.newsAll }}</button>\n </div>\n <ul class=\"list\">\n @for (entry of newsRows(); track entry.id) {\n <li>\n <button type=\"button\" class=\"news\" [title]=\"entry.title + ' \u2014 ' + entry.text\" (click)=\"openNews(entry.id)\">\n <span class=\"news__dot\" [class.news__dot--seen]=\"!entry.unseen\" aria-hidden=\"true\"></span>\n <span class=\"news__title\">{{ entry.title }}</span>\n <span class=\"news__body\">{{ entry.text }}</span>\n </button>\n </li>\n }\n </ul>\n </section>\n }\n </div>\n\n <!-- Atalhos de trabalho e privilegiados dividem a mesma linha (cada grupo na propor\u00E7\u00E3o dos seus atalhos). -->\n @if (workTiles().length || adminTiles().length) {\n <div class=\"shortcuts\">\n @if (workTiles().length) {\n <section class=\"block shortcuts__group\" [style.--tiles]=\"workTiles().length\" [attr.aria-labelledby]=\"uid + '-work'\">\n <h2 class=\"block__title\" [id]=\"uid + '-work'\">{{ descended() ? t.yourScreens : t.modules }}</h2>\n <ng-container *ngTemplateOutlet=\"tileGrid; context: { $implicit: workTiles(), admin: false }\" />\n </section>\n }\n @if (adminTiles().length) {\n <section class=\"block shortcuts__group\" [style.--tiles]=\"adminTiles().length\" [attr.aria-labelledby]=\"uid + '-admin'\">\n <h2 class=\"block__title\" [id]=\"uid + '-admin'\">{{ t.privileged }}</h2>\n <ng-container *ngTemplateOutlet=\"tileGrid; context: { $implicit: adminTiles(), admin: true }\" />\n </section>\n }\n </div>\n }\n\n <!-- Linha 1: o que a pessoa usa (recentes | favoritos). Linha 2: o resto do sistema (m\u00F3dulos | ajuda).\n Sem um dos lados, o outro ocupa a linha inteira. -->\n <div class=\"pair\">\n @if (recentRows().length) {\n <section class=\"card\" [attr.aria-labelledby]=\"uid + '-recent'\">\n <div class=\"card__head\">\n <h2 class=\"card__title\" [id]=\"uid + '-recent'\">{{ t.recent }}</h2>\n @if (moreRecents()) {\n <button type=\"button\" class=\"more\" [attr.aria-expanded]=\"showAllRecents()\" (click)=\"showAllRecents.set(!showAllRecents())\">\n {{ showAllRecents() ? t.seeLess : t.seeAll }}\n </button>\n }\n </div>\n <ul class=\"list\" [attr.aria-label]=\"t.recentCaption\">\n @for (item of recentRows(); track item.id) {\n <li class=\"recent\">\n <button type=\"button\" class=\"recent__open\" (click)=\"go(item.redirect)\">\n <hm-icon class=\"recent__icon\" [shape]=\"item.icon || 'grid-layout'\" />\n <span class=\"recent__name\">{{ item.displayName }}</span>\n <span class=\"recent__where\">{{ item.where }}</span>\n @if (item.when) {\n <span class=\"recent__when\">{{ item.when }}</span>\n }\n </button>\n <hm-icon-button\n emphasis=\"subtle\"\n size=\"small\"\n icon=\"heart\"\n [solid]=\"isFavorite(item.id)\"\n [class.heart--on]=\"isFavorite(item.id)\"\n [label]=\"favoriteLabel(item)\"\n (clicked)=\"toggleFavorite(item)\"\n />\n <hm-menu-button\n class=\"recent__menu\"\n icon=\"ellipsis-vertical\"\n shape=\"circle\"\n size=\"small\"\n align=\"end\"\n [showArrow]=\"false\"\n [label]=\"recentMenuLabel(item)\"\n [items]=\"recentMenu\"\n (chosen)=\"onRecentMenu(item, $event)\"\n />\n </li>\n }\n </ul>\n </section>\n }\n\n <section class=\"card\" [attr.aria-labelledby]=\"uid + '-fav'\">\n <div class=\"card__head\">\n <h2 class=\"card__title\" [id]=\"uid + '-fav'\">{{ t.favorites }}</h2>\n @if (moreFavorites()) {\n <button type=\"button\" class=\"more\" [attr.aria-expanded]=\"showAllFavorites()\" (click)=\"showAllFavorites.set(!showAllFavorites())\">\n {{ showAllFavorites() ? t.seeLess : t.seeAll }}\n </button>\n }\n </div>\n @if (visibleFavorites().length) {\n <ul class=\"list\">\n @for (fav of visibleFavorites(); track fav.id) {\n <li>\n <button type=\"button\" class=\"row\" (click)=\"go(fav.redirect)\">\n <hm-icon class=\"row__icon\" [shape]=\"fav.icon || 'grid-layout'\" />\n <span class=\"row__name\">{{ fav.displayName }}</span>\n <span class=\"row__meta\">{{ whereOf(fav) }}</span>\n <hm-icon class=\"row__chevron\" shape=\"angle-right\" size=\"sm\" />\n </button>\n </li>\n }\n </ul>\n } @else {\n <p class=\"card__empty\">{{ t.favoritesEmpty }}</p>\n }\n </section>\n </div>\n\n <div class=\"pair\">\n @if (systems().length) {\n <section class=\"card\" [attr.aria-labelledby]=\"uid + '-systems'\">\n <div class=\"card__head card__head--stacked\">\n <h2 class=\"card__title\" [id]=\"uid + '-systems'\">{{ t.explore }}</h2>\n <p class=\"card__intro\">{{ t.exploreIntro }}</p>\n </div>\n <ul class=\"systems\">\n @for (system of systems(); track system.id) {\n <!-- O card inteiro abre o m\u00F3dulo (como os atalhos): uma seta no canto, sem link pr\u00F3prio. -->\n <li class=\"systems__cell\">\n <button type=\"button\" class=\"system\" (click)=\"go(system.redirect)\">\n <span class=\"system__head\">\n <hm-icon class=\"system__icon\" [shape]=\"system.icon\" size=\"lg\" />\n <span class=\"system__name\" [title]=\"system.label\">{{ system.label }}</span>\n <span class=\"badge badge--neutral\">{{ system.count }}</span>\n <hm-icon class=\"system__chevron\" shape=\"angle-right\" size=\"sm\" />\n </span>\n @if (system.description) {\n <span class=\"system__description\">{{ system.description }}</span>\n }\n @if (system.chips.length) {\n <span class=\"system__chips\">\n @for (chip of system.chips; track chip) {\n <span class=\"system__chip\">{{ chip }}</span>\n }\n </span>\n }\n </button>\n </li>\n }\n </ul>\n </section>\n }\n\n <section class=\"card\" [attr.aria-labelledby]=\"uid + '-help'\">\n <div class=\"card__head\">\n <h2 class=\"card__title\" [id]=\"uid + '-help'\">{{ t.help }}</h2>\n </div>\n <ul class=\"list\">\n @for (area of areas(); track area.routed_id) {\n <li>\n <button type=\"button\" class=\"row\" (click)=\"go(area.route_redirect)\">\n <hm-icon class=\"row__icon\" [shape]=\"area.route_icon || 'home'\" />\n <span class=\"row__name\">{{ area.route_display_name }}</span>\n <hm-icon class=\"row__chevron\" shape=\"angle-right\" size=\"sm\" />\n </button>\n </li>\n }\n <li>\n <button type=\"button\" class=\"row\" (click)=\"openDocs()\">\n <hm-icon class=\"row__icon\" shape=\"book\" />\n <span class=\"row__name\">{{ t.systemDocs }}</span>\n <hm-icon class=\"row__chevron\" shape=\"angle-right\" size=\"sm\" />\n </button>\n </li>\n <li>\n <button type=\"button\" class=\"row\" (click)=\"openShortcuts()\">\n <hm-icon class=\"row__icon\" shape=\"keyboard\" />\n <span class=\"row__name\">{{ t.shortcuts }}</span>\n <hm-icon class=\"row__chevron\" shape=\"angle-right\" size=\"sm\" />\n </button>\n </li>\n <li>\n <button type=\"button\" class=\"row\" (click)=\"openEssentials()\">\n <hm-icon class=\"row__icon\" shape=\"help\" />\n <span class=\"row__name\">{{ t.essentials }}</span>\n <hm-icon class=\"row__chevron\" shape=\"angle-right\" size=\"sm\" />\n </button>\n </li>\n <li>\n <button type=\"button\" class=\"row\" (click)=\"openSettings()\">\n <hm-icon class=\"row__icon\" shape=\"cog\" />\n <span class=\"row__name\">{{ t.settings }}</span>\n <hm-icon class=\"row__chevron\" shape=\"angle-right\" size=\"sm\" />\n </button>\n </li>\n </ul>\n </section>\n </div>\n </div>\n}\n\n<ng-template #tileGrid let-tiles let-admin=\"admin\">\n <ul class=\"tiles\" [class.tiles--admin]=\"admin\">\n @for (tile of tiles; track tile.id; let i = $index) {\n <li class=\"tiles__cell\" [style.--tile-order]=\"i\">\n <button type=\"button\" class=\"tile\" (click)=\"go(tile.redirect)\">\n <hm-icon class=\"tile__icon\" [shape]=\"tile.icon ?? 'grid-layout'\" size=\"lg\" />\n <span class=\"tile__text\">\n <span class=\"tile__label\">{{ tile.label }}</span>\n @if (caption(tile); as meta) {\n <span class=\"tile__meta\">{{ meta }}</span>\n }\n </span>\n <hm-icon class=\"tile__chevron\" shape=\"angle-right\" size=\"sm\" />\n </button>\n </li>\n }\n </ul>\n</ng-template>\n", styles: [":host{display:flex;flex:1 1 auto;flex-direction:column;min-block-size:100%;min-inline-size:0}hm-state-page{flex:1 1 auto}ul{margin:0;padding:0;list-style:none}p{margin:0}.home{container-type:inline-size;display:flex;flex-direction:column;gap:var(--hm-space-md);min-inline-size:0}.hero{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--hm-space-md)}.hero__text{display:flex;flex:1 1 20rem;flex-direction:column;gap:var(--hm-space-micro);min-inline-size:0}.hero__news{flex:0 1 32rem;min-inline-size:min(100%,20rem)}.hero__greeting{color:var(--hm-text);font-size:var(--hm-font-size-xxl);font-weight:var(--hm-font-weight-bold);line-height:var(--hm-line-height-tight)}.hero__intro{color:var(--hm-muted);font-size:var(--hm-font-size-md)}.block{display:flex;flex-direction:column;gap:var(--hm-space-sm);min-inline-size:0}.block__title{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-semibold)}.news,.row,.system,.recent__open,.more,.tile{display:flex;align-items:center;gap:var(--hm-space-sm);border:var(--hm-border-hairline) solid transparent;border-radius:var(--hm-radius-control);background:transparent;color:var(--hm-text);font:inherit;text-align:start;cursor:pointer;transition:background-color var(--hm-duration-quick) var(--hm-ease-standard),border-color var(--hm-duration-quick) var(--hm-ease-standard)}.news:hover,.row:hover,.system:hover,.recent__open:hover,.more:hover,.tile:hover{background:var(--hm-hover)}.news:focus-visible,.row:focus-visible,.system:focus-visible,.recent__open:focus-visible,.more:focus-visible,.tile:focus-visible{outline:var(--hm-border-thin) solid var(--hm-accent);outline-offset:2px}@media(forced-colors:active){.news,.row,.system,.recent__open,.more,.tile{border-color:ButtonText}.news:focus-visible,.row:focus-visible,.system:focus-visible,.recent__open:focus-visible,.more:focus-visible,.tile:focus-visible{outline-color:Highlight}}.shortcuts{display:flex;flex-wrap:wrap;gap:var(--hm-space-md)}.shortcuts__group{flex:var(--tiles, 1) 1 calc(var(--tiles, 1) * 13rem + (var(--tiles, 1) - 1) * var(--hm-space-sm))}.tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,13rem),1fr));gap:var(--hm-space-sm)}.tiles__cell{display:flex}.tile{flex:1 1 auto;gap:var(--hm-space-md);padding:var(--hm-space-md);border-color:var(--hm-divider);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.tile:hover{border-color:var(--hm-accent);background:var(--hm-surface-raised)}.tile__icon{flex:none;color:var(--hm-accent)}.tiles--admin .tile__icon{color:var(--hm-warning)}.tile__text{display:flex;flex:1 1 auto;flex-direction:column;gap:var(--hm-space-nano);min-inline-size:0}.tile__label{font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-semibold);overflow-wrap:anywhere}.tile__meta{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.tile__chevron,.row__chevron{flex:none;color:var(--hm-muted)}@media(prefers-reduced-motion:no-preference){.tiles__cell{animation:tile-in var(--hm-duration-standard) var(--hm-ease-decelerate) both;animation-delay:calc(min(var(--tile-order, 0),10)*35ms)}}:host-context([data-motion=full]) .tiles__cell{animation:tile-in var(--hm-duration-standard) var(--hm-ease-decelerate) both;animation-delay:calc(min(var(--tile-order, 0),10)*35ms)}:host-context([data-motion=reduce]) .tiles__cell{animation:none}@keyframes tile-in{0%{opacity:0;transform:translateY(var(--hm-space-xs))}}.pair{display:grid;grid-template-columns:minmax(0,2fr) minmax(0,1fr);gap:var(--hm-space-md);align-items:stretch}.pair>:only-child{grid-column:1/-1}.stack{display:flex;flex-direction:column;gap:var(--hm-space-md);min-inline-size:0}.stack>.card:last-child{flex:1 1 auto}@container (max-width: 56rem){.pair{grid-template-columns:minmax(0,1fr)}}.card{display:flex;flex-direction:column;gap:var(--hm-space-sm);min-inline-size:0;padding:var(--hm-space-md);border:var(--hm-border-hairline) solid var(--hm-divider);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.card__head{display:flex;align-items:center;justify-content:space-between;gap:var(--hm-space-sm)}.card__head--stacked{flex-direction:column;align-items:flex-start;gap:var(--hm-space-micro)}.card__heading{display:flex;align-items:center;gap:var(--hm-space-xs)}.card__title{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-semibold)}.card__intro,.card__empty{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.more{flex:none;padding:var(--hm-space-nano) var(--hm-space-xs);color:var(--hm-accent);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-medium)}.badge{flex:none;padding:var(--hm-space-nano) var(--hm-space-xs);border-radius:var(--hm-radius-micro);background:var(--hm-accent-subtle);color:var(--hm-accent);font-size:var(--hm-font-size-3xs);font-weight:var(--hm-font-weight-semibold);letter-spacing:.04em;text-transform:uppercase}.badge--neutral{background:var(--hm-surface-muted);color:var(--hm-muted);letter-spacing:normal;text-transform:none}.card--compact{gap:var(--hm-space-micro);padding:var(--hm-space-sm) var(--hm-space-md)}.recent{display:flex;align-items:center;gap:var(--hm-space-micro)}.recent__open{flex:1 1 auto;min-inline-size:0;padding:var(--hm-space-nano) var(--hm-space-xs)}.recent__icon{flex:none;color:var(--hm-accent)}.recent__name{flex:none;max-inline-size:50%;overflow:hidden;font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold);text-overflow:ellipsis;white-space:nowrap}.recent__where{flex:1 1 auto;min-inline-size:0;overflow:hidden;color:var(--hm-muted);font-size:var(--hm-font-size-xs);text-overflow:ellipsis;white-space:nowrap}.recent__when{flex:none;color:var(--hm-muted);font-size:var(--hm-font-size-xs);white-space:nowrap}.heart--on{color:var(--hm-danger)}.systems{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,14rem),1fr));gap:var(--hm-space-sm)}.systems__cell{display:flex}.system{flex:1 1 auto;flex-direction:column;align-items:stretch;gap:var(--hm-space-xs);padding:var(--hm-space-sm) var(--hm-space-md);border-color:var(--hm-divider);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.system:hover{border-color:var(--hm-accent);background:var(--hm-surface-raised)}.system__head{display:flex;align-items:center;gap:var(--hm-space-sm);min-inline-size:0}.system__icon{flex:none;color:var(--hm-accent)}.system__name{min-inline-size:0;overflow:hidden;font-weight:var(--hm-font-weight-semibold);text-overflow:ellipsis;white-space:nowrap}.system__chevron{flex:none;margin-inline-start:auto;color:var(--hm-muted)}.system__description{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.system__chips{display:flex;flex-wrap:wrap;gap:var(--hm-space-micro)}.system__chip{padding:var(--hm-space-nano) var(--hm-space-xs);border:var(--hm-border-hairline) solid var(--hm-divider);border-radius:var(--hm-radius-pill);background:var(--hm-surface-muted);color:var(--hm-text);font-size:var(--hm-font-size-xs)}.list{display:flex;flex-direction:column}.list>li+li{border-block-start:var(--hm-border-hairline) solid var(--hm-divider)}.row{inline-size:100%;padding:var(--hm-space-nano) var(--hm-space-xs)}.row__icon{flex:none;color:var(--hm-muted)}.row__name{flex:1 1 auto;min-inline-size:0;overflow:hidden;font-size:var(--hm-font-size-sm);text-overflow:ellipsis;white-space:nowrap}.row__meta{flex:0 1 auto;min-inline-size:0;max-inline-size:45%;overflow:hidden;color:var(--hm-muted);font-size:var(--hm-font-size-xs);text-overflow:ellipsis;white-space:nowrap}.news{inline-size:100%;gap:var(--hm-space-xs);padding:var(--hm-space-nano) var(--hm-space-xs)}.news__dot{flex:none;inline-size:var(--hm-space-xs);block-size:var(--hm-space-xs);border-radius:var(--hm-radius-pill);background:var(--hm-accent)}.news__dot--seen{background:var(--hm-divider)}.news__title{flex:none;max-inline-size:50%;overflow:hidden;font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold);text-overflow:ellipsis;white-space:nowrap}.news__body{flex:1 1 auto;min-inline-size:0;overflow:hidden;color:var(--hm-muted);font-size:var(--hm-font-size-xs);text-overflow:ellipsis;white-space:nowrap}\n"] }]
314
+ args: [{ selector: 'home', imports: [NgTemplateOutlet, IconButtonComponent, IconComponent, MenuButtonComponent, StatePageComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (noAccess()) {\n <hm-state-page kind=\"no-rights\" [heading]=\"t.noAccessTitle\" [description]=\"t.noAccessText\" action=\"\" />\n} @else {\n <div class=\"home\">\n <!-- Sauda\u00E7\u00E3o \u00E0 esquerda; as novidades, quando houver, ocupam o espa\u00E7o \u00E0 direita dela. -->\n <div class=\"hero\">\n <div class=\"hero__text\">\n <p class=\"hero__greeting\">{{ greeting }}</p>\n <p class=\"hero__intro\">{{ t.intro }}</p>\n </div>\n @if (newsRows().length) {\n <!-- Compacto: cabe\u00E7alho numa linha e cada novidade numa linha s\u00F3 (t\u00EDtulo e o come\u00E7o do texto). -->\n <section class=\"card card--compact hero__news\" [attr.aria-labelledby]=\"uid + '-news'\">\n <div class=\"card__head\">\n <span class=\"card__heading\">\n <h2 class=\"card__title\" [id]=\"uid + '-news'\">{{ t.news }}</h2>\n <span class=\"badge\">{{ t.newsBadge }}</span>\n </span>\n <button type=\"button\" class=\"more\" (click)=\"openNews()\">{{ t.newsAll }}</button>\n </div>\n <ul class=\"list\">\n @for (entry of newsRows(); track entry.id) {\n <li>\n <button type=\"button\" class=\"news\" [title]=\"entry.title + ' \u2014 ' + entry.text\" (click)=\"openNews(entry.id)\">\n <span class=\"news__dot\" [class.news__dot--seen]=\"!entry.unseen\" aria-hidden=\"true\"></span>\n <span class=\"news__title\">{{ entry.title }}</span>\n <span class=\"news__body\">{{ entry.text }}</span>\n </button>\n </li>\n }\n </ul>\n </section>\n }\n </div>\n\n <!-- Atalhos de trabalho e privilegiados dividem a mesma linha (cada grupo na propor\u00E7\u00E3o dos seus atalhos). -->\n @if (workTiles().length || adminTiles().length) {\n <div class=\"shortcuts\">\n @if (workTiles().length) {\n <section class=\"block shortcuts__group\" [style.--tiles]=\"workTiles().length\" [attr.aria-labelledby]=\"uid + '-work'\">\n <h2 class=\"block__title\" [id]=\"uid + '-work'\">{{ descended() ? t.yourScreens : t.modules }}</h2>\n <ng-container *ngTemplateOutlet=\"tileGrid; context: { $implicit: workTiles(), admin: false }\" />\n </section>\n }\n @if (adminTiles().length) {\n <section class=\"block shortcuts__group\" [style.--tiles]=\"adminTiles().length\" [attr.aria-labelledby]=\"uid + '-admin'\">\n <h2 class=\"block__title\" [id]=\"uid + '-admin'\">{{ t.privileged }}</h2>\n <ng-container *ngTemplateOutlet=\"tileGrid; context: { $implicit: adminTiles(), admin: true }\" />\n </section>\n }\n </div>\n }\n\n <!-- Linha 1: o que a pessoa usa (recentes | favoritos). Linha 2: o resto do sistema (m\u00F3dulos | ajuda).\n Sem um dos lados, o outro ocupa a linha inteira. -->\n <div class=\"pair\">\n @if (recentRows().length) {\n <section class=\"card\" [attr.aria-labelledby]=\"uid + '-recent'\">\n <div class=\"card__head\">\n <h2 class=\"card__title\" [id]=\"uid + '-recent'\">{{ t.recent }}</h2>\n @if (moreRecents()) {\n <button type=\"button\" class=\"more\" [attr.aria-expanded]=\"showAllRecents()\" (click)=\"showAllRecents.set(!showAllRecents())\">\n {{ showAllRecents() ? t.seeLess : t.seeAll }}\n </button>\n }\n </div>\n <ul class=\"list\" [attr.aria-label]=\"t.recentCaption\">\n @for (item of recentRows(); track item.id) {\n <li class=\"recent\">\n <button type=\"button\" class=\"recent__open\" (click)=\"go(item.redirect)\">\n <hm-icon class=\"recent__icon\" [shape]=\"item.icon || 'grid-layout'\" />\n <span class=\"recent__name\">{{ item.displayName }}</span>\n <span class=\"recent__where\">{{ item.where }}</span>\n @if (item.when) {\n <span class=\"recent__when\">{{ item.when }}</span>\n }\n </button>\n <hm-icon-button\n emphasis=\"subtle\"\n size=\"small\"\n icon=\"heart\"\n [solid]=\"isFavorite(item.id)\"\n [class.heart--on]=\"isFavorite(item.id)\"\n [label]=\"favoriteLabel(item)\"\n (clicked)=\"toggleFavorite(item)\"\n />\n <hm-menu-button\n class=\"recent__menu\"\n icon=\"ellipsis-vertical\"\n shape=\"circle\"\n size=\"small\"\n align=\"end\"\n [showArrow]=\"false\"\n [label]=\"recentMenuLabel(item)\"\n [items]=\"recentMenu\"\n (chosen)=\"onRecentMenu(item, $event)\"\n />\n </li>\n }\n </ul>\n </section>\n }\n\n <section class=\"card\" [attr.aria-labelledby]=\"uid + '-fav'\">\n <div class=\"card__head\">\n <h2 class=\"card__title\" [id]=\"uid + '-fav'\">{{ t.favorites }}</h2>\n @if (moreFavorites()) {\n <button type=\"button\" class=\"more\" [attr.aria-expanded]=\"showAllFavorites()\" (click)=\"showAllFavorites.set(!showAllFavorites())\">\n {{ showAllFavorites() ? t.seeLess : t.seeAll }}\n </button>\n }\n </div>\n @if (visibleFavorites().length) {\n <ul class=\"list\">\n @for (fav of visibleFavorites(); track fav.id) {\n <li>\n <button type=\"button\" class=\"row\" (click)=\"go(fav.redirect)\">\n <hm-icon class=\"row__icon\" [shape]=\"fav.icon || 'grid-layout'\" />\n <span class=\"row__name\">{{ fav.displayName }}</span>\n <span class=\"row__meta\">{{ whereOf(fav) }}</span>\n <hm-icon class=\"row__chevron\" shape=\"angle-right\" size=\"sm\" />\n </button>\n </li>\n }\n </ul>\n } @else {\n <p class=\"card__empty\">{{ t.favoritesEmpty }}</p>\n }\n </section>\n </div>\n\n <div class=\"pair\">\n @if (systems().length) {\n <section class=\"card\" [attr.aria-labelledby]=\"uid + '-systems'\">\n <div class=\"card__head card__head--stacked\">\n <h2 class=\"card__title\" [id]=\"uid + '-systems'\">{{ t.explore }}</h2>\n <p class=\"card__intro\">{{ t.exploreIntro }}</p>\n </div>\n <ul class=\"systems\">\n @for (system of systems(); track system.id) {\n <!-- O card inteiro abre o m\u00F3dulo (como os atalhos): uma seta no canto, sem link pr\u00F3prio. -->\n <li class=\"systems__cell\">\n <button type=\"button\" class=\"system\" (click)=\"go(system.redirect)\">\n <span class=\"system__head\">\n <hm-icon class=\"system__icon\" [shape]=\"system.icon\" size=\"lg\" />\n <span class=\"system__name\" [title]=\"system.label\">{{ system.label }}</span>\n <span class=\"badge badge--neutral\">{{ system.count }}</span>\n <hm-icon class=\"system__chevron\" shape=\"angle-right\" size=\"sm\" />\n </span>\n @if (system.description) {\n <span class=\"system__description\">{{ system.description }}</span>\n }\n @if (system.chips.length) {\n <span class=\"system__chips\">\n @for (chip of system.chips; track chip) {\n <span class=\"system__chip\">{{ chip }}</span>\n }\n </span>\n }\n </button>\n </li>\n }\n </ul>\n </section>\n }\n\n <section class=\"card\" [attr.aria-labelledby]=\"uid + '-help'\">\n <div class=\"card__head\">\n <h2 class=\"card__title\" [id]=\"uid + '-help'\">{{ t.help }}</h2>\n </div>\n <ul class=\"list\">\n @for (area of areas(); track area.routed_id) {\n <li>\n <button type=\"button\" class=\"row\" (click)=\"go(area.route_redirect)\">\n <hm-icon class=\"row__icon\" [shape]=\"area.route_icon || 'home'\" />\n <span class=\"row__name\">{{ area.route_display_name }}</span>\n <hm-icon class=\"row__chevron\" shape=\"angle-right\" size=\"sm\" />\n </button>\n </li>\n }\n <li>\n <button type=\"button\" class=\"row\" (click)=\"openDocs()\">\n <hm-icon class=\"row__icon\" shape=\"book\" />\n <span class=\"row__name\">{{ t.systemDocs }}</span>\n <hm-icon class=\"row__chevron\" shape=\"angle-right\" size=\"sm\" />\n </button>\n </li>\n @if (suggestions) {\n <li>\n <button type=\"button\" class=\"row\" (click)=\"go(suggestionsPath)\">\n <hm-icon class=\"row__icon\" shape=\"lightbulb\" />\n <span class=\"row__name\">{{ suggestions }}</span>\n <hm-icon class=\"row__chevron\" shape=\"angle-right\" size=\"sm\" />\n </button>\n </li>\n }\n <li>\n <button type=\"button\" class=\"row\" (click)=\"openShortcuts()\">\n <hm-icon class=\"row__icon\" shape=\"keyboard\" />\n <span class=\"row__name\">{{ t.shortcuts }}</span>\n <hm-icon class=\"row__chevron\" shape=\"angle-right\" size=\"sm\" />\n </button>\n </li>\n <li>\n <button type=\"button\" class=\"row\" (click)=\"openEssentials()\">\n <hm-icon class=\"row__icon\" shape=\"help\" />\n <span class=\"row__name\">{{ t.essentials }}</span>\n <hm-icon class=\"row__chevron\" shape=\"angle-right\" size=\"sm\" />\n </button>\n </li>\n <li>\n <button type=\"button\" class=\"row\" (click)=\"openSettings()\">\n <hm-icon class=\"row__icon\" shape=\"cog\" />\n <span class=\"row__name\">{{ t.settings }}</span>\n <hm-icon class=\"row__chevron\" shape=\"angle-right\" size=\"sm\" />\n </button>\n </li>\n </ul>\n </section>\n </div>\n </div>\n}\n\n<ng-template #tileGrid let-tiles let-admin=\"admin\">\n <ul class=\"tiles\" [class.tiles--admin]=\"admin\">\n @for (tile of tiles; track tile.id; let i = $index) {\n <li class=\"tiles__cell\" [style.--tile-order]=\"i\">\n <button type=\"button\" class=\"tile\" (click)=\"go(tile.redirect)\">\n <hm-icon class=\"tile__icon\" [shape]=\"tile.icon ?? 'grid-layout'\" size=\"lg\" />\n <span class=\"tile__text\">\n <span class=\"tile__label\">{{ tile.label }}</span>\n @if (caption(tile); as meta) {\n <span class=\"tile__meta\">{{ meta }}</span>\n }\n </span>\n <hm-icon class=\"tile__chevron\" shape=\"angle-right\" size=\"sm\" />\n </button>\n </li>\n }\n </ul>\n</ng-template>\n", styles: [":host{display:flex;flex:1 1 auto;flex-direction:column;min-block-size:100%;min-inline-size:0}hm-state-page{flex:1 1 auto}ul{margin:0;padding:0;list-style:none}p{margin:0}.home{container-type:inline-size;display:flex;flex-direction:column;gap:var(--hm-space-md);min-inline-size:0}.hero{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--hm-space-md)}.hero__text{display:flex;flex:1 1 20rem;flex-direction:column;gap:var(--hm-space-micro);min-inline-size:0}.hero__news{flex:0 1 32rem;min-inline-size:min(100%,20rem)}.hero__greeting{color:var(--hm-text);font-size:var(--hm-font-size-xxl);font-weight:var(--hm-font-weight-bold);line-height:var(--hm-line-height-tight)}.hero__intro{color:var(--hm-muted);font-size:var(--hm-font-size-md)}.block{display:flex;flex-direction:column;gap:var(--hm-space-sm);min-inline-size:0}.block__title{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-semibold)}.news,.row,.system,.recent__open,.more,.tile{display:flex;align-items:center;gap:var(--hm-space-sm);border:var(--hm-border-hairline) solid transparent;border-radius:var(--hm-radius-control);background:transparent;color:var(--hm-text);font:inherit;text-align:start;cursor:pointer;transition:background-color var(--hm-duration-quick) var(--hm-ease-standard),border-color var(--hm-duration-quick) var(--hm-ease-standard)}.news:hover,.row:hover,.system:hover,.recent__open:hover,.more:hover,.tile:hover{background:var(--hm-hover)}.news:focus-visible,.row:focus-visible,.system:focus-visible,.recent__open:focus-visible,.more:focus-visible,.tile:focus-visible{outline:var(--hm-border-thin) solid var(--hm-accent);outline-offset:2px}@media(forced-colors:active){.news,.row,.system,.recent__open,.more,.tile{border-color:ButtonText}.news:focus-visible,.row:focus-visible,.system:focus-visible,.recent__open:focus-visible,.more:focus-visible,.tile:focus-visible{outline-color:Highlight}}.shortcuts{display:flex;flex-wrap:wrap;gap:var(--hm-space-md)}.shortcuts__group{flex:var(--tiles, 1) 1 calc(var(--tiles, 1) * 13rem + (var(--tiles, 1) - 1) * var(--hm-space-sm))}.tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,13rem),1fr));gap:var(--hm-space-sm)}.tiles__cell{display:flex}.tile{flex:1 1 auto;gap:var(--hm-space-md);padding:var(--hm-space-md);border-color:var(--hm-divider);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.tile:hover{border-color:var(--hm-accent);background:var(--hm-surface-raised)}.tile__icon{flex:none;color:var(--hm-accent)}.tiles--admin .tile__icon{color:var(--hm-warning)}.tile__text{display:flex;flex:1 1 auto;flex-direction:column;gap:var(--hm-space-nano);min-inline-size:0}.tile__label{font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-semibold);overflow-wrap:anywhere}.tile__meta{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.tile__chevron,.row__chevron{flex:none;color:var(--hm-muted)}@media(prefers-reduced-motion:no-preference){.tiles__cell{animation:tile-in var(--hm-duration-standard) var(--hm-ease-decelerate) both;animation-delay:calc(min(var(--tile-order, 0),10)*35ms)}}:host-context([data-motion=full]) .tiles__cell{animation:tile-in var(--hm-duration-standard) var(--hm-ease-decelerate) both;animation-delay:calc(min(var(--tile-order, 0),10)*35ms)}:host-context([data-motion=reduce]) .tiles__cell{animation:none}@keyframes tile-in{0%{opacity:0;transform:translateY(var(--hm-space-xs))}}.pair{display:grid;grid-template-columns:minmax(0,2fr) minmax(0,1fr);gap:var(--hm-space-md);align-items:stretch}.pair>:only-child{grid-column:1/-1}.stack{display:flex;flex-direction:column;gap:var(--hm-space-md);min-inline-size:0}.stack>.card:last-child{flex:1 1 auto}@container (max-width: 56rem){.pair{grid-template-columns:minmax(0,1fr)}}.card{display:flex;flex-direction:column;gap:var(--hm-space-sm);min-inline-size:0;padding:var(--hm-space-md);border:var(--hm-border-hairline) solid var(--hm-divider);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.card__head{display:flex;align-items:center;justify-content:space-between;gap:var(--hm-space-sm)}.card__head--stacked{flex-direction:column;align-items:flex-start;gap:var(--hm-space-micro)}.card__heading{display:flex;align-items:center;gap:var(--hm-space-xs)}.card__title{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-semibold)}.card__intro,.card__empty{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.more{flex:none;padding:var(--hm-space-nano) var(--hm-space-xs);color:var(--hm-accent);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-medium)}.badge{flex:none;padding:var(--hm-space-nano) var(--hm-space-xs);border-radius:var(--hm-radius-micro);background:var(--hm-accent-subtle);color:var(--hm-accent);font-size:var(--hm-font-size-3xs);font-weight:var(--hm-font-weight-semibold);letter-spacing:.04em;text-transform:uppercase}.badge--neutral{background:var(--hm-surface-muted);color:var(--hm-muted);letter-spacing:normal;text-transform:none}.card--compact{gap:var(--hm-space-micro);padding:var(--hm-space-sm) var(--hm-space-md)}.recent{display:flex;align-items:center;gap:var(--hm-space-micro)}.recent__open{flex:1 1 auto;min-inline-size:0;padding:var(--hm-space-nano) var(--hm-space-xs)}.recent__icon{flex:none;color:var(--hm-accent)}.recent__name{flex:none;max-inline-size:50%;overflow:hidden;font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold);text-overflow:ellipsis;white-space:nowrap}.recent__where{flex:1 1 auto;min-inline-size:0;overflow:hidden;color:var(--hm-muted);font-size:var(--hm-font-size-xs);text-overflow:ellipsis;white-space:nowrap}.recent__when{flex:none;color:var(--hm-muted);font-size:var(--hm-font-size-xs);white-space:nowrap}.heart--on{color:var(--hm-danger)}.systems{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,14rem),1fr));gap:var(--hm-space-sm)}.systems__cell{display:flex}.system{flex:1 1 auto;flex-direction:column;align-items:stretch;gap:var(--hm-space-xs);padding:var(--hm-space-sm) var(--hm-space-md);border-color:var(--hm-divider);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.system:hover{border-color:var(--hm-accent);background:var(--hm-surface-raised)}.system__head{display:flex;align-items:center;gap:var(--hm-space-sm);min-inline-size:0}.system__icon{flex:none;color:var(--hm-accent)}.system__name{min-inline-size:0;overflow:hidden;font-weight:var(--hm-font-weight-semibold);text-overflow:ellipsis;white-space:nowrap}.system__chevron{flex:none;margin-inline-start:auto;color:var(--hm-muted)}.system__description{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.system__chips{display:flex;flex-wrap:wrap;gap:var(--hm-space-micro)}.system__chip{padding:var(--hm-space-nano) var(--hm-space-xs);border:var(--hm-border-hairline) solid var(--hm-divider);border-radius:var(--hm-radius-pill);background:var(--hm-surface-muted);color:var(--hm-text);font-size:var(--hm-font-size-xs)}.list{display:flex;flex-direction:column}.list>li+li{border-block-start:var(--hm-border-hairline) solid var(--hm-divider)}.row{inline-size:100%;padding:var(--hm-space-nano) var(--hm-space-xs)}.row__icon{flex:none;color:var(--hm-muted)}.row__name{flex:1 1 auto;min-inline-size:0;overflow:hidden;font-size:var(--hm-font-size-sm);text-overflow:ellipsis;white-space:nowrap}.row__meta{flex:0 1 auto;min-inline-size:0;max-inline-size:45%;overflow:hidden;color:var(--hm-muted);font-size:var(--hm-font-size-xs);text-overflow:ellipsis;white-space:nowrap}.news{inline-size:100%;gap:var(--hm-space-xs);padding:var(--hm-space-nano) var(--hm-space-xs)}.news__dot{flex:none;inline-size:var(--hm-space-xs);block-size:var(--hm-space-xs);border-radius:var(--hm-radius-pill);background:var(--hm-accent)}.news__dot--seen{background:var(--hm-divider)}.news__title{flex:none;max-inline-size:50%;overflow:hidden;font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold);text-overflow:ellipsis;white-space:nowrap}.news__body{flex:1 1 auto;min-inline-size:0;overflow:hidden;color:var(--hm-muted);font-size:var(--hm-font-size-xs);text-overflow:ellipsis;white-space:nowrap}\n"] }]
312
315
  }], ctorParameters: () => [] });
313
316
 
314
317
  export { HomeAutoRedirect, HomeComponent };
315
- //# sourceMappingURL=trismegisto-spine-auto-service-home.component-rQ3dIGIE.mjs.map
318
+ //# sourceMappingURL=trismegisto-spine-auto-service-home.component-9WvXOH3l.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trismegisto-spine-auto-service-home.component-9WvXOH3l.mjs","sources":["../../../projects/ui/auto-service/src/home/home-shortcuts.ts","../../../projects/ui/auto-service/src/home/home.component.ts","../../../projects/ui/auto-service/src/home/home.component.html"],"sourcesContent":["import { Route } from '@trismegisto/spine';\nimport {\n AUTO_SERVICE_APPS_TEMPLATE_ROUTER_ID,\n AUTO_SERVICE_AUDIT_TEMPLATE_ROUTER_ID,\n AUTO_SERVICE_CLIENT_TEMPLATE_ROUTER_ID,\n AUTO_SERVICE_GROUPS_TEMPLATE_ROUTER_ID,\n AUTO_SERVICE_HOME_TEMPLATE_ROUTER_ID,\n AUTO_SERVICE_LOGIN_ATTEMPTS_TEMPLATE_ROUTER_ID,\n AUTO_SERVICE_NEWS_TEMPLATE_ROUTER_ID,\n AUTO_SERVICE_RATINGS_TEMPLATE_ROUTER_ID,\n AUTO_SERVICE_SUGGESTIONS_TEMPLATE_ROUTER_ID,\n} from '../auto-service.tokens';\n\n/** A page the person can open, with the labels of the menus above it. */\nexport interface HomeDestination {\n readonly id: string;\n readonly label: string;\n readonly icon: string | null;\n readonly redirect: string;\n readonly trail: readonly string[];\n}\n\n/** A large entry tile on the home screen. */\nexport interface HomeShortcut extends HomeDestination {\n readonly screens: number;\n readonly privileged: boolean;\n}\n\n// Above this many tiles a grid stops being scannable at a glance.\nconst MAX_TILES = 12;\n\nconst ADMIN_TEMPLATES = new Set(\n [\n AUTO_SERVICE_GROUPS_TEMPLATE_ROUTER_ID,\n AUTO_SERVICE_APPS_TEMPLATE_ROUTER_ID,\n AUTO_SERVICE_CLIENT_TEMPLATE_ROUTER_ID,\n AUTO_SERVICE_AUDIT_TEMPLATE_ROUTER_ID,\n AUTO_SERVICE_LOGIN_ATTEMPTS_TEMPLATE_ROUTER_ID,\n // Novidades, Sugestões e Avaliações também são de quem administra o client (migration 0036 do argos).\n AUTO_SERVICE_NEWS_TEMPLATE_ROUTER_ID,\n AUTO_SERVICE_SUGGESTIONS_TEMPLATE_ROUTER_ID,\n AUTO_SERVICE_RATINGS_TEMPLATE_ROUTER_ID,\n ].map((id) => id.toLowerCase()),\n);\nconst HOME_TEMPLATE = AUTO_SERVICE_HOME_TEMPLATE_ROUTER_ID.toLowerCase();\n\nfunction childrenOf(node: Route): Route[] {\n return (node.routes ?? []).filter((child): child is Route => child != null);\n}\n\nfunction labelOf(node: Route): string {\n return (node.route_display_name ?? node.route_name ?? '').trim();\n}\n\nfunction identityOf(node: Route): string {\n return node.route_redirect || `${node.route_type ?? 'node'}:${node.routed_id}`;\n}\n\nfunction templateOf(node: Route): string {\n return (node.source_template_router_id ?? '').toLowerCase();\n}\n\nexport function isPage(node: Route): boolean {\n return !!node.route_redirect && (node.route_type === 'page' || childrenOf(node).length === 0);\n}\n\nexport function isHome(node: Route): boolean {\n return templateOf(node) === HOME_TEMPLATE || (isPage(node) && node.route_path === 'home');\n}\n\nexport function modules(tree: readonly Route[]): readonly Route[] {\n return tree.filter((node) => node != null && !isHome(node));\n}\n\nexport function systemAreas(tree: readonly Route[]): readonly Route[] {\n return tree.filter((node) => node != null && isHome(node));\n}\n\nexport function destinations(tree: readonly Route[], trail: readonly string[] = []): readonly HomeDestination[] {\n const found: HomeDestination[] = [];\n const walk = (nodes: readonly Route[], path: readonly string[]) => {\n for (const node of nodes) {\n if (node == null) continue;\n if (isPage(node)) {\n found.push({ id: identityOf(node), label: labelOf(node), icon: node.route_icon ?? null, redirect: node.route_redirect!, trail: path });\n }\n const kids = childrenOf(node);\n if (kids.length) walk(kids, [...path, labelOf(node)]);\n }\n };\n walk(tree, trail);\n return found;\n}\n\nexport function trailFor(tree: readonly Route[], redirect: string): readonly string[] | null {\n return destinations(tree).find((d) => d.redirect === redirect)?.trail ?? null;\n}\n\nfunction isPrivileged(node: Route): boolean {\n return ADMIN_TEMPLATES.has(templateOf(node)) || childrenOf(node).some(isPrivileged);\n}\n\ninterface Candidate {\n readonly node: Route;\n readonly trail: readonly string[];\n readonly privileged: boolean;\n}\n\nfunction navigableChildren(node: Route): Route[] {\n return childrenOf(node).filter((child) => isPage(child) || destinations([child]).length > 0);\n}\n\n/** Replaces each candidate by its navigable children; `null` when nobody has any. */\nfunction descend(level: readonly Candidate[]): Candidate[] | null {\n let changed = false;\n const next = level.flatMap((item) => {\n const kids = navigableChildren(item.node);\n if (!kids.length) return [item];\n changed = true;\n const trail = [...item.trail, labelOf(item.node)];\n return kids.map((node) => ({ node, trail, privileged: item.privileged }));\n });\n return changed ? next : null;\n}\n\nfunction toShortcut({ node, trail, privileged }: Candidate): HomeShortcut {\n const below = destinations([node]);\n return {\n id: identityOf(node),\n label: labelOf(node),\n icon: node.route_icon ?? null,\n redirect: node.route_redirect || below[0]?.redirect || '',\n trail,\n screens: below.length - (isPage(node) ? 1 : 0),\n privileged,\n };\n}\n\n/**\n * Picks the shallowest level of the tree where there is a real choice to make: a single item is\n * skipped over, a pair is opened up while the result still fits the grid, three or more stay put.\n */\nexport function entryShortcuts(tree: readonly Route[]): readonly HomeShortcut[] {\n let level: readonly Candidate[] = modules(tree).map((node) => ({ node, trail: [], privileged: isPrivileged(node) }));\n while (level.length > 0 && level.length < 3) {\n const next = descend(level);\n if (!next) break;\n if (level.length === 2 && next.length > MAX_TILES) break;\n level = next;\n }\n return level.map(toShortcut);\n}\n","import { ChangeDetectionStrategy, Component, Injectable, Injector, computed, effect, inject, signal } from '@angular/core';\nimport { NgTemplateOutlet } from '@angular/common';\nimport { toSignal } from '@angular/core/rxjs-interop';\nimport { Router } from '@angular/router';\nimport { AuthorizationService, ViewService } from '@orfeu/angular/core';\nimport { IconButtonComponent, IconComponent, MenuButtonComponent, MenuItem, StatePageComponent } from '@trismegisto/angular';\nimport { formatMessage, pluralMessage } from '@trismegisto/primitives';\nimport { QuickAccessItem, Route, SPINE_MESSAGES, SpineQuickAccessService, spineRoutesOf } from '@trismegisto/spine';\nimport {\n SPINE_SUGGESTIONS_PAGE_PATH,\n ShortcutService,\n SpineDocsService,\n SpineInitialSetupService,\n SpineNewsService,\n SpineSettingsService,\n spineSuggestionsEnabled,\n} from '@trismegisto/spine/layout';\nimport { map } from 'rxjs';\nimport { HomeShortcut, destinations, entryShortcuts, modules, systemAreas, trailFor } from './home-shortcuts';\n\n/** Remembers, for the whole app load, that the home screen already jumped to the only screen. */\n@Injectable({ providedIn: 'root' })\nexport class HomeAutoRedirect {\n done = false;\n}\n\ninterface RecentRow extends QuickAccessItem {\n readonly where: string;\n readonly when: string;\n}\n\n/** A top-level module in \"Explore systems\": what is inside it and where it starts. */\ninterface SystemCard {\n readonly id: string;\n readonly label: string;\n readonly icon: string;\n /** Descrição do nó no menu (vazia = sem linha). */\n readonly description: string;\n /** \"13 telas\", na etiqueta ao lado do nome. */\n readonly count: string;\n readonly chips: readonly string[];\n readonly redirect: string;\n}\n\nconst TRAIL_SEPARATOR = ' › ';\n// Recentes e favoritos lado a lado mostram o mesmo número de linhas; o resto fica no \"Ver tudo\".\nconst RECENT_ROWS = 5;\nconst FAVORITE_ROWS = 5;\nconst NEWS_ROWS = 2;\nconst SYSTEM_CHIPS = 3;\nconst MINUTE = 60_000;\nconst HOUR = 60 * MINUTE;\nconst DAY = 24 * HOUR;\n\nlet instances = 0;\n\nfunction cleanTree(view: Parameters<typeof spineRoutesOf>[0]): Route[] {\n return spineRoutesOf(view).filter((node): node is Route => node != null);\n}\n\nfunction pathOf(redirect: string | null | undefined): string {\n return (redirect ?? '').split(/[?#]/)[0];\n}\n\nfunction labelOf(node: Route): string {\n return (node.route_display_name ?? node.route_name ?? '').trim();\n}\n\n@Component({\n selector: 'home',\n imports: [NgTemplateOutlet, IconButtonComponent, IconComponent, MenuButtonComponent, StatePageComponent],\n templateUrl: './home.component.html',\n styleUrl: './home.component.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class HomeComponent {\n private readonly router = inject(Router);\n private readonly injector = inject(Injector);\n private readonly quickAccess = inject(SpineQuickAccessService);\n private readonly docs = inject(SpineDocsService);\n private readonly settings = inject(SpineSettingsService);\n private readonly news = inject(SpineNewsService);\n private readonly autoRedirect = inject(HomeAutoRedirect);\n private readonly messages = inject(SPINE_MESSAGES);\n protected readonly t = this.messages.home;\n /** \"Sugestões do app\" em Ajuda e recursos, só com o recurso ligado (o mesmo nome da página). */\n protected readonly suggestions = spineSuggestionsEnabled() ? this.messages.suggestions.page.title : null;\n protected readonly suggestionsPath = SPINE_SUGGESTIONS_PAGE_PATH;\n protected readonly uid = `home-${++instances}`;\n\n private readonly view = inject(ViewService);\n private readonly tree = toSignal(this.view.currentView$.pipe(map(cleanTree)), {\n initialValue: cleanTree(this.view.currentView),\n });\n\n /** \"Bom dia, Lucas\": the first name from the session, the salutation from the hour the page was opened. */\n protected readonly greeting = this.greet(inject(AuthorizationService, { optional: true })?.profile?.name);\n\n private readonly workDestinations = computed(() => destinations(modules(this.tree())));\n protected readonly noAccess = computed(() => this.tree().length > 0 && modules(this.tree()).length === 0);\n protected readonly areas = computed(() => systemAreas(this.tree()));\n\n private readonly shortcuts = computed(() => entryShortcuts(this.tree()));\n protected readonly workTiles = computed(() => this.shortcuts().filter((s) => !s.privileged));\n protected readonly adminTiles = computed(() => this.shortcuts().filter((s) => s.privileged));\n protected readonly descended = computed(() => this.shortcuts().some((s) => s.trail.length > 0));\n\n /**\n * Screens the current badge can open (every page in the tree, the start pages included). Recents and favorites\n * are filtered by it: switching badges hides what the new one cannot reach, without forgetting it.\n */\n private readonly reachable = computed(() => {\n const tree = this.tree();\n return new Set([...destinations(tree).map((d) => pathOf(d.redirect)), ...systemAreas(tree).map((a) => pathOf(a.route_redirect))]);\n });\n private readonly canOpen = (item: QuickAccessItem) => this.reachable().has(pathOf(item.redirect));\n\n /** Top-level modules with something inside: their first sub-items as chips and the first screen to open. */\n protected readonly systems = computed<readonly SystemCard[]>(() =>\n modules(this.tree()).flatMap((node) => {\n const children = (node.routes ?? []).filter((child): child is Route => child != null);\n const screens = destinations([node]);\n if (children.length < 2 || screens.length === 0) return [];\n return [\n {\n id: node.routed_id,\n label: labelOf(node),\n icon: node.route_icon || 'grid-layout',\n description: (node.description ?? '').trim(),\n count: pluralMessage(this.t.screenCount, screens.length, this.messages.meta.locale),\n chips: children.slice(0, SYSTEM_CHIPS).map(labelOf).filter(Boolean),\n redirect: node.route_redirect || screens[0].redirect,\n },\n ];\n }),\n );\n\n protected readonly showAllRecents = signal(false);\n protected readonly showAllFavorites = signal(false);\n\n protected readonly favorites = computed(() => this.quickAccess.favorites().filter(this.canOpen));\n protected readonly visibleFavorites = computed(() =>\n this.showAllFavorites() ? this.favorites() : this.favorites().slice(0, FAVORITE_ROWS),\n );\n protected readonly moreFavorites = computed(() => this.favorites().length > FAVORITE_ROWS);\n\n private readonly allRecents = computed<readonly RecentRow[]>(() => {\n const now = Date.now();\n return this.quickAccess\n .recents()\n .filter(this.canOpen)\n .map((item) => ({ ...item, where: this.whereOf(item), when: this.whenOf(item, now) }));\n });\n protected readonly recentRows = computed(() =>\n this.showAllRecents() ? this.allRecents() : this.allRecents().slice(0, RECENT_ROWS),\n );\n protected readonly moreRecents = computed(() => this.allRecents().length > RECENT_ROWS);\n\n protected readonly newsRows = computed(() => this.news.highlights().slice(0, NEWS_ROWS));\n protected readonly recentMenu: readonly MenuItem[] = [{ id: 'forget', label: this.t.forgetRecent, icon: 'times' }];\n\n constructor() {\n effect(() => {\n const only = this.workDestinations();\n if (only.length !== 1 || this.autoRedirect.done) return;\n this.autoRedirect.done = true;\n void this.router.navigateByUrl(only[0].redirect, { replaceUrl: true });\n });\n }\n\n protected caption(tile: HomeShortcut): string {\n if (tile.screens > 0) return pluralMessage(this.t.screenCount, tile.screens, this.messages.meta.locale);\n return tile.trail.join(TRAIL_SEPARATOR);\n }\n\n /** Where the screen sits in the menu; empty for a top-level screen (nothing above it to show). */\n protected whereOf(item: QuickAccessItem): string {\n const trail = item.trail ?? trailFor(this.tree(), item.redirect);\n return trail?.join(TRAIL_SEPARATOR) ?? '';\n }\n\n protected isFavorite(id: string): boolean {\n return this.quickAccess.isFavorite(id);\n }\n\n protected favoriteLabel(row: RecentRow): string {\n const template = this.isFavorite(row.id) ? this.t.unfavorite : this.t.favorite;\n return formatMessage(template, { name: row.displayName });\n }\n\n protected toggleFavorite(row: RecentRow): void {\n this.quickAccess.toggleFavorite({\n id: row.id,\n icon: row.icon,\n displayName: row.displayName,\n redirect: row.redirect,\n trail: row.trail ?? trailFor(this.tree(), row.redirect) ?? [],\n });\n }\n\n protected recentMenuLabel(row: RecentRow): string {\n return formatMessage(this.t.recentActions, { name: row.displayName });\n }\n\n protected onRecentMenu(row: RecentRow, action: string): void {\n if (action === 'forget') this.quickAccess.forgetRecent(row.id);\n }\n\n protected go(redirect: string | null | undefined): void {\n if (redirect) void this.router.navigateByUrl(redirect);\n }\n\n protected openSettings(): void {\n this.settings.open();\n }\n\n protected openDocs(): void {\n this.docs.openIndex();\n }\n\n // Lidos na hora do clique: a home não precisa montar o painel de atalhos nem a configuração inicial para aparecer.\n protected openShortcuts(): void {\n this.injector.get(ShortcutService).help$.next();\n }\n\n protected openEssentials(): void {\n this.injector.get(SpineInitialSetupService).openEssentials();\n }\n\n /** Com `id`: abre o painel já com a novidade aberta; sem, a lista. */\n protected openNews(id?: string): void {\n if (id) this.news.showItem(id);\n else this.news.showAll();\n }\n\n private greet(fullName: string | null | undefined): string {\n const hour = new Date().getHours();\n const greeting = hour < 12 ? this.t.morning : hour < 18 ? this.t.afternoon : this.t.evening;\n const name = (fullName ?? '').trim().split(/\\s+/)[0];\n return name ? formatMessage(this.t.greetingName, { greeting, name }) : greeting;\n }\n\n /** \"Há 12 min\", \"Ontem\", \"Há 2 dias\" (Intl, no idioma do catálogo); vazio para item gravado sem horário. */\n private whenOf(item: QuickAccessItem, now: number): string {\n if (!item.visitedAt) return '';\n const locale = this.messages.meta.locale;\n const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto', style: 'short' });\n const ago = Math.max(0, now - item.visitedAt);\n const text =\n ago < MINUTE\n ? rtf.format(0, 'second')\n : ago < HOUR\n ? rtf.format(-Math.floor(ago / MINUTE), 'minute')\n : ago < DAY\n ? rtf.format(-Math.floor(ago / HOUR), 'hour')\n : rtf.format(-Math.floor(ago / DAY), 'day');\n return text.charAt(0).toLocaleUpperCase(locale) + text.slice(1);\n }\n}\n","@if (noAccess()) {\n <hm-state-page kind=\"no-rights\" [heading]=\"t.noAccessTitle\" [description]=\"t.noAccessText\" action=\"\" />\n} @else {\n <div class=\"home\">\n <!-- Saudação à esquerda; as novidades, quando houver, ocupam o espaço à direita dela. -->\n <div class=\"hero\">\n <div class=\"hero__text\">\n <p class=\"hero__greeting\">{{ greeting }}</p>\n <p class=\"hero__intro\">{{ t.intro }}</p>\n </div>\n @if (newsRows().length) {\n <!-- Compacto: cabeçalho numa linha e cada novidade numa linha só (título e o começo do texto). -->\n <section class=\"card card--compact hero__news\" [attr.aria-labelledby]=\"uid + '-news'\">\n <div class=\"card__head\">\n <span class=\"card__heading\">\n <h2 class=\"card__title\" [id]=\"uid + '-news'\">{{ t.news }}</h2>\n <span class=\"badge\">{{ t.newsBadge }}</span>\n </span>\n <button type=\"button\" class=\"more\" (click)=\"openNews()\">{{ t.newsAll }}</button>\n </div>\n <ul class=\"list\">\n @for (entry of newsRows(); track entry.id) {\n <li>\n <button type=\"button\" class=\"news\" [title]=\"entry.title + ' — ' + entry.text\" (click)=\"openNews(entry.id)\">\n <span class=\"news__dot\" [class.news__dot--seen]=\"!entry.unseen\" aria-hidden=\"true\"></span>\n <span class=\"news__title\">{{ entry.title }}</span>\n <span class=\"news__body\">{{ entry.text }}</span>\n </button>\n </li>\n }\n </ul>\n </section>\n }\n </div>\n\n <!-- Atalhos de trabalho e privilegiados dividem a mesma linha (cada grupo na proporção dos seus atalhos). -->\n @if (workTiles().length || adminTiles().length) {\n <div class=\"shortcuts\">\n @if (workTiles().length) {\n <section class=\"block shortcuts__group\" [style.--tiles]=\"workTiles().length\" [attr.aria-labelledby]=\"uid + '-work'\">\n <h2 class=\"block__title\" [id]=\"uid + '-work'\">{{ descended() ? t.yourScreens : t.modules }}</h2>\n <ng-container *ngTemplateOutlet=\"tileGrid; context: { $implicit: workTiles(), admin: false }\" />\n </section>\n }\n @if (adminTiles().length) {\n <section class=\"block shortcuts__group\" [style.--tiles]=\"adminTiles().length\" [attr.aria-labelledby]=\"uid + '-admin'\">\n <h2 class=\"block__title\" [id]=\"uid + '-admin'\">{{ t.privileged }}</h2>\n <ng-container *ngTemplateOutlet=\"tileGrid; context: { $implicit: adminTiles(), admin: true }\" />\n </section>\n }\n </div>\n }\n\n <!-- Linha 1: o que a pessoa usa (recentes | favoritos). Linha 2: o resto do sistema (módulos | ajuda).\n Sem um dos lados, o outro ocupa a linha inteira. -->\n <div class=\"pair\">\n @if (recentRows().length) {\n <section class=\"card\" [attr.aria-labelledby]=\"uid + '-recent'\">\n <div class=\"card__head\">\n <h2 class=\"card__title\" [id]=\"uid + '-recent'\">{{ t.recent }}</h2>\n @if (moreRecents()) {\n <button type=\"button\" class=\"more\" [attr.aria-expanded]=\"showAllRecents()\" (click)=\"showAllRecents.set(!showAllRecents())\">\n {{ showAllRecents() ? t.seeLess : t.seeAll }}\n </button>\n }\n </div>\n <ul class=\"list\" [attr.aria-label]=\"t.recentCaption\">\n @for (item of recentRows(); track item.id) {\n <li class=\"recent\">\n <button type=\"button\" class=\"recent__open\" (click)=\"go(item.redirect)\">\n <hm-icon class=\"recent__icon\" [shape]=\"item.icon || 'grid-layout'\" />\n <span class=\"recent__name\">{{ item.displayName }}</span>\n <span class=\"recent__where\">{{ item.where }}</span>\n @if (item.when) {\n <span class=\"recent__when\">{{ item.when }}</span>\n }\n </button>\n <hm-icon-button\n emphasis=\"subtle\"\n size=\"small\"\n icon=\"heart\"\n [solid]=\"isFavorite(item.id)\"\n [class.heart--on]=\"isFavorite(item.id)\"\n [label]=\"favoriteLabel(item)\"\n (clicked)=\"toggleFavorite(item)\"\n />\n <hm-menu-button\n class=\"recent__menu\"\n icon=\"ellipsis-vertical\"\n shape=\"circle\"\n size=\"small\"\n align=\"end\"\n [showArrow]=\"false\"\n [label]=\"recentMenuLabel(item)\"\n [items]=\"recentMenu\"\n (chosen)=\"onRecentMenu(item, $event)\"\n />\n </li>\n }\n </ul>\n </section>\n }\n\n <section class=\"card\" [attr.aria-labelledby]=\"uid + '-fav'\">\n <div class=\"card__head\">\n <h2 class=\"card__title\" [id]=\"uid + '-fav'\">{{ t.favorites }}</h2>\n @if (moreFavorites()) {\n <button type=\"button\" class=\"more\" [attr.aria-expanded]=\"showAllFavorites()\" (click)=\"showAllFavorites.set(!showAllFavorites())\">\n {{ showAllFavorites() ? t.seeLess : t.seeAll }}\n </button>\n }\n </div>\n @if (visibleFavorites().length) {\n <ul class=\"list\">\n @for (fav of visibleFavorites(); track fav.id) {\n <li>\n <button type=\"button\" class=\"row\" (click)=\"go(fav.redirect)\">\n <hm-icon class=\"row__icon\" [shape]=\"fav.icon || 'grid-layout'\" />\n <span class=\"row__name\">{{ fav.displayName }}</span>\n <span class=\"row__meta\">{{ whereOf(fav) }}</span>\n <hm-icon class=\"row__chevron\" shape=\"angle-right\" size=\"sm\" />\n </button>\n </li>\n }\n </ul>\n } @else {\n <p class=\"card__empty\">{{ t.favoritesEmpty }}</p>\n }\n </section>\n </div>\n\n <div class=\"pair\">\n @if (systems().length) {\n <section class=\"card\" [attr.aria-labelledby]=\"uid + '-systems'\">\n <div class=\"card__head card__head--stacked\">\n <h2 class=\"card__title\" [id]=\"uid + '-systems'\">{{ t.explore }}</h2>\n <p class=\"card__intro\">{{ t.exploreIntro }}</p>\n </div>\n <ul class=\"systems\">\n @for (system of systems(); track system.id) {\n <!-- O card inteiro abre o módulo (como os atalhos): uma seta no canto, sem link próprio. -->\n <li class=\"systems__cell\">\n <button type=\"button\" class=\"system\" (click)=\"go(system.redirect)\">\n <span class=\"system__head\">\n <hm-icon class=\"system__icon\" [shape]=\"system.icon\" size=\"lg\" />\n <span class=\"system__name\" [title]=\"system.label\">{{ system.label }}</span>\n <span class=\"badge badge--neutral\">{{ system.count }}</span>\n <hm-icon class=\"system__chevron\" shape=\"angle-right\" size=\"sm\" />\n </span>\n @if (system.description) {\n <span class=\"system__description\">{{ system.description }}</span>\n }\n @if (system.chips.length) {\n <span class=\"system__chips\">\n @for (chip of system.chips; track chip) {\n <span class=\"system__chip\">{{ chip }}</span>\n }\n </span>\n }\n </button>\n </li>\n }\n </ul>\n </section>\n }\n\n <section class=\"card\" [attr.aria-labelledby]=\"uid + '-help'\">\n <div class=\"card__head\">\n <h2 class=\"card__title\" [id]=\"uid + '-help'\">{{ t.help }}</h2>\n </div>\n <ul class=\"list\">\n @for (area of areas(); track area.routed_id) {\n <li>\n <button type=\"button\" class=\"row\" (click)=\"go(area.route_redirect)\">\n <hm-icon class=\"row__icon\" [shape]=\"area.route_icon || 'home'\" />\n <span class=\"row__name\">{{ area.route_display_name }}</span>\n <hm-icon class=\"row__chevron\" shape=\"angle-right\" size=\"sm\" />\n </button>\n </li>\n }\n <li>\n <button type=\"button\" class=\"row\" (click)=\"openDocs()\">\n <hm-icon class=\"row__icon\" shape=\"book\" />\n <span class=\"row__name\">{{ t.systemDocs }}</span>\n <hm-icon class=\"row__chevron\" shape=\"angle-right\" size=\"sm\" />\n </button>\n </li>\n @if (suggestions) {\n <li>\n <button type=\"button\" class=\"row\" (click)=\"go(suggestionsPath)\">\n <hm-icon class=\"row__icon\" shape=\"lightbulb\" />\n <span class=\"row__name\">{{ suggestions }}</span>\n <hm-icon class=\"row__chevron\" shape=\"angle-right\" size=\"sm\" />\n </button>\n </li>\n }\n <li>\n <button type=\"button\" class=\"row\" (click)=\"openShortcuts()\">\n <hm-icon class=\"row__icon\" shape=\"keyboard\" />\n <span class=\"row__name\">{{ t.shortcuts }}</span>\n <hm-icon class=\"row__chevron\" shape=\"angle-right\" size=\"sm\" />\n </button>\n </li>\n <li>\n <button type=\"button\" class=\"row\" (click)=\"openEssentials()\">\n <hm-icon class=\"row__icon\" shape=\"help\" />\n <span class=\"row__name\">{{ t.essentials }}</span>\n <hm-icon class=\"row__chevron\" shape=\"angle-right\" size=\"sm\" />\n </button>\n </li>\n <li>\n <button type=\"button\" class=\"row\" (click)=\"openSettings()\">\n <hm-icon class=\"row__icon\" shape=\"cog\" />\n <span class=\"row__name\">{{ t.settings }}</span>\n <hm-icon class=\"row__chevron\" shape=\"angle-right\" size=\"sm\" />\n </button>\n </li>\n </ul>\n </section>\n </div>\n </div>\n}\n\n<ng-template #tileGrid let-tiles let-admin=\"admin\">\n <ul class=\"tiles\" [class.tiles--admin]=\"admin\">\n @for (tile of tiles; track tile.id; let i = $index) {\n <li class=\"tiles__cell\" [style.--tile-order]=\"i\">\n <button type=\"button\" class=\"tile\" (click)=\"go(tile.redirect)\">\n <hm-icon class=\"tile__icon\" [shape]=\"tile.icon ?? 'grid-layout'\" size=\"lg\" />\n <span class=\"tile__text\">\n <span class=\"tile__label\">{{ tile.label }}</span>\n @if (caption(tile); as meta) {\n <span class=\"tile__meta\">{{ meta }}</span>\n }\n </span>\n <hm-icon class=\"tile__chevron\" shape=\"angle-right\" size=\"sm\" />\n </button>\n </li>\n }\n </ul>\n</ng-template>\n"],"names":["labelOf"],"mappings":"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"}
@@ -61,9 +61,22 @@ Tela \`AutoServiceSuggestionsPageComponent\`, ligada ao router de template \`AUT
61
61
  conclusão, rótulos (catálogo do app; cria na hora), capa, tela com "Abrir a tela" (só quando o client administrado
62
62
  é o do app logado), apoios, criada/atualizada, "Última alteração há X por Fulano", chave com copiar, arquivar e
63
63
  excluir. Cada mudança grava na hora e o servidor devolve a tarefa inteira, que atualiza o cartão.
64
+ - **Planejamento** (pasta \`planning/\`, migration 0043 do argos): Resumo, Tarefas, Planejamento, Cronograma,
65
+ Riscos e Fluxo leem do \`SuggestionPlanningStore\` (nos \`providers\` do quadro), que carrega \`GET …/suggestions\`
66
+ sem recorte (as concluídas antigas entram no burndown; arquivadas e agrupadas são tiradas na tela), \`GET …/sprints\`,
67
+ \`GET …/milestones\` e \`GET …/risks\` ao entrar numa delas. Gravações: \`PATCH …/:id\` com \`estimate\` (0..999) e
68
+ \`sprint_id\` (arrastar no \`hm-backlog\`) ou \`start_date\`/\`due_date\` (arrastar no \`hm-gantt\`),
69
+ \`PUT\`/\`DELETE …/:id/dependencies/:dependsOnId\` (409 \`dependency_cycle\`), \`POST\`/\`PATCH\`/\`DELETE …/sprints\`
70
+ (uma ativa por app: 409 \`active_sprint_exists\`; \`state: closed\` devolve as não concluídas ao backlog),
71
+ \`…/milestones\` e \`…/risks\` (migration 0044: o risco leva \`suggestion_id\`, obrigatório ao criar; \`invalid_suggestion\`
72
+ vira o aviso de escolher a tarefa). A nota de risco da tarefa é \`risk_score\` (do servidor, junto com a tarefa) até
73
+ os riscos serem lidos; depois, \`SuggestionPlanningStore.riskScoreOf\` calcula dos riscos abertos e acompanha cada
74
+ gravação. O detalhe da tarefa lê os riscos uma vez ao abrir (\`ensureRisks\`). O burndown sai de \`taskBurndownInput\` → \`burndownSeries\` numa tabela (o spine não
75
+ depende do \`@trismegisto/charts\`); o Gantt e a carga, de \`taskGanttItems\` e \`taskWorkloadAllocations\`. O
76
+ Fluxo é o \`hm-workflow-diagram\` só leitura dos 5 status (o contrato com quem pede).
64
77
  - **Comentários**: texto simples. Interno (\`internal: true\`) só a equipe vê: não conta nem aparece como a última
65
78
  resposta em "Minhas sugestões" (\`/me/suggestions\`). Sugestão de outro client: 404 \`suggestion_not_found\`.
66
79
  `;
67
80
 
68
81
  export { suggestions_technical as default };
69
- //# sourceMappingURL=trismegisto-spine-auto-service-suggestions.technical-BtUK6sKD.mjs.map
82
+ //# sourceMappingURL=trismegisto-spine-auto-service-suggestions.technical-CGBBzAR8.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"trismegisto-spine-auto-service-suggestions.technical-BtUK6sKD.mjs","sources":["../../../projects/ui/auto-service/src/docs/suggestions.technical.ts"],"sourcesContent":["// Especificação técnica da tela Sugestões do auto-serviço (`RouteDocument.loadTechnical`), em Markdown. Módulo próprio,\n// carregado só pelo `import()` de `AUTO_SERVICE_DOCUMENTS`: vira um chunk à parte no FESM e fica fora do bundle\n// inicial. Não importe estaticamente nem reexporte no `public-api.ts`.\nexport default `# Sugestões — especificação técnica\n\nTela \\`AutoServiceSuggestionsPageComponent\\`, ligada ao router de template \\`AUTO_SERVICE_SUGGESTIONS_TEMPLATE_ROUTER_ID\\`\n(\\`ad100007-…\\`, página \\`app-suggestions\\` na raiz do menu, migration 0036 do argos): o seletor de client\n(\\`AutoServiceClientPickerComponent\\`) e, embaixo, o painel da tela. Fala com o argos-admin-server em\n\\`{adminApi}/auto-service\\`, com o token no cabeçalho \\`x-access-token\\`.\n\n## Endpoints\n\n| Ação | Endpoint |\n| --- | --- |\n| Clients que a pessoa administra | \\`GET /auto-service\\` |\n| Quadro (\\`web.app_suggestions\\`) | \\`GET /auto-service/:clientId/suggestions?board=true\\`: sem arquivadas, recusadas e concluídas fora da janela (\\`done_days\\`, 7; \\`due_soon_days\\`, 1), com autor, responsáveis e envolvidos (com foto), rótulos, checklist e a última atividade |\n| Arquivo | \\`GET …/suggestions?archive=true\\` (\\`search\\`, \\`kind\\`, \\`status\\`, \\`limit\\`, \\`offset\\`): arquivadas, recusadas e concluídas antigas, com \\`total\\` |\n| Filtros do servidor | \\`kind\\`, \\`status\\`, \\`screen\\`, \\`search\\` (inclui a chave \\`SUG-12\\`), \\`priority\\`, \\`label\\`, \\`assignee\\` (\\`me\\`, \\`none\\` ou um id), \\`due\\` (\\`overdue\\` ou \\`soon\\`) |\n| Criar (equipe) | \\`POST …/suggestions\\` (\\`{ title, status?, kind?, priority?, start_date?, due_date?, cover?, description_html?, people?, label_ids? }\\`; \\`origin: equipe\\`) |\n| Tarefa | \\`GET …/suggestions/:id\\` (com o checklist completo); \\`GET …/suggestions/lookup?key=SUG-12\\` (o link copiado) |\n| Editar, mover, mudar status | \\`PATCH …/suggestions/:id\\` (campos da tarefa e/ou \\`{ status?, position?, comment?, internal? }\\`) |\n| Arquivar, restaurar, excluir | \\`POST …/:id/archive\\`, \\`POST …/:id/restore\\`, \\`DELETE …/:id\\` |\n| Atividades e comentários | \\`GET …/:id/events?type=comments\\` ou \\`type=activity\\` (do mais novo, paginado); \\`POST …/:id/comments\\` (\\`{ comment, internal? }\\`) |\n| Pessoas | \\`POST …/:id/people\\` (\\`{ user_id, role }\\`; \\`user_id: \"me\"\\` = atribuir a mim; \\`role\\` responsavel ou envolvido), \\`DELETE …/:id/people/:userId?role=\\`; busca em \\`GET /auto-service/:clientId/users\\` |\n| Checklist | \\`POST …/:id/checklist\\`, \\`PATCH …/:id/checklist/:itemId\\` (\\`{ text?, done?, position? }\\`), \\`DELETE …/:id/checklist/:itemId\\` |\n| Rótulos | catálogo \\`GET\\`/\\`POST …/suggestions/labels\\`, \\`PATCH\\`/\\`DELETE …/labels/:labelId\\`; na tarefa \\`PUT\\`/\\`DELETE …/:id/labels/:labelId\\` |\n\n## Regras\n\n- **Autorização** (\\`clientAdminGuard\\`): grupo de administração do projeto do client + crachá Administrador ativo. O\n \\`client_id\\` vem sempre da URL; 403 vira a mensagem de falta de acesso.\n- **Menu**: página privada, com os tipos permitidos e os guards que a tela Cliente tinha em cada client (a migration\n copia); o painel só é montado depois que o client é escolhido.\n- **Status**: toda sugestão nasce \\`em_analise\\` (não existe mais \\`nova\\`; a migration 0035 do argos move as antigas).\n O quadro tem 4 colunas (\\`em_analise\\`, \\`planejada\\`, \\`em_desenvolvimento\\`, \\`concluida\\`); \\`recusada\\` só aparece no\n arquivo. O corte da janela das concluídas é do servidor.\n- **Tela** (\\`AutoServiceSuggestionsComponent\\`, serviço próprio \\`AutoServiceSuggestionsService\\`): \\`hm-kanban\\` com os\n cartões por \\`position\\`. Os 8 indicadores (abertas, em análise, planejadas, em desenvolvimento, atribuídas a mim,\n vencidas, concluídas na janela e a mais votada) são do quadro inteiro, sem filtro; \"atribuídas a mim\" e \"vencidas\"\n também ligam o filtro. Os filtros (texto sem acento ou a chave, responsável — minhas, sem responsável ou uma pessoa\n —, rótulo, prioridade, prazo, tipo, tela, quem pediu) e a ordenação (\\`position\\`, \\`votes\\`, \\`recent\\`, prioridade,\n prazo) são da tela, com a mesma regra dos filtros do servidor. \"Eu\" é o \\`sub\\` da sessão (\\`SpineUserStorage\\`).\n- **Cartão**: capa (token da paleta do tema: \\`cat-1\\`..\\`cat-8\\`, \\`grad-1\\`..\\`grad-6\\`), chave, tipo, rótulos, título,\n trecho, tela, checklist (feitos/total; os itens marcáveis no cartão com \\`checklist_on_card\\`), prioridade, prazo\n (vencida, hoje, amanhã), comentários, apoios e até 3 responsáveis com \"+N\". O \"⋯\" (\\`hm-menu-button\\`): status e\n prioridade em submenu, atribuir a mim, rótulos, capa (\\`hm-popover\\` ancorado no cartão), copiar link\n (\\`?sugestao=SUG-12\\`, que abre a tarefa ao carregar o quadro) e chave, arquivar e excluir (\\`HermesDialogService.confirm\\`).\n- **Criar**: o \"+\" no cabeçalho de cada coluna abre a criação rápida (título e Enter, no fim da coluna); \"Nova tarefa\"\n abre a tarefa em branco (\\`SuggestionTaskDetailComponent\\` em rascunho) e, criada, vira a tarefa completa. Na ordem do quadro, o \\`moved\\` dá o índice na coluna\n FILTRADA, e a tela converte para o índice na coluna inteira pelo vizinho visível de cima (ou o de baixo; sozinho =\n fim) antes do \\`PATCH\\` -- o servidor renumera a coluna de destino 0..n; com outra ordenação, só a troca de coluna\n grava (\\`{ status }\\`, no fim da coluna) e o arrasto dentro dela volta. O movimento é aplicado na hora; falha = toast\n \\`suggestions.moveError\\` e releitura. Selo \"Mais votada\" nas 3 com mais apoios.\n- **Arquivo**: paginado no servidor (20 por página), com busca e filtros de status e tipo; **Reabrir** nas recusadas\n (\\`PATCH\\` com \\`status: em_analise\\`) e **Restaurar** nas arquivadas (\\`POST …/restore\\`).\n- **Tarefa** (\\`SuggestionTaskDetailComponent\\`, \\`hm-modal\\` xlarge em duas colunas): à esquerda título editável no\n lugar, descrição em \\`hm-rich-text\\` (o servidor saneia com a lista branca das novidades), checklist (marcar,\n renomear, subir/descer, remover, \"Mostrar no cartão\") e as abas Comentários e Atividades (\\`events\\`, do mais novo,\n \"Carregar mais\"); à direita status, responsáveis (com \"Atribuir a mim\") e envolvidos (busca de pessoas do realm do\n app, com foto; o servidor recusa quem é de fora, \\`person_not_in_realm\\`), relator e origem, prioridade, tipo, início e\n conclusão, rótulos (catálogo do app; cria na hora), capa, tela com \"Abrir a tela\" (só quando o client administrado\n é o do app logado), apoios, criada/atualizada, \"Última alteração há X por Fulano\", chave com copiar, arquivar e\n excluir. Cada mudança grava na hora e o servidor devolve a tarefa inteira, que atualiza o cartão.\n- **Comentários**: texto simples. Interno (\\`internal: true\\`) só a equipe vê: não conta nem aparece como a última\n resposta em \"Minhas sugestões\" (\\`/me/suggestions\\`). Sugestão de outro client: 404 \\`suggestion_not_found\\`.\n`;\n"],"names":[],"mappings":"AAAA;AACA;AACA;AACA,4BAAe,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8Dd;;"}
1
+ {"version":3,"file":"trismegisto-spine-auto-service-suggestions.technical-CGBBzAR8.mjs","sources":["../../../projects/ui/auto-service/src/docs/suggestions.technical.ts"],"sourcesContent":["// Especificação técnica da tela Sugestões do auto-serviço (`RouteDocument.loadTechnical`), em Markdown. Módulo próprio,\n// carregado só pelo `import()` de `AUTO_SERVICE_DOCUMENTS`: vira um chunk à parte no FESM e fica fora do bundle\n// inicial. Não importe estaticamente nem reexporte no `public-api.ts`.\nexport default `# Sugestões — especificação técnica\n\nTela \\`AutoServiceSuggestionsPageComponent\\`, ligada ao router de template \\`AUTO_SERVICE_SUGGESTIONS_TEMPLATE_ROUTER_ID\\`\n(\\`ad100007-…\\`, página \\`app-suggestions\\` na raiz do menu, migration 0036 do argos): o seletor de client\n(\\`AutoServiceClientPickerComponent\\`) e, embaixo, o painel da tela. Fala com o argos-admin-server em\n\\`{adminApi}/auto-service\\`, com o token no cabeçalho \\`x-access-token\\`.\n\n## Endpoints\n\n| Ação | Endpoint |\n| --- | --- |\n| Clients que a pessoa administra | \\`GET /auto-service\\` |\n| Quadro (\\`web.app_suggestions\\`) | \\`GET /auto-service/:clientId/suggestions?board=true\\`: sem arquivadas, recusadas e concluídas fora da janela (\\`done_days\\`, 7; \\`due_soon_days\\`, 1), com autor, responsáveis e envolvidos (com foto), rótulos, checklist e a última atividade |\n| Arquivo | \\`GET …/suggestions?archive=true\\` (\\`search\\`, \\`kind\\`, \\`status\\`, \\`limit\\`, \\`offset\\`): arquivadas, recusadas e concluídas antigas, com \\`total\\` |\n| Filtros do servidor | \\`kind\\`, \\`status\\`, \\`screen\\`, \\`search\\` (inclui a chave \\`SUG-12\\`), \\`priority\\`, \\`label\\`, \\`assignee\\` (\\`me\\`, \\`none\\` ou um id), \\`due\\` (\\`overdue\\` ou \\`soon\\`) |\n| Criar (equipe) | \\`POST …/suggestions\\` (\\`{ title, status?, kind?, priority?, start_date?, due_date?, cover?, description_html?, people?, label_ids? }\\`; \\`origin: equipe\\`) |\n| Tarefa | \\`GET …/suggestions/:id\\` (com o checklist completo); \\`GET …/suggestions/lookup?key=SUG-12\\` (o link copiado) |\n| Editar, mover, mudar status | \\`PATCH …/suggestions/:id\\` (campos da tarefa e/ou \\`{ status?, position?, comment?, internal? }\\`) |\n| Arquivar, restaurar, excluir | \\`POST …/:id/archive\\`, \\`POST …/:id/restore\\`, \\`DELETE …/:id\\` |\n| Atividades e comentários | \\`GET …/:id/events?type=comments\\` ou \\`type=activity\\` (do mais novo, paginado); \\`POST …/:id/comments\\` (\\`{ comment, internal? }\\`) |\n| Pessoas | \\`POST …/:id/people\\` (\\`{ user_id, role }\\`; \\`user_id: \"me\"\\` = atribuir a mim; \\`role\\` responsavel ou envolvido), \\`DELETE …/:id/people/:userId?role=\\`; busca em \\`GET /auto-service/:clientId/users\\` |\n| Checklist | \\`POST …/:id/checklist\\`, \\`PATCH …/:id/checklist/:itemId\\` (\\`{ text?, done?, position? }\\`), \\`DELETE …/:id/checklist/:itemId\\` |\n| Rótulos | catálogo \\`GET\\`/\\`POST …/suggestions/labels\\`, \\`PATCH\\`/\\`DELETE …/labels/:labelId\\`; na tarefa \\`PUT\\`/\\`DELETE …/:id/labels/:labelId\\` |\n\n## Regras\n\n- **Autorização** (\\`clientAdminGuard\\`): grupo de administração do projeto do client + crachá Administrador ativo. O\n \\`client_id\\` vem sempre da URL; 403 vira a mensagem de falta de acesso.\n- **Menu**: página privada, com os tipos permitidos e os guards que a tela Cliente tinha em cada client (a migration\n copia); o painel só é montado depois que o client é escolhido.\n- **Status**: toda sugestão nasce \\`em_analise\\` (não existe mais \\`nova\\`; a migration 0035 do argos move as antigas).\n O quadro tem 4 colunas (\\`em_analise\\`, \\`planejada\\`, \\`em_desenvolvimento\\`, \\`concluida\\`); \\`recusada\\` só aparece no\n arquivo. O corte da janela das concluídas é do servidor.\n- **Tela** (\\`AutoServiceSuggestionsComponent\\`, serviço próprio \\`AutoServiceSuggestionsService\\`): \\`hm-kanban\\` com os\n cartões por \\`position\\`. Os 8 indicadores (abertas, em análise, planejadas, em desenvolvimento, atribuídas a mim,\n vencidas, concluídas na janela e a mais votada) são do quadro inteiro, sem filtro; \"atribuídas a mim\" e \"vencidas\"\n também ligam o filtro. Os filtros (texto sem acento ou a chave, responsável — minhas, sem responsável ou uma pessoa\n —, rótulo, prioridade, prazo, tipo, tela, quem pediu) e a ordenação (\\`position\\`, \\`votes\\`, \\`recent\\`, prioridade,\n prazo) são da tela, com a mesma regra dos filtros do servidor. \"Eu\" é o \\`sub\\` da sessão (\\`SpineUserStorage\\`).\n- **Cartão**: capa (token da paleta do tema: \\`cat-1\\`..\\`cat-8\\`, \\`grad-1\\`..\\`grad-6\\`), chave, tipo, rótulos, título,\n trecho, tela, checklist (feitos/total; os itens marcáveis no cartão com \\`checklist_on_card\\`), prioridade, prazo\n (vencida, hoje, amanhã), comentários, apoios e até 3 responsáveis com \"+N\". O \"⋯\" (\\`hm-menu-button\\`): status e\n prioridade em submenu, atribuir a mim, rótulos, capa (\\`hm-popover\\` ancorado no cartão), copiar link\n (\\`?sugestao=SUG-12\\`, que abre a tarefa ao carregar o quadro) e chave, arquivar e excluir (\\`HermesDialogService.confirm\\`).\n- **Criar**: o \"+\" no cabeçalho de cada coluna abre a criação rápida (título e Enter, no fim da coluna); \"Nova tarefa\"\n abre a tarefa em branco (\\`SuggestionTaskDetailComponent\\` em rascunho) e, criada, vira a tarefa completa. Na ordem do quadro, o \\`moved\\` dá o índice na coluna\n FILTRADA, e a tela converte para o índice na coluna inteira pelo vizinho visível de cima (ou o de baixo; sozinho =\n fim) antes do \\`PATCH\\` -- o servidor renumera a coluna de destino 0..n; com outra ordenação, só a troca de coluna\n grava (\\`{ status }\\`, no fim da coluna) e o arrasto dentro dela volta. O movimento é aplicado na hora; falha = toast\n \\`suggestions.moveError\\` e releitura. Selo \"Mais votada\" nas 3 com mais apoios.\n- **Arquivo**: paginado no servidor (20 por página), com busca e filtros de status e tipo; **Reabrir** nas recusadas\n (\\`PATCH\\` com \\`status: em_analise\\`) e **Restaurar** nas arquivadas (\\`POST …/restore\\`).\n- **Tarefa** (\\`SuggestionTaskDetailComponent\\`, \\`hm-modal\\` xlarge em duas colunas): à esquerda título editável no\n lugar, descrição em \\`hm-rich-text\\` (o servidor saneia com a lista branca das novidades), checklist (marcar,\n renomear, subir/descer, remover, \"Mostrar no cartão\") e as abas Comentários e Atividades (\\`events\\`, do mais novo,\n \"Carregar mais\"); à direita status, responsáveis (com \"Atribuir a mim\") e envolvidos (busca de pessoas do realm do\n app, com foto; o servidor recusa quem é de fora, \\`person_not_in_realm\\`), relator e origem, prioridade, tipo, início e\n conclusão, rótulos (catálogo do app; cria na hora), capa, tela com \"Abrir a tela\" (só quando o client administrado\n é o do app logado), apoios, criada/atualizada, \"Última alteração há X por Fulano\", chave com copiar, arquivar e\n excluir. Cada mudança grava na hora e o servidor devolve a tarefa inteira, que atualiza o cartão.\n- **Planejamento** (pasta \\`planning/\\`, migration 0043 do argos): Resumo, Tarefas, Planejamento, Cronograma,\n Riscos e Fluxo leem do \\`SuggestionPlanningStore\\` (nos \\`providers\\` do quadro), que carrega \\`GET …/suggestions\\`\n sem recorte (as concluídas antigas entram no burndown; arquivadas e agrupadas são tiradas na tela), \\`GET …/sprints\\`,\n \\`GET …/milestones\\` e \\`GET …/risks\\` ao entrar numa delas. Gravações: \\`PATCH …/:id\\` com \\`estimate\\` (0..999) e\n \\`sprint_id\\` (arrastar no \\`hm-backlog\\`) ou \\`start_date\\`/\\`due_date\\` (arrastar no \\`hm-gantt\\`),\n \\`PUT\\`/\\`DELETE …/:id/dependencies/:dependsOnId\\` (409 \\`dependency_cycle\\`), \\`POST\\`/\\`PATCH\\`/\\`DELETE …/sprints\\`\n (uma ativa por app: 409 \\`active_sprint_exists\\`; \\`state: closed\\` devolve as não concluídas ao backlog),\n \\`…/milestones\\` e \\`…/risks\\` (migration 0044: o risco leva \\`suggestion_id\\`, obrigatório ao criar; \\`invalid_suggestion\\`\n vira o aviso de escolher a tarefa). A nota de risco da tarefa é \\`risk_score\\` (do servidor, junto com a tarefa) até\n os riscos serem lidos; depois, \\`SuggestionPlanningStore.riskScoreOf\\` calcula dos riscos abertos e acompanha cada\n gravação. O detalhe da tarefa lê os riscos uma vez ao abrir (\\`ensureRisks\\`). O burndown sai de \\`taskBurndownInput\\` → \\`burndownSeries\\` numa tabela (o spine não\n depende do \\`@trismegisto/charts\\`); o Gantt e a carga, de \\`taskGanttItems\\` e \\`taskWorkloadAllocations\\`. O\n Fluxo é o \\`hm-workflow-diagram\\` só leitura dos 5 status (o contrato com quem pede).\n- **Comentários**: texto simples. Interno (\\`internal: true\\`) só a equipe vê: não conta nem aparece como a última\n resposta em \"Minhas sugestões\" (\\`/me/suggestions\\`). Sugestão de outro client: 404 \\`suggestion_not_found\\`.\n`;\n"],"names":[],"mappings":"AAAA;AACA;AACA;AACA,4BAAe,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2Ed;;"}
@@ -14,8 +14,31 @@ preciso estar no grupo de administração do projeto desse app e com o crachá *
14
14
  ## Como usar
15
15
 
16
16
  As pessoas pedem melhorias pelo botão **Sugerir melhoria** da barra de cada tela (tipo, título, descrição e a tela de
17
- onde vieram). Toda sugestão chega **Em análise**. No topo, escolha **Quadro** (o que está andando) ou **Arquivo** (o
18
- que já saiu do quadro).
17
+ onde vieram). Toda sugestão chega **Em análise**. No topo, escolha a visão: **Resumo**, **Tarefas**, **Quadro** (o
18
+ que está andando), **Planejamento**, **Cronograma**, **Riscos**, **Fluxo** ou **Arquivo** (o que já saiu do quadro).
19
+
20
+ ## Planejamento
21
+
22
+ - **Resumo**: a sprint em andamento (meta, datas e pontos concluídos contra o comprometido e a capacidade), as tarefas
23
+ abertas, as vencidas, **Minhas tarefas**, os prazos dos próximos 7 dias, os riscos altos em aberto e o próximo marco.
24
+ - **Tarefas**: as abertas em **Lista** (agrupada pelo status) ou em **Tabela** (chave, título, status, prioridade,
25
+ responsável, sprint, pontos e prazo; clique no título da coluna para ordenar). **Abrir** leva à tarefa.
26
+ - **Planejamento**: as sprints (**Nova sprint** com nome, meta, início, fim e capacidade em pontos; **Iniciar**, só uma
27
+ por vez; **Encerrar**, que devolve ao backlog as não concluídas; **Editar** e **Excluir**). Arraste uma tarefa do
28
+ **Backlog** para uma sprint, ou de volta. Abaixo, o andamento da sprint em curso e o burndown, dia a dia.
29
+ - **Cronograma**: as tarefas abertas com data, agrupadas pela sprint, com as setas de dependência e os marcos.
30
+ Arraste uma barra para mudar início e prazo (dependência quebrada vira aviso) ou um marco para mudar a data. Os
31
+ marcos se cadastram e se excluem ali. **Carga da equipe**: os pontos de cada pessoa nas próximas 4 semanas;
32
+ clicar numa pessoa mostra o quadro filtrado por ela.
33
+ - **Riscos**: a matriz de probabilidade e impacto (clique numa célula para filtrar) e a lista, com a tarefa ameaçada
34
+ embaixo de cada risco. Todo risco é de uma tarefa. **Novo risco**: título, tarefa, probabilidade, impacto, dono,
35
+ status (**Identificado**, **Em mitigação**, **Encerrado**) e a mitigação. No detalhe da tarefa, a seção **Riscos**
36
+ lista os dela e cadastra outros já com a tarefa preenchida. A nota de risco da tarefa (Baixo, Médio, Alto, Crítico) é
37
+ a do maior risco aberto: aparece como selo no cartão do quadro e na coluna **Risco** da tabela de Tarefas.
38
+ - **Fluxo**: os 5 status e as passagens entre eles, com quantas há em cada um. É o que quem pede acompanha em
39
+ **Minhas sugestões**, por isso não muda por app.
40
+ - Na tarefa: **Pontos**, **Sprint** e **Depende de** (as tarefas que precisam terminar antes; uma ligação que formaria
41
+ um ciclo é recusada). Uma tarefa com dependência aberta mostra **Bloqueada por** com as chaves.
19
42
 
20
43
  ## Quadro
21
44
 
@@ -69,4 +92,4 @@ Clique no **título** do cartão para abrir a tarefa:
69
92
  `;
70
93
 
71
94
  export { suggestions_usage as default };
72
- //# sourceMappingURL=trismegisto-spine-auto-service-suggestions.usage-BwO9Hjsx.mjs.map
95
+ //# sourceMappingURL=trismegisto-spine-auto-service-suggestions.usage-DnAJS-Z0.mjs.map