@proef/common-ui 2.6.1 → 2.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/v1/components/forms/FormField/FormField.js +1 -1
- package/dist/esm/v1/components/forms/FormField/FormField.js.map +1 -1
- package/dist/esm/v1/components/overlays/Dialog/Dialog.js +15 -6
- package/dist/esm/v1/components/overlays/Dialog/Dialog.js.map +1 -1
- package/dist/esm/v1/primitives/display/Avatar/Avatar.js +3 -2
- package/dist/esm/v1/primitives/display/Avatar/Avatar.js.map +1 -1
- package/dist/style.components.css +1 -1
- package/dist/style.css +1 -1
- package/dist/style.layered.css +1 -1
- package/dist/theme.css +5 -0
- package/dist/tokens.css +5 -0
- package/package.json +1 -1
|
@@ -23,7 +23,7 @@ function FormField({ label, required, hint, marks, markSlots, aside, end, wide,
|
|
|
23
23
|
"label",
|
|
24
24
|
{
|
|
25
25
|
htmlFor: id,
|
|
26
|
-
className: "m-0 text-xs leading-[18px] font-medium
|
|
26
|
+
className: "m-0 text-xs leading-[18px] font-medium text-muted-foreground",
|
|
27
27
|
children: [
|
|
28
28
|
label,
|
|
29
29
|
required && /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "ml-0.5 text-destructive", children: "*" })
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FormField.js","sources":["../../../../../../src/v1/components/forms/FormField/FormField.tsx"],"sourcesContent":["import React, { useId } from 'react'\r\nimport { FieldContext } from '../../../primitives/forms/field'\r\nimport { useFormGrid } from '../FormGrid/FormGrid'\r\n\r\nexport interface FormFieldProps {\r\n label: string\r\n /** Asterisco vermelho na etiqueta; o campo lá dentro fica `required`. */\r\n required?: boolean\r\n /** Texto pequeno por baixo da etiqueta. */\r\n hint?: string\r\n /**\r\n * Marcas antes da etiqueta, a dizer de onde veio o valor (ex: lido do QR,\r\n * identificado pela IA). Usar [FieldMark](#fieldmark).\r\n */\r\n marks?: React.ReactNode\r\n /**\r\n * Lugares reservados para marcas, mesmo sem nenhuma. Assim a etiqueta não\r\n * salta quando uma marca aparece depois (ex: ao ler o QR).\r\n */\r\n markSlots?: number\r\n /** Algo junto à etiqueta, depois das marcas (ex: o valor máximo permitido). */\r\n aside?: React.ReactNode\r\n /** Depois do campo, na mesma linha (ex: cadeado, ícone de informação). */\r\n end?: React.ReactNode\r\n /** Ocupa a linha toda (ex: observações, anexo). */\r\n wide?: boolean\r\n /** Etiqueta alinhada ao topo em vez de ao meio (campos altos, como o [TextArea](?path=/docs/v1-primitives-forms-textarea--docs)). */\r\n alignTop?: boolean\r\n /** O campo: [TextInput](?path=/docs/v1-primitives-forms-textinput--docs), [SelectInput](?path=/docs/v1-primitives-forms-selectinput--docs), [TextArea](?path=/docs/v1-primitives-forms-textarea--docs), [LookupInput](?path=/docs/v1-primitives-forms-lookupinput--docs) ou outro. */\r\n children: React.ReactNode\r\n}\r\n\r\nconst MARK = 17\r\nconst MARK_GAP = 4\r\n\r\n/**\r\n * Uma linha do formulário: etiqueta e campo. Vai dentro de um\r\n * [FormGrid](?path=/docs/v1-components-forms-formgrid--docs).\r\n *\r\n * Liga a etiqueta ao campo (clicar na etiqueta foca o campo) e passa-lhe o\r\n * `required`, se o campo for [TextInput](?path=/docs/v1-primitives-forms-textinput--docs), [SelectInput](?path=/docs/v1-primitives-forms-selectinput--docs), [TextArea](?path=/docs/v1-primitives-forms-textarea--docs) ou [LookupInput](?path=/docs/v1-primitives-forms-lookupinput--docs).\r\n */\r\nexport default function FormField({ label, required, hint, marks, markSlots, aside, end, wide, alignTop, children }: FormFieldProps) {\r\n const id = useId()\r\n const { columns } = useFormGrid()\r\n const marksCount = React.Children.count(marks)\r\n const slots = Math.max(markSlots ?? 0, marksCount)\r\n // Em 2 colunas, o campo largo começa sempre numa linha nova: depois de um campo\r\n // de meia linha, a etiqueta caía na 3.ª coluna e o campo descia para a linha seguinte.\r\n const wideRow = wide && columns === 2\r\n\r\n return (\r\n <FieldContext.Provider value={{ id, required }}>\r\n <div\r\n className={`flex flex-col items-end pr-3 text-right @max-form-sm:items-start @max-form-sm:pr-0 @max-form-sm:pt-0 @max-form-sm:text-left ${wideRow ? 'col-start-1' : ''} ${alignTop ? 'self-start pt-[7px]' : ''}`}\r\n >\r\n {/* As marcas ficam fora do <label>, para não entrarem no nome do campo lido pelos leitores de ecrã. */}\r\n <div className=\"flex items-center justify-end @max-form-sm:justify-start\">\r\n {slots > 0 && (\r\n <span className=\"mr-[5px] inline-flex flex-none items-center justify-end gap-1\" style={{ width: slots * MARK + (slots - 1) * MARK_GAP }}>\r\n {marks}\r\n </span>\r\n )}\r\n {aside && <span className=\"mr-1.5 flex-none\">{aside}</span>}\r\n <label\r\n htmlFor={id}\r\n className=\"m-0 text-xs leading-[18px] font-medium
|
|
1
|
+
{"version":3,"file":"FormField.js","sources":["../../../../../../src/v1/components/forms/FormField/FormField.tsx"],"sourcesContent":["import React, { useId } from 'react'\r\nimport { FieldContext } from '../../../primitives/forms/field'\r\nimport { useFormGrid } from '../FormGrid/FormGrid'\r\n\r\nexport interface FormFieldProps {\r\n label: string\r\n /** Asterisco vermelho na etiqueta; o campo lá dentro fica `required`. */\r\n required?: boolean\r\n /** Texto pequeno por baixo da etiqueta. */\r\n hint?: string\r\n /**\r\n * Marcas antes da etiqueta, a dizer de onde veio o valor (ex: lido do QR,\r\n * identificado pela IA). Usar [FieldMark](#fieldmark).\r\n */\r\n marks?: React.ReactNode\r\n /**\r\n * Lugares reservados para marcas, mesmo sem nenhuma. Assim a etiqueta não\r\n * salta quando uma marca aparece depois (ex: ao ler o QR).\r\n */\r\n markSlots?: number\r\n /** Algo junto à etiqueta, depois das marcas (ex: o valor máximo permitido). */\r\n aside?: React.ReactNode\r\n /** Depois do campo, na mesma linha (ex: cadeado, ícone de informação). */\r\n end?: React.ReactNode\r\n /** Ocupa a linha toda (ex: observações, anexo). */\r\n wide?: boolean\r\n /** Etiqueta alinhada ao topo em vez de ao meio (campos altos, como o [TextArea](?path=/docs/v1-primitives-forms-textarea--docs)). */\r\n alignTop?: boolean\r\n /** O campo: [TextInput](?path=/docs/v1-primitives-forms-textinput--docs), [SelectInput](?path=/docs/v1-primitives-forms-selectinput--docs), [TextArea](?path=/docs/v1-primitives-forms-textarea--docs), [LookupInput](?path=/docs/v1-primitives-forms-lookupinput--docs) ou outro. */\r\n children: React.ReactNode\r\n}\r\n\r\nconst MARK = 17\r\nconst MARK_GAP = 4\r\n\r\n/**\r\n * Uma linha do formulário: etiqueta e campo. Vai dentro de um\r\n * [FormGrid](?path=/docs/v1-components-forms-formgrid--docs).\r\n *\r\n * Liga a etiqueta ao campo (clicar na etiqueta foca o campo) e passa-lhe o\r\n * `required`, se o campo for [TextInput](?path=/docs/v1-primitives-forms-textinput--docs), [SelectInput](?path=/docs/v1-primitives-forms-selectinput--docs), [TextArea](?path=/docs/v1-primitives-forms-textarea--docs) ou [LookupInput](?path=/docs/v1-primitives-forms-lookupinput--docs).\r\n */\r\nexport default function FormField({ label, required, hint, marks, markSlots, aside, end, wide, alignTop, children }: FormFieldProps) {\r\n const id = useId()\r\n const { columns } = useFormGrid()\r\n const marksCount = React.Children.count(marks)\r\n const slots = Math.max(markSlots ?? 0, marksCount)\r\n // Em 2 colunas, o campo largo começa sempre numa linha nova: depois de um campo\r\n // de meia linha, a etiqueta caía na 3.ª coluna e o campo descia para a linha seguinte.\r\n const wideRow = wide && columns === 2\r\n\r\n return (\r\n <FieldContext.Provider value={{ id, required }}>\r\n <div\r\n className={`flex flex-col items-end pr-3 text-right @max-form-sm:items-start @max-form-sm:pr-0 @max-form-sm:pt-0 @max-form-sm:text-left ${wideRow ? 'col-start-1' : ''} ${alignTop ? 'self-start pt-[7px]' : ''}`}\r\n >\r\n {/* As marcas ficam fora do <label>, para não entrarem no nome do campo lido pelos leitores de ecrã. */}\r\n <div className=\"flex items-center justify-end @max-form-sm:justify-start\">\r\n {slots > 0 && (\r\n <span className=\"mr-[5px] inline-flex flex-none items-center justify-end gap-1\" style={{ width: slots * MARK + (slots - 1) * MARK_GAP }}>\r\n {marks}\r\n </span>\r\n )}\r\n {aside && <span className=\"mr-1.5 flex-none\">{aside}</span>}\r\n {/* A coluna das etiquetas tem largura fixa: uma etiqueta mais comprida passa para a\r\n linha seguinte em vez de sair pela esquerda (ex: \"Mensagem para o requisitante\"). */}\r\n <label\r\n htmlFor={id}\r\n className=\"m-0 text-xs leading-[18px] font-medium text-muted-foreground\"\r\n >\r\n {label}\r\n {required && (\r\n <span aria-hidden className=\"ml-0.5 text-destructive\">\r\n *\r\n </span>\r\n )}\r\n </label>\r\n </div>\r\n {hint && <span className=\"text-micro text-muted-foreground\">{hint}</span>}\r\n </div>\r\n <div className={`min-w-0 ${wideRow ? 'col-span-3 @max-form-sm:col-span-1' : ''} ${end ? 'flex items-center gap-1.5' : ''}`}>\r\n {children}\r\n {end}\r\n </div>\r\n </FieldContext.Provider>\r\n )\r\n}\r\n\r\nexport interface FieldMarkProps {\r\n /** De onde veio o valor (ex: \"Lido do QR Code\"). Mostrado ao passar o rato e lido pelos leitores de ecrã. */\r\n title: string\r\n /** O ícone, a 11 px (ex: `<QrCodeIcon size={11} />`). */\r\n icon: React.ReactNode\r\n}\r\n\r\n/** Marca pequena antes da etiqueta de um FormField, a dizer de onde veio o valor. */\r\nexport function FieldMark({ title, icon }: FieldMarkProps) {\r\n return (\r\n <span\r\n role=\"img\"\r\n aria-label={title}\r\n title={title}\r\n className=\"inline-flex h-[17px] w-[17px] flex-none cursor-help items-center justify-center rounded-[5px] border border-primary/20 bg-primary/5 text-primary\"\r\n >\r\n {icon}\r\n </span>\r\n )\r\n}\r\n"],"names":[],"mappings":";;;;AAgCA,MAAM,OAAO;AACb,MAAM,WAAW;AASjB,SAAwB,UAAU,EAAE,OAAO,UAAU,MAAM,OAAO,WAAW,OAAO,KAAK,MAAM,UAAU,SAAA,GAA4B;AACnI,QAAM,KAAK,MAAA;AACX,QAAM,EAAE,QAAA,IAAY,YAAA;AACpB,QAAM,aAAa,MAAM,SAAS,MAAM,KAAK;AAC7C,QAAM,QAAQ,KAAK,IAAI,aAAa,GAAG,UAAU;AAGjD,QAAM,UAAU,QAAQ,YAAY;AAEpC,SACE,qBAAC,aAAa,UAAb,EAAsB,OAAO,EAAE,IAAI,YAClC,UAAA;AAAA,IAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,WAAW,+HAA+H,UAAU,gBAAgB,EAAE,IAAI,WAAW,wBAAwB,EAAE;AAAA,QAG/M,UAAA;AAAA,UAAA,qBAAC,OAAA,EAAI,WAAU,4DACZ,UAAA;AAAA,YAAA,QAAQ,KACP,oBAAC,QAAA,EAAK,WAAU,iEAAgE,OAAO,EAAE,OAAO,QAAQ,QAAQ,QAAQ,KAAK,YAC1H,UAAA,OACH;AAAA,YAED,SAAS,oBAAC,QAAA,EAAK,WAAU,oBAAoB,UAAA,OAAM;AAAA,YAGpD;AAAA,cAAC;AAAA,cAAA;AAAA,gBACC,SAAS;AAAA,gBACT,WAAU;AAAA,gBAET,UAAA;AAAA,kBAAA;AAAA,kBACA,YACC,oBAAC,QAAA,EAAK,eAAW,MAAC,WAAU,2BAA0B,UAAA,IAAA,CAEtD;AAAA,gBAAA;AAAA,cAAA;AAAA,YAAA;AAAA,UAEJ,GACF;AAAA,UACC,QAAQ,oBAAC,QAAA,EAAK,WAAU,oCAAoC,UAAA,KAAA,CAAK;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA;AAAA,IAEpE,qBAAC,OAAA,EAAI,WAAW,WAAW,UAAU,uCAAuC,EAAE,IAAI,MAAM,8BAA8B,EAAE,IACrH,UAAA;AAAA,MAAA;AAAA,MACA;AAAA,IAAA,EAAA,CACH;AAAA,EAAA,GACF;AAEJ;AAUO,SAAS,UAAU,EAAE,OAAO,QAAwB;AACzD,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,MAAK;AAAA,MACL,cAAY;AAAA,MACZ;AAAA,MACA,WAAU;AAAA,MAET,UAAA;AAAA,IAAA;AAAA,EAAA;AAGP;"}
|
|
@@ -8,6 +8,19 @@ const WIDTHS = { sm: "max-w-dialog-sm", md: "max-w-dialog-md", lg: "max-w-dialog
|
|
|
8
8
|
const abertas = [];
|
|
9
9
|
const silenciados = /* @__PURE__ */ new Set();
|
|
10
10
|
const doTopo = () => abertas[abertas.length - 1];
|
|
11
|
+
let bloqueios = 0;
|
|
12
|
+
let overflowAntes = "";
|
|
13
|
+
function bloquearScroll() {
|
|
14
|
+
const html = document.documentElement;
|
|
15
|
+
if (bloqueios++ === 0) {
|
|
16
|
+
overflowAntes = html.style.overflow;
|
|
17
|
+
html.style.overflow = "hidden";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function libertarScroll() {
|
|
21
|
+
if (bloqueios === 0) return;
|
|
22
|
+
if (--bloqueios === 0) document.documentElement.style.overflow = overflowAntes;
|
|
23
|
+
}
|
|
11
24
|
function silenciar() {
|
|
12
25
|
for (const el of silenciados) el.removeAttribute("inert");
|
|
13
26
|
silenciados.clear();
|
|
@@ -71,12 +84,8 @@ function Dialog({
|
|
|
71
84
|
}, [open, root, depth]);
|
|
72
85
|
useEffect(() => {
|
|
73
86
|
if (!open) return;
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
html.style.overflow = "hidden";
|
|
77
|
-
return () => {
|
|
78
|
-
html.style.overflow = before;
|
|
79
|
-
};
|
|
87
|
+
bloquearScroll();
|
|
88
|
+
return libertarScroll;
|
|
80
89
|
}, [open]);
|
|
81
90
|
const noFundo = (e) => e.target === fundo.current || e.target === e.currentTarget;
|
|
82
91
|
return createPortal(
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Dialog.js","sources":["../../../../../../src/v1/components/overlays/Dialog/Dialog.tsx"],"sourcesContent":["import React, { createContext, useContext, useEffect, useId, useRef, useState } from 'react'\r\nimport { createPortal } from 'react-dom'\r\n\r\nconst DialogContainerContext = createContext<HTMLElement | null>(null)\r\nconst DialogDepthContext = createContext(0)\r\n\r\n/**\r\n * O elemento da janela, para usar como destino de `createPortal` (ex: avisos,\r\n * confirmações, sobreposições) quando o conteúdo deve ficar junto dela. Um\r\n * portal para `document.body` também funciona, desde que fique por cima\r\n * (`z-index` acima de 200). Fora de um Dialog devolve `null`.\r\n *\r\n * `createPortal(conteudo, useDialogContainer() ?? document.body)`\r\n */\r\nexport const useDialogContainer = () => useContext(DialogContainerContext)\r\n\r\nexport interface DialogProps {\r\n /** Aberta ou fechada. A app guarda este estado. */\r\n open: boolean\r\n /** Pedido para fechar (×, Esc ou clique fora): a app põe `open` a `false`. */\r\n onClose: () => void\r\n /** Normalmente texto; aceita JSX (ex: um nome em negrito). */\r\n title: React.ReactNode\r\n /** Linha por baixo do título (ex: nome · departamento). */\r\n subtitle?: React.ReactNode\r\n /** Botões no rodapé, sempre visíveis (ex: Confirmar, Fechar). */\r\n footer?: React.ReactNode\r\n /**\r\n * Largura máxima: `sm` 480px, `md` 720px, `lg` 1000px, `xl` até 1600px (95% do ecrã, para janelas com\r\n * mapa e tabela lado a lado). Nunca passa o ecrã.\r\n */\r\n size?: 'sm' | 'md' | 'lg' | 'xl'\r\n /**\r\n * Altura: `auto` (por omissão) cresce com o conteúdo; `full` ocupa sempre a\r\n * altura disponível (útil quando o conteúdo muda de tamanho e a janela não\r\n * deve saltar). Em ambos, se o conteúdo não couber, o corpo faz scroll.\r\n */\r\n height?: 'auto' | 'full'\r\n /** Fechar ao clicar fora da janela. Por omissão sim; num formulário longo pode valer a pena desligar. */\r\n closeOnBackdrop?: boolean\r\n /** Nome do botão ×, para leitores de ecrã. Por omissão: \"Fechar\". */\r\n closeLabel?: string\r\n /**\r\n * Corpo sem margens e sem scroll: o conteúdo ocupa-o todo e gere o seu próprio\r\n * scroll (ex: passos que deslizam, rodapé próprio de cada passo).\r\n */\r\n flush?: boolean\r\n children: React.ReactNode\r\n}\r\n\r\nconst WIDTHS = { sm: 'max-w-dialog-sm', md: 'max-w-dialog-md', lg: 'max-w-dialog-lg', xl: 'max-w-dialog-xl' }\r\n\r\ninterface Aberta {\r\n root: HTMLElement\r\n janela: HTMLElement | null\r\n depth: number\r\n}\r\n\r\nconst abertas: Aberta[] = []\r\nconst silenciados = new Set<Element>()\r\n\r\nconst doTopo = () => abertas[abertas.length - 1]\r\n\r\nfunction silenciar() {\r\n for (const el of silenciados) el.removeAttribute('inert')\r\n silenciados.clear()\r\n const topo = doTopo()\r\n if (!topo) return\r\n for (const el of Array.from(document.body.children)) {\r\n if (el === topo.root || el.hasAttribute('inert')) continue\r\n el.setAttribute('inert', '')\r\n silenciados.add(el)\r\n }\r\n}\r\n\r\n// Uma janela aberta dentro de outra fica sempre por cima dela, mesmo que abram juntas.\r\nfunction empilhar(aberta: Aberta) {\r\n const acima = abertas.findIndex((a) => a.depth > aberta.depth)\r\n if (acima === -1) abertas.push(aberta)\r\n else abertas.splice(acima, 0, aberta)\r\n}\r\n\r\n/**\r\n * Janela por cima da página: cabeçalho navy com título, corpo que faz scroll e\r\n * rodapé com os botões ([Button](?path=/docs/v1-primitives-buttons-button--docs)).\r\n *\r\n * É a janela modal da V1 (substitui o `Modal` legacy).\r\n *\r\n * Fundo escurecido, Esc para fechar e, ao fechar, o foco volta ao sítio onde\r\n * estava. Enquanto está aberta, o resto da página fica\r\n * [inert](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/inert):\r\n * nem rato, nem teclado, nem leitores de ecrã lhe chegam. Os portais abertos\r\n * depois dela (seletores, confirmações) continuam a responder.\r\n *\r\n * O conteúdo fica montado mesmo com a janela fechada, para poder carregar antes\r\n * de ela abrir.\r\n *\r\n * O tamanho adapta-se ao ecrã só com CSS: nunca passa a largura nem a altura\r\n * da janela do browser (com uma margem), e acompanha-a quando é\r\n * redimensionada. O que não couber faz scroll no corpo; o cabeçalho e o\r\n * rodapé ficam sempre visíveis.\r\n *\r\n * **Exemplo completo:** [Formulário em modal](?path=/docs/v1-patterns-forms-formulário-em-modal--docs).\r\n */\r\nexport default function Dialog({\r\n open,\r\n onClose,\r\n title,\r\n subtitle,\r\n footer,\r\n size = 'md',\r\n height = 'auto',\r\n closeOnBackdrop = true,\r\n closeLabel = 'Fechar',\r\n flush = false,\r\n children,\r\n}: DialogProps) {\r\n const [root, setRoot] = useState<HTMLDivElement | null>(null)\r\n const janela = useRef<HTMLDivElement>(null)\r\n const fundo = useRef<HTMLDivElement>(null)\r\n const titleId = useId()\r\n const depth = useContext(DialogDepthContext)\r\n const fechar = useRef(onClose)\r\n fechar.current = onClose\r\n // Clique fora: só conta se começou e acabou no fundo (arrastar a partir de um\r\n // campo para fora não fecha a janela).\r\n const pressedBackdrop = useRef(false)\r\n\r\n useEffect(() => {\r\n if (!open || !root) return\r\n const anterior = document.activeElement as HTMLElement | null\r\n const aberta = { root, janela: janela.current, depth }\r\n empilhar(aberta)\r\n silenciar()\r\n const topo = doTopo()\r\n const dentro = topo.janela?.contains(document.activeElement)\r\n if (!dentro) topo.janela?.focus()\r\n\r\n const onKeyDown = (e: KeyboardEvent) => {\r\n if (e.key !== 'Escape' || e.defaultPrevented) return\r\n if (doTopo() !== aberta) return\r\n fechar.current()\r\n }\r\n document.addEventListener('keydown', onKeyDown)\r\n\r\n return () => {\r\n document.removeEventListener('keydown', onKeyDown)\r\n abertas.splice(abertas.indexOf(aberta), 1)\r\n silenciar()\r\n const voltar = anterior && !anterior.closest('[inert]')\r\n if (voltar) anterior.focus()\r\n else doTopo()?.janela?.focus()\r\n }\r\n }, [open, root, depth])\r\n\r\n // A página por trás não faz scroll enquanto a janela está aberta.\r\n useEffect(() => {\r\n if (!open) return\r\n const html = document.documentElement\r\n const before = html.style.overflow\r\n html.style.overflow = 'hidden'\r\n return () => {\r\n html.style.overflow = before\r\n }\r\n }, [open])\r\n\r\n const noFundo = (e: React.SyntheticEvent) => e.target === fundo.current || e.target === e.currentTarget\r\n\r\n return createPortal(\r\n <div\r\n ref={setRoot}\r\n hidden={!open}\r\n onPointerDown={(e) => {\r\n pressedBackdrop.current = noFundo(e)\r\n }}\r\n onClick={(e) => {\r\n if (closeOnBackdrop && pressedBackdrop.current && noFundo(e)) onClose()\r\n }}\r\n // No telemóvel, a janela de altura total ocupa o ecrã todo (sem margem nem cantos): o cabeçalho, com o ×, fica sempre à vista.\r\n className={`fixed inset-0 z-[200] items-center justify-center p-dialog-margin ${height === 'full' ? 'max-sm:p-0' : ''} ${open ? 'flex' : 'hidden'}`}\r\n >\r\n <div ref={fundo} className=\"absolute inset-0 bg-black/40 motion-safe:animate-backdrop-in\" />\r\n <div\r\n ref={janela}\r\n role=\"dialog\"\r\n aria-modal=\"true\"\r\n aria-labelledby={titleId}\r\n tabIndex={-1}\r\n className={\r\n // No escuro a sombra não se vê: a linha (a mesma dos menus) separa a janela do fundo.\r\n `relative flex max-h-full w-full ${WIDTHS[size]} flex-col overflow-hidden rounded-2xl bg-card text-foreground shadow-modal outline-none dark:border dark:border-slate-700 ` +\r\n `motion-safe:animate-dialog-in ${height === 'full' ? 'h-full max-sm:rounded-none max-sm:border-0' : ''}`\r\n }\r\n >\r\n <header className=\"flex flex-none items-center justify-between gap-3 bg-gradient-to-br from-primary to-primary-strong px-5 py-3.5\">\r\n <div className=\"min-w-0\">\r\n <h2 id={titleId} className=\"m-0 truncate text-title font-bold text-primary-foreground\">\r\n {title}\r\n </h2>\r\n {subtitle && <p className=\"m-0 mt-0.5 truncate text-caption text-primary-foreground/75\">{subtitle}</p>}\r\n </div>\r\n <button\r\n type=\"button\"\r\n aria-label={closeLabel}\r\n title={closeLabel}\r\n onClick={onClose}\r\n className=\"flex h-7 w-7 flex-none cursor-pointer items-center justify-center rounded-md border border-primary-foreground/25 bg-primary-foreground/15 text-base leading-none text-primary-foreground transition-colors hover:bg-primary-foreground/25 focus-visible:ring-2 focus-visible:ring-primary-foreground/60 focus-visible:outline-none\"\r\n >\r\n ×\r\n </button>\r\n </header>\r\n\r\n <DialogContainerContext.Provider value={root}>\r\n <DialogDepthContext.Provider value={depth + 1}>\r\n <div className={flush ? 'flex min-h-0 flex-1 flex-col overflow-hidden bg-canvas' : 'min-h-0 flex-1 overflow-y-auto bg-canvas px-5 py-4'}>{children}</div>\r\n\r\n {footer && <footer className=\"flex flex-none flex-wrap justify-end gap-2 border-t border-border bg-muted/40 px-5 py-[18px]\">{footer}</footer>}\r\n </DialogDepthContext.Provider>\r\n </DialogContainerContext.Provider>\r\n </div>\r\n </div>,\r\n document.body,\r\n )\r\n}\r\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Dialog.js","sources":["../../../../../../src/v1/components/overlays/Dialog/Dialog.tsx"],"sourcesContent":["import React, { createContext, useContext, useEffect, useId, useRef, useState } from 'react'\r\nimport { createPortal } from 'react-dom'\r\n\r\nconst DialogContainerContext = createContext<HTMLElement | null>(null)\r\nconst DialogDepthContext = createContext(0)\r\n\r\n/**\r\n * O elemento da janela, para usar como destino de `createPortal` (ex: avisos,\r\n * confirmações, sobreposições) quando o conteúdo deve ficar junto dela. Um\r\n * portal para `document.body` também funciona, desde que fique por cima\r\n * (`z-index` acima de 200). Fora de um Dialog devolve `null`.\r\n *\r\n * `createPortal(conteudo, useDialogContainer() ?? document.body)`\r\n */\r\nexport const useDialogContainer = () => useContext(DialogContainerContext)\r\n\r\nexport interface DialogProps {\r\n /** Aberta ou fechada. A app guarda este estado. */\r\n open: boolean\r\n /** Pedido para fechar (×, Esc ou clique fora): a app põe `open` a `false`. */\r\n onClose: () => void\r\n /** Normalmente texto; aceita JSX (ex: um nome em negrito). */\r\n title: React.ReactNode\r\n /** Linha por baixo do título (ex: nome · departamento). */\r\n subtitle?: React.ReactNode\r\n /** Botões no rodapé, sempre visíveis (ex: Confirmar, Fechar). */\r\n footer?: React.ReactNode\r\n /**\r\n * Largura máxima: `sm` 480px, `md` 720px, `lg` 1000px, `xl` até 1600px (95% do ecrã, para janelas com\r\n * mapa e tabela lado a lado). Nunca passa o ecrã.\r\n */\r\n size?: 'sm' | 'md' | 'lg' | 'xl'\r\n /**\r\n * Altura: `auto` (por omissão) cresce com o conteúdo; `full` ocupa sempre a\r\n * altura disponível (útil quando o conteúdo muda de tamanho e a janela não\r\n * deve saltar). Em ambos, se o conteúdo não couber, o corpo faz scroll.\r\n */\r\n height?: 'auto' | 'full'\r\n /** Fechar ao clicar fora da janela. Por omissão sim; num formulário longo pode valer a pena desligar. */\r\n closeOnBackdrop?: boolean\r\n /** Nome do botão ×, para leitores de ecrã. Por omissão: \"Fechar\". */\r\n closeLabel?: string\r\n /**\r\n * Corpo sem margens e sem scroll: o conteúdo ocupa-o todo e gere o seu próprio\r\n * scroll (ex: passos que deslizam, rodapé próprio de cada passo).\r\n */\r\n flush?: boolean\r\n children: React.ReactNode\r\n}\r\n\r\nconst WIDTHS = { sm: 'max-w-dialog-sm', md: 'max-w-dialog-md', lg: 'max-w-dialog-lg', xl: 'max-w-dialog-xl' }\r\n\r\ninterface Aberta {\r\n root: HTMLElement\r\n janela: HTMLElement | null\r\n depth: number\r\n}\r\n\r\nconst abertas: Aberta[] = []\r\nconst silenciados = new Set<Element>()\r\n\r\nconst doTopo = () => abertas[abertas.length - 1]\r\n\r\n// Scroll da página bloqueado enquanto houver pelo menos uma janela aberta. Um contador\r\n// partilhado, e não o \"antes\" de cada janela: com duas abertas que fecham fora de ordem\r\n// (ex: a de baixo desmonta primeiro), a última a fechar repunha o `hidden` e a página\r\n// ficava sem scroll até recarregar.\r\nlet bloqueios = 0\r\nlet overflowAntes = ''\r\n\r\nfunction bloquearScroll() {\r\n const html = document.documentElement\r\n if (bloqueios++ === 0) {\r\n overflowAntes = html.style.overflow\r\n html.style.overflow = 'hidden'\r\n }\r\n}\r\n\r\nfunction libertarScroll() {\r\n if (bloqueios === 0) return\r\n if (--bloqueios === 0) document.documentElement.style.overflow = overflowAntes\r\n}\r\n\r\nfunction silenciar() {\r\n for (const el of silenciados) el.removeAttribute('inert')\r\n silenciados.clear()\r\n const topo = doTopo()\r\n if (!topo) return\r\n for (const el of Array.from(document.body.children)) {\r\n if (el === topo.root || el.hasAttribute('inert')) continue\r\n el.setAttribute('inert', '')\r\n silenciados.add(el)\r\n }\r\n}\r\n\r\n// Uma janela aberta dentro de outra fica sempre por cima dela, mesmo que abram juntas.\r\nfunction empilhar(aberta: Aberta) {\r\n const acima = abertas.findIndex((a) => a.depth > aberta.depth)\r\n if (acima === -1) abertas.push(aberta)\r\n else abertas.splice(acima, 0, aberta)\r\n}\r\n\r\n/**\r\n * Janela por cima da página: cabeçalho navy com título, corpo que faz scroll e\r\n * rodapé com os botões ([Button](?path=/docs/v1-primitives-buttons-button--docs)).\r\n *\r\n * É a janela modal da V1 (substitui o `Modal` legacy).\r\n *\r\n * Fundo escurecido, Esc para fechar e, ao fechar, o foco volta ao sítio onde\r\n * estava. Enquanto está aberta, o resto da página fica\r\n * [inert](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/inert):\r\n * nem rato, nem teclado, nem leitores de ecrã lhe chegam. Os portais abertos\r\n * depois dela (seletores, confirmações) continuam a responder.\r\n *\r\n * O conteúdo fica montado mesmo com a janela fechada, para poder carregar antes\r\n * de ela abrir.\r\n *\r\n * O tamanho adapta-se ao ecrã só com CSS: nunca passa a largura nem a altura\r\n * da janela do browser (com uma margem), e acompanha-a quando é\r\n * redimensionada. O que não couber faz scroll no corpo; o cabeçalho e o\r\n * rodapé ficam sempre visíveis.\r\n *\r\n * **Exemplo completo:** [Formulário em modal](?path=/docs/v1-patterns-forms-formulário-em-modal--docs).\r\n */\r\nexport default function Dialog({\r\n open,\r\n onClose,\r\n title,\r\n subtitle,\r\n footer,\r\n size = 'md',\r\n height = 'auto',\r\n closeOnBackdrop = true,\r\n closeLabel = 'Fechar',\r\n flush = false,\r\n children,\r\n}: DialogProps) {\r\n const [root, setRoot] = useState<HTMLDivElement | null>(null)\r\n const janela = useRef<HTMLDivElement>(null)\r\n const fundo = useRef<HTMLDivElement>(null)\r\n const titleId = useId()\r\n const depth = useContext(DialogDepthContext)\r\n const fechar = useRef(onClose)\r\n fechar.current = onClose\r\n // Clique fora: só conta se começou e acabou no fundo (arrastar a partir de um\r\n // campo para fora não fecha a janela).\r\n const pressedBackdrop = useRef(false)\r\n\r\n useEffect(() => {\r\n if (!open || !root) return\r\n const anterior = document.activeElement as HTMLElement | null\r\n const aberta = { root, janela: janela.current, depth }\r\n empilhar(aberta)\r\n silenciar()\r\n const topo = doTopo()\r\n const dentro = topo.janela?.contains(document.activeElement)\r\n if (!dentro) topo.janela?.focus()\r\n\r\n const onKeyDown = (e: KeyboardEvent) => {\r\n if (e.key !== 'Escape' || e.defaultPrevented) return\r\n if (doTopo() !== aberta) return\r\n fechar.current()\r\n }\r\n document.addEventListener('keydown', onKeyDown)\r\n\r\n return () => {\r\n document.removeEventListener('keydown', onKeyDown)\r\n abertas.splice(abertas.indexOf(aberta), 1)\r\n silenciar()\r\n const voltar = anterior && !anterior.closest('[inert]')\r\n if (voltar) anterior.focus()\r\n else doTopo()?.janela?.focus()\r\n }\r\n }, [open, root, depth])\r\n\r\n // A página por trás não faz scroll enquanto a janela está aberta.\r\n useEffect(() => {\r\n if (!open) return\r\n bloquearScroll()\r\n return libertarScroll\r\n }, [open])\r\n\r\n const noFundo = (e: React.SyntheticEvent) => e.target === fundo.current || e.target === e.currentTarget\r\n\r\n return createPortal(\r\n <div\r\n ref={setRoot}\r\n hidden={!open}\r\n onPointerDown={(e) => {\r\n pressedBackdrop.current = noFundo(e)\r\n }}\r\n onClick={(e) => {\r\n if (closeOnBackdrop && pressedBackdrop.current && noFundo(e)) onClose()\r\n }}\r\n // No telemóvel, a janela de altura total ocupa o ecrã todo (sem margem nem cantos): o cabeçalho, com o ×, fica sempre à vista.\r\n className={`fixed inset-0 z-[200] items-center justify-center p-dialog-margin ${height === 'full' ? 'max-sm:p-0' : ''} ${open ? 'flex' : 'hidden'}`}\r\n >\r\n <div ref={fundo} className=\"absolute inset-0 bg-black/40 motion-safe:animate-backdrop-in\" />\r\n <div\r\n ref={janela}\r\n role=\"dialog\"\r\n aria-modal=\"true\"\r\n aria-labelledby={titleId}\r\n tabIndex={-1}\r\n className={\r\n // No escuro a sombra não se vê: a linha (a mesma dos menus) separa a janela do fundo.\r\n `relative flex max-h-full w-full ${WIDTHS[size]} flex-col overflow-hidden rounded-2xl bg-card text-foreground shadow-modal outline-none dark:border dark:border-slate-700 ` +\r\n `motion-safe:animate-dialog-in ${height === 'full' ? 'h-full max-sm:rounded-none max-sm:border-0' : ''}`\r\n }\r\n >\r\n <header className=\"flex flex-none items-center justify-between gap-3 bg-gradient-to-br from-primary to-primary-strong px-5 py-3.5\">\r\n <div className=\"min-w-0\">\r\n <h2 id={titleId} className=\"m-0 truncate text-title font-bold text-primary-foreground\">\r\n {title}\r\n </h2>\r\n {subtitle && <p className=\"m-0 mt-0.5 truncate text-caption text-primary-foreground/75\">{subtitle}</p>}\r\n </div>\r\n <button\r\n type=\"button\"\r\n aria-label={closeLabel}\r\n title={closeLabel}\r\n onClick={onClose}\r\n className=\"flex h-7 w-7 flex-none cursor-pointer items-center justify-center rounded-md border border-primary-foreground/25 bg-primary-foreground/15 text-base leading-none text-primary-foreground transition-colors hover:bg-primary-foreground/25 focus-visible:ring-2 focus-visible:ring-primary-foreground/60 focus-visible:outline-none\"\r\n >\r\n ×\r\n </button>\r\n </header>\r\n\r\n <DialogContainerContext.Provider value={root}>\r\n <DialogDepthContext.Provider value={depth + 1}>\r\n <div className={flush ? 'flex min-h-0 flex-1 flex-col overflow-hidden bg-canvas' : 'min-h-0 flex-1 overflow-y-auto bg-canvas px-5 py-4'}>{children}</div>\r\n\r\n {footer && <footer className=\"flex flex-none flex-wrap justify-end gap-2 border-t border-border bg-muted/40 px-5 py-[18px]\">{footer}</footer>}\r\n </DialogDepthContext.Provider>\r\n </DialogContainerContext.Provider>\r\n </div>\r\n </div>,\r\n document.body,\r\n )\r\n}\r\n"],"names":[],"mappings":"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"}
|
|
@@ -14,7 +14,8 @@ const SIZES = {
|
|
|
14
14
|
md: "h-7 w-7 text-[10px]",
|
|
15
15
|
lg: "h-8 w-8 text-[11px]"
|
|
16
16
|
};
|
|
17
|
-
const
|
|
17
|
+
const INITIALS_COLORS = "bg-gradient-to-br from-avatar-from to-avatar-to text-avatar-fg";
|
|
18
|
+
const INITIALS = `rounded-full font-black ${INITIALS_COLORS}`;
|
|
18
19
|
function Avatar({
|
|
19
20
|
name,
|
|
20
21
|
photo,
|
|
@@ -95,7 +96,7 @@ function Avatar({
|
|
|
95
96
|
style: { position: "fixed", top: pos.top, left: pos.left, zIndex: 9999, width: 200 },
|
|
96
97
|
className: "pointer-events-none overflow-hidden rounded-2xl border border-border bg-card shadow-2xl ring-4 ring-white dark:border-slate-700 dark:bg-slate-900 dark:ring-slate-800",
|
|
97
98
|
children: [
|
|
98
|
-
/* @__PURE__ */ jsx("div", { className: "h-48 w-48 overflow-hidden rounded-xl", children: large && !failed ? /* @__PURE__ */ jsx("img", { src: large, alt: name ?? "", className: "h-full w-full object-cover", onError: () => setFailed(true) }) : /* @__PURE__ */ jsx("div", { className:
|
|
99
|
+
/* @__PURE__ */ jsx("div", { className: "h-48 w-48 overflow-hidden rounded-xl", children: large && !failed ? /* @__PURE__ */ jsx("img", { src: large, alt: name ?? "", className: "h-full w-full object-cover", onError: () => setFailed(true) }) : /* @__PURE__ */ jsx("div", { className: `flex h-full w-full items-center justify-center text-4xl font-black ${INITIALS_COLORS}`, children: initials(name) }) }),
|
|
99
100
|
/* @__PURE__ */ jsxs("div", { className: "space-y-1 px-3 py-2.5", children: [
|
|
100
101
|
name && /* @__PURE__ */ jsx("p", { className: "text-[12px] leading-tight font-black tracking-tight break-words text-foreground", children: name }),
|
|
101
102
|
email && /* @__PURE__ */ jsx("p", { className: "truncate text-[10px] text-muted-foreground", children: email }),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Avatar.js","sources":["../../../../../../src/v1/primitives/display/Avatar/Avatar.tsx"],"sourcesContent":["import React, { useEffect, useRef, useState } from 'react'\r\nimport { createPortal } from 'react-dom'\r\n\r\n/** Iniciais a partir do nome completo (primeira + última). Sem nome: \"?\". */\r\nexport function initials(fullName?: string | null): string {\r\n const parts = (fullName ?? '').trim().split(/\\s+/).filter(Boolean)\r\n if (parts.length === 0) return '?'\r\n const first = parts[0]!.charAt(0)\r\n const last = parts.length > 1 ? parts[parts.length - 1]!.charAt(0) : ''\r\n return (first + last).toUpperCase()\r\n}\r\n\r\n/** `none`: sem tamanho nem cores das iniciais — quem usa põe as classes (ex: o UserAvatar da Legacy). */\r\nexport type AvatarSize = 'sm' | 'md' | 'lg' | 'none'\r\n\r\nexport interface AvatarProps {\r\n name?: string | null\r\n /** URL da fotografia. Sem ela (ou se falhar) ficam as iniciais. */\r\n photo?: string | null\r\n /** Fotografia maior para o cartão (por omissão, a mesma). */\r\n photoLarge?: string | null\r\n email?: string | null\r\n company?: string | null\r\n department?: string | null\r\n jobTitle?: string | null\r\n /** `sm` 24px (tabelas e listas), `md` 28px, `lg` 32px (formulários, cabeçalhos). */\r\n size?: AvatarSize\r\n /** Cartão com a fotografia grande e os dados ao passar o rato (no toque, ao tocar). */\r\n card?: boolean\r\n className?: string\r\n}\r\n\r\nconst SIZES: Record<AvatarSize, string> = {\r\n none: '',\r\n sm: 'h-6 w-6 text-[9px]',\r\n md: 'h-7 w-7 text-[10px]',\r\n lg: 'h-8 w-8 text-[11px]',\r\n}\r\n\r\n// Iniciais (sem fotografia): o mesmo azul em todos os portais.\r\nconst INITIALS = 'rounded-full bg-gradient-to-br from-sky-400 to-sky-600 font-black text-white'\r\n\r\n/**\r\n * A fotografia de uma pessoa (ou as iniciais), redonda. Com `card`, mostra a\r\n * fotografia grande, o email, a empresa, o departamento e a função ao passar o rato;\r\n * no toque abre e fecha ao tocar, sem passar o toque à linha por baixo.\r\n *\r\n * Para uma pessoa com o nome ao lado (ex: numa tabela), usar o `Person`.\r\n */\r\nexport default function Avatar({\r\n name,\r\n photo,\r\n photoLarge,\r\n email,\r\n company,\r\n department,\r\n jobTitle,\r\n size = 'sm',\r\n card = true,\r\n className = '',\r\n}: AvatarProps) {\r\n const ref = useRef<HTMLSpanElement>(null)\r\n const [pos, setPos] = useState<{ top: number; left: number } | null>(null)\r\n const [pinned, setPinned] = useState(false)\r\n const [failed, setFailed] = useState(false)\r\n const pointer = useRef('mouse')\r\n const large = photoLarge ?? photo\r\n\r\n // Uma falha (ex: API em baixo) não pode marcar \"sem foto\" para sempre: em listas a\r\n // mesma instância passa a ser outra pessoa.\r\n useEffect(() => setFailed(false), [photo])\r\n\r\n const open = () => {\r\n if (!card || !ref.current) return\r\n const r = ref.current.getBoundingClientRect()\r\n const W = 200\r\n const H = email || company || jobTitle || department ? 280 : 196\r\n let left = r.right + 10\r\n let top = r.top + r.height / 2 - H / 2\r\n if (left + W > window.innerWidth - 8) left = r.left - W - 10\r\n if (top + H > window.innerHeight - 8) top = window.innerHeight - H - 8\r\n if (top < 8) top = 8\r\n setPos({ top, left })\r\n }\r\n const close = () => {\r\n setPos(null)\r\n setPinned(false)\r\n }\r\n\r\n // Aberto por toque: fecha ao tocar fora, ao deslocar ou ao redimensionar.\r\n useEffect(() => {\r\n if (!pinned) return\r\n const dismiss = (e: Event) => {\r\n if (e.type === 'pointerdown' && ref.current?.contains(e.target as Node)) return\r\n close()\r\n }\r\n document.addEventListener('pointerdown', dismiss)\r\n window.addEventListener('scroll', dismiss, true)\r\n window.addEventListener('resize', dismiss)\r\n return () => {\r\n document.removeEventListener('pointerdown', dismiss)\r\n window.removeEventListener('scroll', dismiss, true)\r\n window.removeEventListener('resize', dismiss)\r\n }\r\n }, [pinned])\r\n\r\n const showPhoto = !!photo && !failed\r\n\r\n return (\r\n <>\r\n <span\r\n ref={ref}\r\n className={`inline-flex shrink-0 items-center justify-center overflow-hidden ${SIZES[size]} ${showPhoto ? 'rounded-full' : size === 'none' ? '' : INITIALS} ${card ? 'cursor-pointer' : ''} ${className}`}\r\n onPointerEnter={(e) => e.pointerType === 'mouse' && open()}\r\n onPointerLeave={(e) => e.pointerType === 'mouse' && !pinned && close()}\r\n onPointerDown={(e) => {\r\n pointer.current = e.pointerType\r\n }}\r\n onClick={(e) => {\r\n if (!card || pointer.current === 'mouse') return\r\n e.preventDefault()\r\n e.stopPropagation()\r\n if (pinned) return close()\r\n open()\r\n setPinned(true)\r\n }}\r\n >\r\n {showPhoto ? (\r\n <img src={photo!} alt={name ?? ''} className=\"h-full w-full object-cover\" onError={() => setFailed(true)} />\r\n ) : (\r\n initials(name)\r\n )}\r\n </span>\r\n {pos &&\r\n createPortal(\r\n <div\r\n style={{ position: 'fixed', top: pos.top, left: pos.left, zIndex: 9999, width: 200 }}\r\n className=\"pointer-events-none overflow-hidden rounded-2xl border border-border bg-card shadow-2xl ring-4 ring-white dark:border-slate-700 dark:bg-slate-900 dark:ring-slate-800\"\r\n >\r\n <div className=\"h-48 w-48 overflow-hidden rounded-xl\">\r\n {large && !failed ? (\r\n <img src={large} alt={name ?? ''} className=\"h-full w-full object-cover\" onError={() => setFailed(true)} />\r\n ) : (\r\n <div className=\"flex h-full w-full items-center justify-center bg-gradient-to-br from-sky-400 to-sky-600 text-4xl font-black text-white\">\r\n {initials(name)}\r\n </div>\r\n )}\r\n </div>\r\n <div className=\"space-y-1 px-3 py-2.5\">\r\n {name && <p className=\"text-[12px] leading-tight font-black tracking-tight break-words text-foreground\">{name}</p>}\r\n {email && <p className=\"truncate text-[10px] text-muted-foreground\">{email}</p>}\r\n {(company || department || jobTitle) && (\r\n <div className=\"mt-1 space-y-0.5 border-t border-border pt-1 dark:border-slate-700\">\r\n {company && <p className=\"truncate text-[10px] font-semibold text-foreground\">{company}</p>}\r\n {department && <p className=\"truncate text-[10px] text-muted-foreground\">{department}</p>}\r\n {jobTitle && <p className=\"truncate text-[10px] text-muted-foreground italic\">{jobTitle}</p>}\r\n </div>\r\n )}\r\n </div>\r\n </div>,\r\n document.body,\r\n )}\r\n </>\r\n )\r\n}\r\n\r\nexport { Avatar }\r\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Avatar.js","sources":["../../../../../../src/v1/primitives/display/Avatar/Avatar.tsx"],"sourcesContent":["import React, { useEffect, useRef, useState } from 'react'\r\nimport { createPortal } from 'react-dom'\r\n\r\n/** Iniciais a partir do nome completo (primeira + última). Sem nome: \"?\". */\r\nexport function initials(fullName?: string | null): string {\r\n const parts = (fullName ?? '').trim().split(/\\s+/).filter(Boolean)\r\n if (parts.length === 0) return '?'\r\n const first = parts[0]!.charAt(0)\r\n const last = parts.length > 1 ? parts[parts.length - 1]!.charAt(0) : ''\r\n return (first + last).toUpperCase()\r\n}\r\n\r\n/** `none`: sem tamanho nem cores das iniciais — quem usa põe as classes (ex: o UserAvatar da Legacy). */\r\nexport type AvatarSize = 'sm' | 'md' | 'lg' | 'none'\r\n\r\nexport interface AvatarProps {\r\n name?: string | null\r\n /** URL da fotografia. Sem ela (ou se falhar) ficam as iniciais. */\r\n photo?: string | null\r\n /** Fotografia maior para o cartão (por omissão, a mesma). */\r\n photoLarge?: string | null\r\n email?: string | null\r\n company?: string | null\r\n department?: string | null\r\n jobTitle?: string | null\r\n /** `sm` 24px (tabelas e listas), `md` 28px, `lg` 32px (formulários, cabeçalhos). */\r\n size?: AvatarSize\r\n /** Cartão com a fotografia grande e os dados ao passar o rato (no toque, ao tocar). */\r\n card?: boolean\r\n className?: string\r\n}\r\n\r\nconst SIZES: Record<AvatarSize, string> = {\r\n none: '',\r\n sm: 'h-6 w-6 text-[9px]',\r\n md: 'h-7 w-7 text-[10px]',\r\n lg: 'h-8 w-8 text-[11px]',\r\n}\r\n\r\n// Iniciais (sem fotografia): as cores vêm dos tokens --color-avatar-* (azul por omissão;\r\n// um portal pode sobrepô-los no seu CSS).\r\nconst INITIALS_COLORS = 'bg-gradient-to-br from-avatar-from to-avatar-to text-avatar-fg'\r\nconst INITIALS = `rounded-full font-black ${INITIALS_COLORS}`\r\n\r\n/**\r\n * A fotografia de uma pessoa (ou as iniciais), redonda. Com `card`, mostra a\r\n * fotografia grande, o email, a empresa, o departamento e a função ao passar o rato;\r\n * no toque abre e fecha ao tocar, sem passar o toque à linha por baixo.\r\n *\r\n * Para uma pessoa com o nome ao lado (ex: numa tabela), usar o `Person`.\r\n */\r\nexport default function Avatar({\r\n name,\r\n photo,\r\n photoLarge,\r\n email,\r\n company,\r\n department,\r\n jobTitle,\r\n size = 'sm',\r\n card = true,\r\n className = '',\r\n}: AvatarProps) {\r\n const ref = useRef<HTMLSpanElement>(null)\r\n const [pos, setPos] = useState<{ top: number; left: number } | null>(null)\r\n const [pinned, setPinned] = useState(false)\r\n const [failed, setFailed] = useState(false)\r\n const pointer = useRef('mouse')\r\n const large = photoLarge ?? photo\r\n\r\n // Uma falha (ex: API em baixo) não pode marcar \"sem foto\" para sempre: em listas a\r\n // mesma instância passa a ser outra pessoa.\r\n useEffect(() => setFailed(false), [photo])\r\n\r\n const open = () => {\r\n if (!card || !ref.current) return\r\n const r = ref.current.getBoundingClientRect()\r\n const W = 200\r\n const H = email || company || jobTitle || department ? 280 : 196\r\n let left = r.right + 10\r\n let top = r.top + r.height / 2 - H / 2\r\n if (left + W > window.innerWidth - 8) left = r.left - W - 10\r\n if (top + H > window.innerHeight - 8) top = window.innerHeight - H - 8\r\n if (top < 8) top = 8\r\n setPos({ top, left })\r\n }\r\n const close = () => {\r\n setPos(null)\r\n setPinned(false)\r\n }\r\n\r\n // Aberto por toque: fecha ao tocar fora, ao deslocar ou ao redimensionar.\r\n useEffect(() => {\r\n if (!pinned) return\r\n const dismiss = (e: Event) => {\r\n if (e.type === 'pointerdown' && ref.current?.contains(e.target as Node)) return\r\n close()\r\n }\r\n document.addEventListener('pointerdown', dismiss)\r\n window.addEventListener('scroll', dismiss, true)\r\n window.addEventListener('resize', dismiss)\r\n return () => {\r\n document.removeEventListener('pointerdown', dismiss)\r\n window.removeEventListener('scroll', dismiss, true)\r\n window.removeEventListener('resize', dismiss)\r\n }\r\n }, [pinned])\r\n\r\n const showPhoto = !!photo && !failed\r\n\r\n return (\r\n <>\r\n <span\r\n ref={ref}\r\n className={`inline-flex shrink-0 items-center justify-center overflow-hidden ${SIZES[size]} ${showPhoto ? 'rounded-full' : size === 'none' ? '' : INITIALS} ${card ? 'cursor-pointer' : ''} ${className}`}\r\n onPointerEnter={(e) => e.pointerType === 'mouse' && open()}\r\n onPointerLeave={(e) => e.pointerType === 'mouse' && !pinned && close()}\r\n onPointerDown={(e) => {\r\n pointer.current = e.pointerType\r\n }}\r\n onClick={(e) => {\r\n if (!card || pointer.current === 'mouse') return\r\n e.preventDefault()\r\n e.stopPropagation()\r\n if (pinned) return close()\r\n open()\r\n setPinned(true)\r\n }}\r\n >\r\n {showPhoto ? (\r\n <img src={photo!} alt={name ?? ''} className=\"h-full w-full object-cover\" onError={() => setFailed(true)} />\r\n ) : (\r\n initials(name)\r\n )}\r\n </span>\r\n {pos &&\r\n createPortal(\r\n <div\r\n style={{ position: 'fixed', top: pos.top, left: pos.left, zIndex: 9999, width: 200 }}\r\n className=\"pointer-events-none overflow-hidden rounded-2xl border border-border bg-card shadow-2xl ring-4 ring-white dark:border-slate-700 dark:bg-slate-900 dark:ring-slate-800\"\r\n >\r\n <div className=\"h-48 w-48 overflow-hidden rounded-xl\">\r\n {large && !failed ? (\r\n <img src={large} alt={name ?? ''} className=\"h-full w-full object-cover\" onError={() => setFailed(true)} />\r\n ) : (\r\n <div className={`flex h-full w-full items-center justify-center text-4xl font-black ${INITIALS_COLORS}`}>\r\n {initials(name)}\r\n </div>\r\n )}\r\n </div>\r\n <div className=\"space-y-1 px-3 py-2.5\">\r\n {name && <p className=\"text-[12px] leading-tight font-black tracking-tight break-words text-foreground\">{name}</p>}\r\n {email && <p className=\"truncate text-[10px] text-muted-foreground\">{email}</p>}\r\n {(company || department || jobTitle) && (\r\n <div className=\"mt-1 space-y-0.5 border-t border-border pt-1 dark:border-slate-700\">\r\n {company && <p className=\"truncate text-[10px] font-semibold text-foreground\">{company}</p>}\r\n {department && <p className=\"truncate text-[10px] text-muted-foreground\">{department}</p>}\r\n {jobTitle && <p className=\"truncate text-[10px] text-muted-foreground italic\">{jobTitle}</p>}\r\n </div>\r\n )}\r\n </div>\r\n </div>,\r\n document.body,\r\n )}\r\n </>\r\n )\r\n}\r\n\r\nexport { Avatar }\r\n"],"names":[],"mappings":"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"}
|