@proef/common-ui 2.3.0 → 2.6.0
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/legacy/components/molecules/DataTable/DataTable.js +195 -103
- package/dist/esm/legacy/components/molecules/DataTable/DataTable.js.map +1 -1
- package/dist/esm/v1/capabilities/overlay/useFloatingPanel.js +5 -2
- package/dist/esm/v1/capabilities/overlay/useFloatingPanel.js.map +1 -1
- package/dist/esm/v1/components/data/Pagination/Pagination.js +30 -26
- package/dist/esm/v1/components/data/Pagination/Pagination.js.map +1 -1
- package/dist/esm/v1/components/forms/FormField/FormField.js +3 -2
- package/dist/esm/v1/components/forms/FormField/FormField.js.map +1 -1
- package/dist/esm/v1/components/layout/AppShell/AppShell.js +8 -3
- package/dist/esm/v1/components/layout/AppShell/AppShell.js.map +1 -1
- package/dist/esm/v1/components/layout/Page/Page.js +1 -1
- package/dist/esm/v1/components/layout/Page/Page.js.map +1 -1
- package/dist/esm/v1/components/layout/PageHeader/PageHeader.js +21 -18
- package/dist/esm/v1/components/layout/PageHeader/PageHeader.js.map +1 -1
- package/dist/esm/v1/components/lists/ListFinder/ListFinder.js +16 -16
- package/dist/esm/v1/components/lists/ListFinder/ListFinder.js.map +1 -1
- package/dist/esm/v1/components/overlays/Dialog/Dialog.js +2 -2
- package/dist/esm/v1/components/overlays/Dialog/Dialog.js.map +1 -1
- package/dist/esm/v1/primitives/buttons/SegmentedControl/SegmentedControl.js +3 -3
- package/dist/esm/v1/primitives/buttons/SegmentedControl/SegmentedControl.js.map +1 -1
- package/dist/esm/v1/primitives/display/Kpi/Kpi.js +6 -3
- package/dist/esm/v1/primitives/display/Kpi/Kpi.js.map +1 -1
- package/dist/esm/v1/primitives/forms/SelectInput/SelectInput.js +28 -8
- package/dist/esm/v1/primitives/forms/SelectInput/SelectInput.js.map +1 -1
- package/dist/esm/v1/primitives/lists/ListItem/ListItem.js +5 -5
- package/dist/esm/v1/primitives/lists/ListItem/ListItem.js.map +1 -1
- package/dist/esm/v1/primitives/navigation/Tabs/Tabs.js +97 -36
- package/dist/esm/v1/primitives/navigation/Tabs/Tabs.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/types/legacy/components/molecules/DataTable/DataTable.d.ts +15 -1
- package/dist/types/v1/primitives/forms/SelectInput/SelectInput.d.ts +12 -0
- package/dist/types/v1/primitives/lists/ListItem/ListItem.d.ts +1 -1
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Fragment as Fragment$1 } from "react";
|
|
3
3
|
import Checkbox from "../../forms/Checkbox/Checkbox.js";
|
|
4
|
-
const LINHA_ESBATIDA = "bg-[linear-gradient(to_right,transparent,var(--color-slate-200)_15%,var(--color-slate-200)_85%,transparent)] bg-[length:100%_1px] bg-bottom bg-no-repeat last:bg-none";
|
|
4
|
+
const LINHA_ESBATIDA = "bg-[linear-gradient(to_right,transparent,var(--color-slate-200)_15%,var(--color-slate-200)_85%,transparent)] dark:bg-[linear-gradient(to_right,transparent,var(--color-slate-800)_15%,var(--color-slate-800)_85%,transparent)] bg-[length:100%_1px] bg-bottom bg-no-repeat last:bg-none";
|
|
5
5
|
const semAcento = (c) => c.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase();
|
|
6
6
|
function HighlightText({ text, terms }) {
|
|
7
7
|
const termos = (terms ?? []).map((t) => [...t].map(semAcento).join("")).filter(Boolean);
|
|
@@ -21,7 +21,7 @@ function HighlightText({ text, terms }) {
|
|
|
21
21
|
if (ultimo && ultimo.hit === marcado[i]) ultimo.txt += c;
|
|
22
22
|
else partes.push({ txt: c, hit: marcado[i] });
|
|
23
23
|
});
|
|
24
|
-
return /* @__PURE__ */ jsx(Fragment, { children: partes.map((p, i) => p.hit ? /* @__PURE__ */ jsx("mark", { className: "rounded-sm bg-amber-200 font-bold text-amber-900", children: p.txt }, i) : /* @__PURE__ */ jsx(Fragment$1, { children: p.txt }, i)) });
|
|
24
|
+
return /* @__PURE__ */ jsx(Fragment, { children: partes.map((p, i) => p.hit ? /* @__PURE__ */ jsx("mark", { className: "rounded-sm bg-amber-200 font-bold text-amber-900 dark:bg-amber-500/30 dark:text-amber-100", children: p.txt }, i) : /* @__PURE__ */ jsx(Fragment$1, { children: p.txt }, i)) });
|
|
25
25
|
}
|
|
26
26
|
function ListItem({
|
|
27
27
|
title,
|
|
@@ -38,7 +38,7 @@ function ListItem({
|
|
|
38
38
|
highlight,
|
|
39
39
|
className = ""
|
|
40
40
|
}) {
|
|
41
|
-
const fundo = active ? "bg-blue-50" : checked ? "bg-proef-turquoise/5" : "hover:bg-slate-50";
|
|
41
|
+
const fundo = active ? "bg-blue-50 dark:bg-blue-500/10" : checked ? "bg-proef-turquoise/5" : "hover:bg-slate-50 dark:hover:bg-slate-800/50";
|
|
42
42
|
const realcar = (n) => typeof n === "string" ? /* @__PURE__ */ jsx(HighlightText, { text: n, terms: highlight }) : n;
|
|
43
43
|
return /* @__PURE__ */ jsxs("li", { className: `v1-reset relative flex list-none items-start gap-1.5 px-3 py-3 transition-colors ${LINHA_ESBATIDA} ${fundo} ${className}`, children: [
|
|
44
44
|
onCheckedChange && /* @__PURE__ */ jsxs("span", { className: "flex w-7 shrink-0 flex-col items-center gap-1", children: [
|
|
@@ -54,10 +54,10 @@ function ListItem({
|
|
|
54
54
|
className: "min-w-0 flex-1 space-y-1 text-left outline-none after:absolute after:inset-0 focus-visible:after:outline-2 focus-visible:after:-outline-offset-2 focus-visible:after:outline-primary",
|
|
55
55
|
children: [
|
|
56
56
|
(code || badges) && /* @__PURE__ */ jsxs("span", { className: "flex flex-wrap items-center gap-x-2 gap-y-0.5 leading-tight", children: [
|
|
57
|
-
code && /* @__PURE__ */ jsx("span", { className: "font-mono text-xs font-semibold text-slate-700", children: realcar(code) }),
|
|
57
|
+
code && /* @__PURE__ */ jsx("span", { className: "font-mono text-xs font-semibold text-slate-700 dark:text-slate-100", children: realcar(code) }),
|
|
58
58
|
badges
|
|
59
59
|
] }),
|
|
60
|
-
/* @__PURE__ */ jsx("span", { className: code ? "line-clamp-2 block text-[10px] leading-snug break-words text-slate-600" : "block truncate text-xs text-slate-700", children: realcar(title) }),
|
|
60
|
+
/* @__PURE__ */ jsx("span", { className: code ? "line-clamp-2 block text-[10px] leading-snug break-words text-slate-600 dark:text-slate-300" : "block truncate text-xs text-slate-700 dark:text-slate-100", children: realcar(title) }),
|
|
61
61
|
meta && /* @__PURE__ */ jsx("span", { className: "block truncate text-[9px] leading-tight text-slate-400", children: meta })
|
|
62
62
|
]
|
|
63
63
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ListItem.js","sources":["../../../../../../src/v1/primitives/lists/ListItem/ListItem.tsx"],"sourcesContent":["import React, { Fragment } from 'react'\r\nimport Checkbox from '../../forms/Checkbox/Checkbox'\r\n\r\n/** O que se mostra num ListItem: o resto (ativo, marcado) é estado. */\r\nexport interface ListItemContent {\r\n /** Por baixo do código: o que a linha é (ex: a descrição). Até duas linhas, cortada com \"…\" se não couber. */\r\n title: React.ReactNode\r\n /** O texto mais forte da linha, em letra de máquina (ex: o número ou código). */\r\n code?: React.ReactNode\r\n /** Na linha do código, logo a seguir a ele: indicadores e avisos curtos. */\r\n badges?: React.ReactNode\r\n /** Por baixo do título, pequeno e numa linha só (cortada com \"…\"): o resto do que se quer ver de relance, separado por \" · \" (ex: local · data). */\r\n meta?: React.ReactNode\r\n /**\r\n * Opcional, e só com caixa de marcar (`onCheckedChange`): uma etiqueta curta\r\n * (ex: a categoria) no espaço que a caixa abre por baixo dela. Sem caixa não\r\n * se mostra — não há coluna para ela.\r\n */\r\n tag?: React.ReactNode\r\n /** À direita, ao nível do código: marcas que se querem ver mesmo a correr a lista. */\r\n aside?: React.ReactNode\r\n}\r\n\r\nexport interface ListItemProps extends ListItemContent {\r\n /** O que está aberto ao lado (fica realçado). */\r\n active?: boolean\r\n /** Ao clicar (ou Enter/Espaço com o foco nele): normalmente, abri-lo. */\r\n onClick?: () => void\r\n /** Marcado. Só há caixa de marcar se vier com `onCheckedChange`. */\r\n checked?: boolean\r\n onCheckedChange?: (checked: boolean) => void\r\n /** O nome da caixa de marcar, para leitores de ecrã (ex: \"Marcar P-020000\"). */\r\n checkboxLabel?: string\r\n /**\r\n * O que se procurou: realça-se no código e no título quando são texto (sem\r\n * acentos nem maiúsculas). O [ListFinder](?path=/docs/v1-components-lists-listfinder--docs) passa-o sozinho.\r\n */\r\n highlight?: string[]\r\n className?: string\r\n}\r\n\r\n/**\r\n * A linha entre linhas, esbatida nas pontas: uma risca de ponta a ponta\r\n * fechava cada linha numa caixa. Desenhada como fundo (e não com um ::after,\r\n * que o botão da linha já usa); a última linha não a leva.\r\n */\r\nexport const LINHA_ESBATIDA = 'bg-[linear-gradient(to_right,transparent,var(--color-slate-200)_15%,var(--color-slate-200)_85%,transparent)] bg-[length:100%_1px] bg-bottom bg-no-repeat last:bg-none'\r\n\r\nconst semAcento = (c: string) => c.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase()\r\n\r\n/**\r\n * Texto com os pedaços que batem com a pesquisa realçados: numa descrição\r\n * comprida, ou num número de dez dígitos, não se via onde é que o que se\r\n * escreveu bateu.\r\n */\r\nexport function HighlightText({ text, terms }: { text: string; terms?: string[] }) {\r\n const termos = (terms ?? []).map(t => [...t].map(semAcento).join('')).filter(Boolean)\r\n if (!termos.length) return <>{text}</>\r\n // Carácter a carácter, para as posições baterem com o texto original.\r\n const alvo = [...text].map(c => semAcento(c).charAt(0) || c).join('')\r\n const marcado = new Array<boolean>(alvo.length).fill(false)\r\n for (const t of termos) {\r\n for (let i = alvo.indexOf(t); i >= 0; i = alvo.indexOf(t, i + 1)) {\r\n for (let k = i; k < i + t.length; k++) marcado[k] = true\r\n }\r\n }\r\n if (!marcado.some(Boolean)) return <>{text}</>\r\n const chars = [...text]\r\n const partes: { txt: string; hit: boolean }[] = []\r\n chars.forEach((c, i) => {\r\n const ultimo = partes[partes.length - 1]\r\n if (ultimo && ultimo.hit === marcado[i]) ultimo.txt += c\r\n else partes.push({ txt: c, hit: marcado[i] })\r\n })\r\n return (\r\n <>\r\n {partes.map((p, i) => p.hit\r\n ? <mark key={i} className=\"rounded-sm bg-amber-200 font-bold text-amber-900\">{p.txt}</mark>\r\n : <Fragment key={i}>{p.txt}</Fragment>)}\r\n </>\r\n )\r\n}\r\n\r\n/**\r\n * Uma linha de uma lista para escolher: o cartão das listas do\r\n * [ListFinder](?path=/docs/v1-components-lists-listfinder--docs).\r\n *\r\n * Não sabe o que mostra: a app dá as partes e o ListItem arruma-as sempre da\r\n * mesma forma. O código é o texto mais forte da linha, com os indicadores ao\r\n * lado; o título vem por baixo, mais pequeno, e o resto por baixo dele. Com\r\n * `onCheckedChange` leva uma caixa à esquerda, para marcar vários (um\r\n * [Checkbox](?path=/docs/v1-primitives-forms-checkbox--docs)), numa coluna de\r\n * largura fixa — senão os títulos começavam em sítios diferentes de linha para\r\n * linha — e, se se quiser, a `tag` por baixo dela.\r\n *\r\n * É um `<li>`: vai dentro de um `<ul>`.\r\n */\r\nexport default function ListItem({\r\n title, code, badges, meta, tag, aside, active = false, onClick, checked = false, onCheckedChange, checkboxLabel,\r\n highlight, className = '',\r\n}: ListItemProps) {\r\n const fundo = active ? 'bg-blue-50' : checked ? 'bg-proef-turquoise/5' : 'hover:bg-slate-50'\r\n const realcar = (n: React.ReactNode) => (typeof n === 'string' ? <HighlightText text={n} terms={highlight} /> : n)\r\n return (\r\n <li className={`v1-reset relative flex list-none items-start gap-1.5 px-3 py-3 transition-colors ${LINHA_ESBATIDA} ${fundo} ${className}`}>\r\n {onCheckedChange && (\r\n <span className=\"flex w-7 shrink-0 flex-col items-center gap-1\">\r\n <Checkbox checked={checked} onChange={e => onCheckedChange(e.target.checked)} aria-label={checkboxLabel} className=\"z-10 mt-0.5\" />\r\n {tag && <span className=\"text-[8px] leading-none font-semibold text-slate-400\">{tag}</span>}\r\n </span>\r\n )}\r\n {/* O botão cobre a linha toda (o ::after), para se clicar em qualquer\r\n sítio; a caixa de marcar fica por cima dele. */}\r\n <button\r\n type=\"button\"\r\n onClick={onClick}\r\n aria-current={active || undefined}\r\n className=\"min-w-0 flex-1 space-y-1 text-left outline-none after:absolute after:inset-0 focus-visible:after:outline-2 focus-visible:after:-outline-offset-2 focus-visible:after:outline-primary\"\r\n >\r\n {(code || badges) && (\r\n <span className=\"flex flex-wrap items-center gap-x-2 gap-y-0.5 leading-tight\">\r\n {code && <span className=\"font-mono text-xs font-semibold text-slate-700\">{realcar(code)}</span>}\r\n {badges}\r\n </span>\r\n )}\r\n {/* Sem código, o título é a linha principal. */}\r\n <span className={code\r\n ? 'line-clamp-2 block text-[10px] leading-snug break-words text-slate-600'\r\n : 'block truncate text-xs text-slate-700'}>\r\n {realcar(title)}\r\n </span>\r\n {meta && <span className=\"block truncate text-[9px] leading-tight text-slate-400\">{meta}</span>}\r\n </button>\r\n {aside && <span className=\"flex shrink-0 flex-col items-end gap-1\">{aside}</span>}\r\n </li>\r\n )\r\n}\r\n"],"names":["Fragment"],"mappings":";;;AA8CO,MAAM,iBAAiB;AAE9B,MAAM,YAAY,CAAC,MAAc,EAAE,UAAU,KAAK,EAAE,QAAQ,UAAU,EAAE,EAAE,YAAA;AAOnE,SAAS,cAAc,EAAE,MAAM,SAA6C;AACjF,QAAM,UAAU,SAAS,CAAA,GAAI,IAAI,CAAA,MAAK,CAAC,GAAG,CAAC,EAAE,IAAI,SAAS,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,OAAO;AACpF,MAAI,CAAC,OAAO,OAAQ,wCAAU,UAAA,MAAK;AAEnC,QAAM,OAAO,CAAC,GAAG,IAAI,EAAE,IAAI,CAAA,MAAK,UAAU,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE;AACpE,QAAM,UAAU,IAAI,MAAe,KAAK,MAAM,EAAE,KAAK,KAAK;AAC1D,aAAW,KAAK,QAAQ;AACtB,aAAS,IAAI,KAAK,QAAQ,CAAC,GAAG,KAAK,GAAG,IAAI,KAAK,QAAQ,GAAG,IAAI,CAAC,GAAG;AAChE,eAAS,IAAI,GAAG,IAAI,IAAI,EAAE,QAAQ,IAAK,SAAQ,CAAC,IAAI;AAAA,IACtD;AAAA,EACF;AACA,MAAI,CAAC,QAAQ,KAAK,OAAO,EAAG,wCAAU,UAAA,MAAK;AAC3C,QAAM,QAAQ,CAAC,GAAG,IAAI;AACtB,QAAM,SAA0C,CAAA;AAChD,QAAM,QAAQ,CAAC,GAAG,MAAM;AACtB,UAAM,SAAS,OAAO,OAAO,SAAS,CAAC;AACvC,QAAI,UAAU,OAAO,QAAQ,QAAQ,CAAC,UAAU,OAAO;AAAA,QAClD,QAAO,KAAK,EAAE,KAAK,GAAG,KAAK,QAAQ,CAAC,GAAG;AAAA,EAC9C,CAAC;AACD,SACE,oBAAA,UAAA,EACG,iBAAO,IAAI,CAAC,GAAG,MAAM,EAAE,MACpB,oBAAC,QAAA,EAAa,WAAU,
|
|
1
|
+
{"version":3,"file":"ListItem.js","sources":["../../../../../../src/v1/primitives/lists/ListItem/ListItem.tsx"],"sourcesContent":["import React, { Fragment } from 'react'\r\nimport Checkbox from '../../forms/Checkbox/Checkbox'\r\n\r\n/** O que se mostra num ListItem: o resto (ativo, marcado) é estado. */\r\nexport interface ListItemContent {\r\n /** Por baixo do código: o que a linha é (ex: a descrição). Até duas linhas, cortada com \"…\" se não couber. */\r\n title: React.ReactNode\r\n /** O texto mais forte da linha, em letra de máquina (ex: o número ou código). */\r\n code?: React.ReactNode\r\n /** Na linha do código, logo a seguir a ele: indicadores e avisos curtos. */\r\n badges?: React.ReactNode\r\n /** Por baixo do título, pequeno e numa linha só (cortada com \"…\"): o resto do que se quer ver de relance, separado por \" · \" (ex: local · data). */\r\n meta?: React.ReactNode\r\n /**\r\n * Opcional, e só com caixa de marcar (`onCheckedChange`): uma etiqueta curta\r\n * (ex: a categoria) no espaço que a caixa abre por baixo dela. Sem caixa não\r\n * se mostra — não há coluna para ela.\r\n */\r\n tag?: React.ReactNode\r\n /** À direita, ao nível do código: marcas que se querem ver mesmo a correr a lista. */\r\n aside?: React.ReactNode\r\n}\r\n\r\nexport interface ListItemProps extends ListItemContent {\r\n /** O que está aberto ao lado (fica realçado). */\r\n active?: boolean\r\n /** Ao clicar (ou Enter/Espaço com o foco nele): normalmente, abri-lo. */\r\n onClick?: () => void\r\n /** Marcado. Só há caixa de marcar se vier com `onCheckedChange`. */\r\n checked?: boolean\r\n onCheckedChange?: (checked: boolean) => void\r\n /** O nome da caixa de marcar, para leitores de ecrã (ex: \"Marcar P-020000\"). */\r\n checkboxLabel?: string\r\n /**\r\n * O que se procurou: realça-se no código e no título quando são texto (sem\r\n * acentos nem maiúsculas). O [ListFinder](?path=/docs/v1-components-lists-listfinder--docs) passa-o sozinho.\r\n */\r\n highlight?: string[]\r\n className?: string\r\n}\r\n\r\n/**\r\n * A linha entre linhas, esbatida nas pontas: uma risca de ponta a ponta\r\n * fechava cada linha numa caixa. Desenhada como fundo (e não com um ::after,\r\n * que o botão da linha já usa); a última linha não a leva.\r\n */\r\nexport const LINHA_ESBATIDA = 'bg-[linear-gradient(to_right,transparent,var(--color-slate-200)_15%,var(--color-slate-200)_85%,transparent)] dark:bg-[linear-gradient(to_right,transparent,var(--color-slate-800)_15%,var(--color-slate-800)_85%,transparent)] bg-[length:100%_1px] bg-bottom bg-no-repeat last:bg-none'\r\n\r\nconst semAcento = (c: string) => c.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase()\r\n\r\n/**\r\n * Texto com os pedaços que batem com a pesquisa realçados: numa descrição\r\n * comprida, ou num número de dez dígitos, não se via onde é que o que se\r\n * escreveu bateu.\r\n */\r\nexport function HighlightText({ text, terms }: { text: string; terms?: string[] }) {\r\n const termos = (terms ?? []).map(t => [...t].map(semAcento).join('')).filter(Boolean)\r\n if (!termos.length) return <>{text}</>\r\n // Carácter a carácter, para as posições baterem com o texto original.\r\n const alvo = [...text].map(c => semAcento(c).charAt(0) || c).join('')\r\n const marcado = new Array<boolean>(alvo.length).fill(false)\r\n for (const t of termos) {\r\n for (let i = alvo.indexOf(t); i >= 0; i = alvo.indexOf(t, i + 1)) {\r\n for (let k = i; k < i + t.length; k++) marcado[k] = true\r\n }\r\n }\r\n if (!marcado.some(Boolean)) return <>{text}</>\r\n const chars = [...text]\r\n const partes: { txt: string; hit: boolean }[] = []\r\n chars.forEach((c, i) => {\r\n const ultimo = partes[partes.length - 1]\r\n if (ultimo && ultimo.hit === marcado[i]) ultimo.txt += c\r\n else partes.push({ txt: c, hit: marcado[i] })\r\n })\r\n return (\r\n <>\r\n {partes.map((p, i) => p.hit\r\n ? <mark key={i} className=\"rounded-sm bg-amber-200 font-bold text-amber-900 dark:bg-amber-500/30 dark:text-amber-100\">{p.txt}</mark>\r\n : <Fragment key={i}>{p.txt}</Fragment>)}\r\n </>\r\n )\r\n}\r\n\r\n/**\r\n * Uma linha de uma lista para escolher: o cartão das listas do\r\n * [ListFinder](?path=/docs/v1-components-lists-listfinder--docs).\r\n *\r\n * Não sabe o que mostra: a app dá as partes e o ListItem arruma-as sempre da\r\n * mesma forma. O código é o texto mais forte da linha, com os indicadores ao\r\n * lado; o título vem por baixo, mais pequeno, e o resto por baixo dele. Com\r\n * `onCheckedChange` leva uma caixa à esquerda, para marcar vários (um\r\n * [Checkbox](?path=/docs/v1-primitives-forms-checkbox--docs)), numa coluna de\r\n * largura fixa — senão os títulos começavam em sítios diferentes de linha para\r\n * linha — e, se se quiser, a `tag` por baixo dela.\r\n *\r\n * É um `<li>`: vai dentro de um `<ul>`.\r\n */\r\nexport default function ListItem({\r\n title, code, badges, meta, tag, aside, active = false, onClick, checked = false, onCheckedChange, checkboxLabel,\r\n highlight, className = '',\r\n}: ListItemProps) {\r\n const fundo = active ? 'bg-blue-50 dark:bg-blue-500/10' : checked ? 'bg-proef-turquoise/5' : 'hover:bg-slate-50 dark:hover:bg-slate-800/50'\r\n const realcar = (n: React.ReactNode) => (typeof n === 'string' ? <HighlightText text={n} terms={highlight} /> : n)\r\n return (\r\n <li className={`v1-reset relative flex list-none items-start gap-1.5 px-3 py-3 transition-colors ${LINHA_ESBATIDA} ${fundo} ${className}`}>\r\n {onCheckedChange && (\r\n <span className=\"flex w-7 shrink-0 flex-col items-center gap-1\">\r\n <Checkbox checked={checked} onChange={e => onCheckedChange(e.target.checked)} aria-label={checkboxLabel} className=\"z-10 mt-0.5\" />\r\n {tag && <span className=\"text-[8px] leading-none font-semibold text-slate-400\">{tag}</span>}\r\n </span>\r\n )}\r\n {/* O botão cobre a linha toda (o ::after), para se clicar em qualquer\r\n sítio; a caixa de marcar fica por cima dele. */}\r\n <button\r\n type=\"button\"\r\n onClick={onClick}\r\n aria-current={active || undefined}\r\n className=\"min-w-0 flex-1 space-y-1 text-left outline-none after:absolute after:inset-0 focus-visible:after:outline-2 focus-visible:after:-outline-offset-2 focus-visible:after:outline-primary\"\r\n >\r\n {(code || badges) && (\r\n <span className=\"flex flex-wrap items-center gap-x-2 gap-y-0.5 leading-tight\">\r\n {code && <span className=\"font-mono text-xs font-semibold text-slate-700 dark:text-slate-100\">{realcar(code)}</span>}\r\n {badges}\r\n </span>\r\n )}\r\n {/* Sem código, o título é a linha principal. */}\r\n <span className={code\r\n ? 'line-clamp-2 block text-[10px] leading-snug break-words text-slate-600 dark:text-slate-300'\r\n : 'block truncate text-xs text-slate-700 dark:text-slate-100'}>\r\n {realcar(title)}\r\n </span>\r\n {meta && <span className=\"block truncate text-[9px] leading-tight text-slate-400\">{meta}</span>}\r\n </button>\r\n {aside && <span className=\"flex shrink-0 flex-col items-end gap-1\">{aside}</span>}\r\n </li>\r\n )\r\n}\r\n"],"names":["Fragment"],"mappings":"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"}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useRef } from "react";
|
|
2
|
+
import { useRef, useEffect, useState, useLayoutEffect } from "react";
|
|
3
|
+
import { SelectInput } from "../../forms/SelectInput/SelectInput.js";
|
|
3
4
|
import { SEGMENTED } from "../../buttons/SegmentedControl/segmentedStyles.js";
|
|
4
5
|
const STYLES = {
|
|
5
6
|
underline: {
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
// No telemóvel numa só linha, a deslizar para o lado: partidos em várias linhas comiam o ecrã.
|
|
8
|
+
list: "flex flex-wrap border-b border-border max-md:flex-nowrap max-md:overflow-x-auto max-md:scrollbar-none",
|
|
9
|
+
tab: "-mb-px h-11 shrink-0 border-0 border-b-[3px] px-4 max-md:px-3",
|
|
8
10
|
active: "border-primary text-primary dark:border-foreground dark:text-foreground",
|
|
9
11
|
idle: "border-transparent bg-transparent text-muted-foreground hover:text-primary dark:hover:text-foreground",
|
|
10
12
|
countActive: "bg-primary text-primary-foreground dark:bg-proef-blue dark:text-white"
|
|
@@ -53,42 +55,101 @@ function Tabs(props) {
|
|
|
53
55
|
e.preventDefault();
|
|
54
56
|
};
|
|
55
57
|
const focusIndex = Math.max(0, items.findIndex((it) => isActive(it.id)));
|
|
58
|
+
const listRef = useRef(null);
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
const list = listRef.current;
|
|
61
|
+
const tab = refs.current[focusIndex];
|
|
62
|
+
if (!list || !tab || list.scrollWidth <= list.clientWidth) return;
|
|
63
|
+
const l = list.getBoundingClientRect();
|
|
64
|
+
const r = tab.getBoundingClientRect();
|
|
65
|
+
if (r.left < l.left) list.scrollLeft -= l.left - r.left;
|
|
66
|
+
else if (r.right > l.right) list.scrollLeft += r.right - l.right;
|
|
67
|
+
}, [focusIndex]);
|
|
56
68
|
const padding = iconOnly ? "w-[26px] justify-center px-0" : "px-3";
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
69
|
+
const [naoCabe, setNaoCabe] = useState(false);
|
|
70
|
+
useLayoutEffect(() => {
|
|
71
|
+
const list = listRef.current;
|
|
72
|
+
if (!list || variant !== "underline") return;
|
|
73
|
+
const medir = () => setNaoCabe(list.scrollWidth > list.clientWidth + 1);
|
|
74
|
+
medir();
|
|
75
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
76
|
+
const ro = new ResizeObserver(medir);
|
|
77
|
+
ro.observe(list);
|
|
78
|
+
return () => ro.disconnect();
|
|
79
|
+
}, [variant, items]);
|
|
80
|
+
const texto = (item) => `${typeof item.label === "string" ? item.label : item.id}${item.count !== void 0 ? ` (${item.count})` : ""}`;
|
|
81
|
+
const lista = naoCabe && (props.multiple ? /* @__PURE__ */ jsx(
|
|
82
|
+
SelectInput,
|
|
83
|
+
{
|
|
84
|
+
"aria-label": label,
|
|
85
|
+
multiple: true,
|
|
86
|
+
values: props.value,
|
|
87
|
+
onValuesChange: (ids) => {
|
|
88
|
+
const antes = new Set(props.value);
|
|
89
|
+
const clicado = ids.find((id) => !antes.has(id)) ?? props.value.find((id) => !ids.includes(id));
|
|
90
|
+
props.onChange(ids, clicado ?? props.allId ?? items[0].id);
|
|
91
|
+
},
|
|
92
|
+
options: items.filter((it) => it.id !== props.allId).map((it) => ({ value: it.id, label: texto(it), disabled: it.disabled })),
|
|
93
|
+
placeholder: props.allId !== void 0 ? texto(items.find((it) => it.id === props.allId)) : void 0
|
|
94
|
+
}
|
|
95
|
+
) : /* @__PURE__ */ jsx(
|
|
96
|
+
SelectInput,
|
|
97
|
+
{
|
|
98
|
+
"aria-label": label,
|
|
99
|
+
value: props.value,
|
|
100
|
+
onValueChange: (v) => props.onChange(v),
|
|
101
|
+
options: items.map((it) => ({ value: it.id, label: texto(it), disabled: it.disabled }))
|
|
102
|
+
}
|
|
103
|
+
));
|
|
104
|
+
return /* @__PURE__ */ jsxs("div", { className: `relative ${className}`, children: [
|
|
105
|
+
lista,
|
|
106
|
+
/* @__PURE__ */ jsx(
|
|
107
|
+
"div",
|
|
62
108
|
{
|
|
63
|
-
ref:
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
"
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
onClick: (e) => select(item.id, e.ctrlKey || e.metaKey || e.shiftKey),
|
|
75
|
-
onKeyDown: (e) => onKeyDown(e, i),
|
|
76
|
-
className: `inline-flex cursor-pointer items-center gap-2 text-caption font-black tracking-wider whitespace-nowrap uppercase transition-colors ${s.tab} ${variant === "underline" ? "px-4" : padding} focus-visible:ring-2 focus-visible:ring-primary/30 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-40 ` + (active ? s.active : s.idle),
|
|
77
|
-
children: [
|
|
78
|
-
item.icon,
|
|
79
|
-
!iconOnly && item.label,
|
|
80
|
-
item.count !== void 0 && /* @__PURE__ */ jsx(
|
|
81
|
-
"span",
|
|
109
|
+
ref: listRef,
|
|
110
|
+
role: "tablist",
|
|
111
|
+
"aria-label": label,
|
|
112
|
+
"aria-multiselectable": props.multiple || void 0,
|
|
113
|
+
"aria-hidden": naoCabe || void 0,
|
|
114
|
+
className: `${s.list} ${naoCabe ? "pointer-events-none invisible absolute inset-x-0 top-0" : ""}`,
|
|
115
|
+
children: items.map((item, i) => {
|
|
116
|
+
const active = isActive(item.id);
|
|
117
|
+
const text = typeof item.label === "string" ? item.label : void 0;
|
|
118
|
+
return /* @__PURE__ */ jsxs(
|
|
119
|
+
"button",
|
|
82
120
|
{
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
121
|
+
ref: (el) => {
|
|
122
|
+
refs.current[i] = el;
|
|
123
|
+
},
|
|
124
|
+
type: "button",
|
|
125
|
+
role: "tab",
|
|
126
|
+
"aria-selected": active,
|
|
127
|
+
"aria-label": iconOnly ? text : void 0,
|
|
128
|
+
"data-tip": item.tip ?? (iconOnly ? text : void 0),
|
|
129
|
+
"data-tip-title": item.tip ? text : void 0,
|
|
130
|
+
tabIndex: i === focusIndex ? 0 : -1,
|
|
131
|
+
disabled: item.disabled,
|
|
132
|
+
onClick: (e) => select(item.id, e.ctrlKey || e.metaKey || e.shiftKey),
|
|
133
|
+
onKeyDown: (e) => onKeyDown(e, i),
|
|
134
|
+
className: `inline-flex cursor-pointer items-center gap-2 text-caption font-black tracking-wider whitespace-nowrap uppercase transition-colors ${s.tab} ${variant === "underline" ? "" : padding} focus-visible:ring-2 focus-visible:ring-primary/30 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-40 ` + (active ? s.active : s.idle),
|
|
135
|
+
children: [
|
|
136
|
+
item.icon,
|
|
137
|
+
!iconOnly && item.label,
|
|
138
|
+
item.count !== void 0 && /* @__PURE__ */ jsx(
|
|
139
|
+
"span",
|
|
140
|
+
{
|
|
141
|
+
className: `min-w-[18px] rounded-full px-1.5 py-px text-center text-micro font-extrabold tracking-normal tabular-nums ${active ? s.countActive : "bg-muted text-foreground"}`,
|
|
142
|
+
children: item.count
|
|
143
|
+
}
|
|
144
|
+
)
|
|
145
|
+
]
|
|
146
|
+
},
|
|
147
|
+
item.id
|
|
148
|
+
);
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
)
|
|
152
|
+
] });
|
|
92
153
|
}
|
|
93
154
|
export {
|
|
94
155
|
Tabs as default
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Tabs.js","sources":["../../../../../../src/v1/primitives/navigation/Tabs/Tabs.tsx"],"sourcesContent":["import React, { useRef } from 'react'\r\nimport { SEGMENTED } from '../../buttons/SegmentedControl/segmentedStyles'\r\n\r\nexport interface TabItem<K extends string = string> {\r\n id: K\r\n /** Normalmente texto; aceita JSX. Com `iconOnly`, tem de ser texto (vai para o nome do botão e para o tooltip). */\r\n label: React.ReactNode\r\n /** Ícone antes do texto (ex: um ícone do lucide com `size={14}`). */\r\n icon?: React.ReactNode\r\n /** Número ao lado do texto (ex: pendentes). Não aparece se for `undefined`. */\r\n count?: number\r\n /** Explicação no tooltip (`data-tip`, com o `TooltipLayer`), com o `label` como título. */\r\n tip?: string\r\n disabled?: boolean\r\n}\r\n\r\ninterface TabsBase<K extends string> {\r\n items: TabItem<K>[]\r\n /** Nome do conjunto, para leitores de ecrã (ex: \"Estado das faturas\"). */\r\n label?: string\r\n /**\r\n * `underline` (por omissão): separadores da página, com a linha por baixo.\r\n * `segmented`: botões juntos numa caixa, para trocar de vista dentro de uma\r\n * secção (ex: Lista · Quadro).\r\n */\r\n variant?: 'underline' | 'segmented'\r\n /** Só os ícones; o `label` fica no tooltip e no nome do botão. */\r\n iconOnly?: boolean\r\n className?: string\r\n}\r\n\r\nexport interface TabsSingleProps<K extends string = string> extends TabsBase<K> {\r\n multiple?: false\r\n /** O separador escolhido. A app guarda este estado. */\r\n value: K\r\n onChange: (id: K) => void\r\n}\r\n\r\nexport interface TabsMultipleProps<K extends string = string> extends TabsBase<K> {\r\n /**\r\n * Vários escolhidos ao mesmo tempo: um clique escolhe só esse separador;\r\n * Ctrl/⌘/Shift + clique junta-o ou tira-o dos escolhidos.\r\n */\r\n multiple: true\r\n value: K[]\r\n /** Os escolhidos depois do clique, e o separador clicado. */\r\n onChange: (ids: K[], clicked: K) => void\r\n /** O separador \"Todos\": escolhê-lo limpa a escolha, e fica ativo quando não há nenhum escolhido. */\r\n allId?: K\r\n}\r\n\r\nexport type TabsProps<K extends string = string> = TabsSingleProps<K> | TabsMultipleProps<K>\r\n\r\nconst STYLES = {\r\n underline: {\r\n list: 'flex flex-wrap border-b border-border',\r\n tab: '-mb-px h-11 border-0 border-b-[3px] px-4',\r\n active: 'border-primary text-primary dark:border-foreground dark:text-foreground',\r\n idle: 'border-transparent bg-transparent text-muted-foreground hover:text-primary dark:hover:text-foreground',\r\n countActive: 'bg-primary text-primary-foreground dark:bg-proef-blue dark:text-white',\r\n },\r\n // O mesmo aspeto do SegmentedControl (definido num só sítio).\r\n segmented: {\r\n list: SEGMENTED.list,\r\n tab: 'h-[26px] rounded-md border-0',\r\n active: SEGMENTED.active,\r\n idle: SEGMENTED.idle,\r\n countActive: 'bg-primary-foreground/20',\r\n },\r\n}\r\n\r\n/**\r\n * Separadores: escolher uma de várias vistas do mesmo ecrã.\r\n *\r\n * Só desenha os botões; o conteúdo de cada vista fica na app, que mostra o\r\n * que corresponde a `value`. Setas ← → (e Home/End) mudam de separador,\r\n * saltando os desativados. O escolhido fica a navy (não turquesa: turquesa\r\n * sobre branco não tem contraste para texto).\r\n *\r\n * Com `multiple`, vários podem estar escolhidos (ex: estados de uma lista).\r\n */\r\nexport default function Tabs<K extends string = string>(props: TabsProps<K>) {\r\n const { items, label, variant = 'underline', iconOnly = false, className = '' } = props\r\n const refs = useRef<(HTMLButtonElement | null)[]>([])\r\n const s = STYLES[variant]\r\n\r\n const isActive = (id: K) => {\r\n if (!props.multiple) return id === props.value\r\n if (props.allId !== undefined && id === props.allId) return props.value.length === 0\r\n return props.value.includes(id)\r\n }\r\n\r\n const select = (id: K, additive: boolean) => {\r\n if (!props.multiple) return props.onChange(id)\r\n if (props.allId !== undefined && id === props.allId) return props.onChange([], id)\r\n if (!additive) return props.onChange([id], id)\r\n props.onChange(props.value.includes(id) ? props.value.filter((v) => v !== id) : [...props.value, id], id)\r\n }\r\n\r\n const move = (from: number, step: number) => {\r\n const n = items.length\r\n for (let i = 1; i <= n; i++) {\r\n const next = (((from + step * i) % n) + n) % n\r\n if (!items[next].disabled) {\r\n select(items[next].id, false)\r\n refs.current[next]?.focus()\r\n return\r\n }\r\n }\r\n }\r\n\r\n const onKeyDown = (e: React.KeyboardEvent, index: number) => {\r\n if (e.key === 'ArrowRight') move(index, 1)\r\n else if (e.key === 'ArrowLeft') move(index, -1)\r\n else if (e.key === 'Home') move(-1, 1)\r\n else if (e.key === 'End') move(items.length, -1)\r\n else return\r\n e.preventDefault()\r\n }\r\n\r\n // O primeiro escolhido entra no Tab do teclado (ou o primeiro, se nenhum).\r\n const focusIndex = Math.max(0, items.findIndex((it) => isActive(it.id)))\r\n const padding = iconOnly ? 'w-[26px] justify-center px-0' : 'px-3'\r\n\r\n return (\r\n <div role=\"tablist\" aria-label={label} aria-multiselectable={props.multiple || undefined} className={`${s.list} ${className}`}>\r\n {items.map((item, i) => {\r\n const active = isActive(item.id)\r\n const text = typeof item.label === 'string' ? item.label : undefined\r\n return (\r\n <button\r\n key={item.id}\r\n ref={(el) => {\r\n refs.current[i] = el\r\n }}\r\n type=\"button\"\r\n role=\"tab\"\r\n aria-selected={active}\r\n aria-label={iconOnly ? text : undefined}\r\n data-tip={item.tip ?? (iconOnly ? text : undefined)}\r\n data-tip-title={item.tip ? text : undefined}\r\n tabIndex={i === focusIndex ? 0 : -1}\r\n disabled={item.disabled}\r\n onClick={(e) => select(item.id, e.ctrlKey || e.metaKey || e.shiftKey)}\r\n onKeyDown={(e) => onKeyDown(e, i)}\r\n className={\r\n `inline-flex cursor-pointer items-center gap-2 text-caption font-black tracking-wider whitespace-nowrap uppercase transition-colors ${s.tab} ${variant === 'underline' ? 'px-4' : padding} ` +\r\n 'focus-visible:ring-2 focus-visible:ring-primary/30 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-40 ' +\r\n (active ? s.active : s.idle)\r\n }\r\n >\r\n {item.icon}\r\n {!iconOnly && item.label}\r\n {item.count !== undefined && (\r\n <span\r\n className={`min-w-[18px] rounded-full px-1.5 py-px text-center text-micro font-extrabold tracking-normal tabular-nums ${\r\n active ? s.countActive : 'bg-muted text-foreground'\r\n }`}\r\n >\r\n {item.count}\r\n </span>\r\n )}\r\n </button>\r\n )\r\n })}\r\n </div>\r\n )\r\n}\r\n"],"names":[],"mappings":";;;AAqDA,MAAM,SAAS;AAAA,EACb,WAAW;AAAA,IACT,MAAM;AAAA,IACN,KAAK;AAAA,IACL,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,aAAa;AAAA,EAAA;AAAA;AAAA,EAGf,WAAW;AAAA,IACT,MAAM,UAAU;AAAA,IAChB,KAAK;AAAA,IACL,QAAQ,UAAU;AAAA,IAClB,MAAM,UAAU;AAAA,IAChB,aAAa;AAAA,EAAA;AAEjB;AAYA,SAAwB,KAAgC,OAAqB;AAC3E,QAAM,EAAE,OAAO,OAAO,UAAU,aAAa,WAAW,OAAO,YAAY,GAAA,IAAO;AAClF,QAAM,OAAO,OAAqC,EAAE;AACpD,QAAM,IAAI,OAAO,OAAO;AAExB,QAAM,WAAW,CAAC,OAAU;AAC1B,QAAI,CAAC,MAAM,SAAU,QAAO,OAAO,MAAM;AACzC,QAAI,MAAM,UAAU,UAAa,OAAO,MAAM,MAAO,QAAO,MAAM,MAAM,WAAW;AACnF,WAAO,MAAM,MAAM,SAAS,EAAE;AAAA,EAChC;AAEA,QAAM,SAAS,CAAC,IAAO,aAAsB;AAC3C,QAAI,CAAC,MAAM,SAAU,QAAO,MAAM,SAAS,EAAE;AAC7C,QAAI,MAAM,UAAU,UAAa,OAAO,MAAM,MAAO,QAAO,MAAM,SAAS,CAAA,GAAI,EAAE;AACjF,QAAI,CAAC,SAAU,QAAO,MAAM,SAAS,CAAC,EAAE,GAAG,EAAE;AAC7C,UAAM,SAAS,MAAM,MAAM,SAAS,EAAE,IAAI,MAAM,MAAM,OAAO,CAAC,MAAM,MAAM,EAAE,IAAI,CAAC,GAAG,MAAM,OAAO,EAAE,GAAG,EAAE;AAAA,EAC1G;AAEA,QAAM,OAAO,CAAC,MAAc,SAAiB;AAC3C,UAAM,IAAI,MAAM;AAChB,aAAS,IAAI,GAAG,KAAK,GAAG,KAAK;AAC3B,YAAM,SAAU,OAAO,OAAO,KAAK,IAAK,KAAK;AAC7C,UAAI,CAAC,MAAM,IAAI,EAAE,UAAU;AACzB,eAAO,MAAM,IAAI,EAAE,IAAI,KAAK;AAC5B,aAAK,QAAQ,IAAI,GAAG,MAAA;AACpB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,QAAM,YAAY,CAAC,GAAwB,UAAkB;AAC3D,QAAI,EAAE,QAAQ,aAAc,MAAK,OAAO,CAAC;AAAA,aAChC,EAAE,QAAQ,YAAa,MAAK,OAAO,EAAE;AAAA,aACrC,EAAE,QAAQ,OAAQ,MAAK,IAAI,CAAC;AAAA,aAC5B,EAAE,QAAQ,MAAO,MAAK,MAAM,QAAQ,EAAE;AAAA,QAC1C;AACL,MAAE,eAAA;AAAA,EACJ;AAGA,QAAM,aAAa,KAAK,IAAI,GAAG,MAAM,UAAU,CAAC,OAAO,SAAS,GAAG,EAAE,CAAC,CAAC;AACvE,QAAM,UAAU,WAAW,iCAAiC;AAE5D,SACE,oBAAC,SAAI,MAAK,WAAU,cAAY,OAAO,wBAAsB,MAAM,YAAY,QAAW,WAAW,GAAG,EAAE,IAAI,IAAI,SAAS,IACxH,UAAA,MAAM,IAAI,CAAC,MAAM,MAAM;AACtB,UAAM,SAAS,SAAS,KAAK,EAAE;AAC/B,UAAM,OAAO,OAAO,KAAK,UAAU,WAAW,KAAK,QAAQ;AAC3D,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QAEC,KAAK,CAAC,OAAO;AACX,eAAK,QAAQ,CAAC,IAAI;AAAA,QACpB;AAAA,QACA,MAAK;AAAA,QACL,MAAK;AAAA,QACL,iBAAe;AAAA,QACf,cAAY,WAAW,OAAO;AAAA,QAC9B,YAAU,KAAK,QAAQ,WAAW,OAAO;AAAA,QACzC,kBAAgB,KAAK,MAAM,OAAO;AAAA,QAClC,UAAU,MAAM,aAAa,IAAI;AAAA,QACjC,UAAU,KAAK;AAAA,QACf,SAAS,CAAC,MAAM,OAAO,KAAK,IAAI,EAAE,WAAW,EAAE,WAAW,EAAE,QAAQ;AAAA,QACpE,WAAW,CAAC,MAAM,UAAU,GAAG,CAAC;AAAA,QAChC,WACE,sIAAsI,EAAE,GAAG,IAAI,YAAY,cAAc,SAAS,OAAO,qIAExL,SAAS,EAAE,SAAS,EAAE;AAAA,QAGxB,UAAA;AAAA,UAAA,KAAK;AAAA,UACL,CAAC,YAAY,KAAK;AAAA,UAClB,KAAK,UAAU,UACd;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,WAAW,6GACT,SAAS,EAAE,cAAc,0BAC3B;AAAA,cAEC,UAAA,KAAK;AAAA,YAAA;AAAA,UAAA;AAAA,QACR;AAAA,MAAA;AAAA,MA7BG,KAAK;AAAA,IAAA;AAAA,EAiChB,CAAC,EAAA,CACH;AAEJ;"}
|
|
1
|
+
{"version":3,"file":"Tabs.js","sources":["../../../../../../src/v1/primitives/navigation/Tabs/Tabs.tsx"],"sourcesContent":["import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'\r\nimport SelectInput from '../../forms/SelectInput/SelectInput'\r\nimport { SEGMENTED } from '../../buttons/SegmentedControl/segmentedStyles'\r\n\r\nexport interface TabItem<K extends string = string> {\r\n id: K\r\n /** Normalmente texto; aceita JSX. Com `iconOnly`, tem de ser texto (vai para o nome do botão e para o tooltip). */\r\n label: React.ReactNode\r\n /** Ícone antes do texto (ex: um ícone do lucide com `size={14}`). */\r\n icon?: React.ReactNode\r\n /** Número ao lado do texto (ex: pendentes). Não aparece se for `undefined`. */\r\n count?: number\r\n /** Explicação no tooltip (`data-tip`, com o `TooltipLayer`), com o `label` como título. */\r\n tip?: string\r\n disabled?: boolean\r\n}\r\n\r\ninterface TabsBase<K extends string> {\r\n items: TabItem<K>[]\r\n /** Nome do conjunto, para leitores de ecrã (ex: \"Estado das faturas\"). */\r\n label?: string\r\n /**\r\n * `underline` (por omissão): separadores da página, com a linha por baixo.\r\n * `segmented`: botões juntos numa caixa, para trocar de vista dentro de uma\r\n * secção (ex: Lista · Quadro).\r\n */\r\n variant?: 'underline' | 'segmented'\r\n /** Só os ícones; o `label` fica no tooltip e no nome do botão. */\r\n iconOnly?: boolean\r\n className?: string\r\n}\r\n\r\nexport interface TabsSingleProps<K extends string = string> extends TabsBase<K> {\r\n multiple?: false\r\n /** O separador escolhido. A app guarda este estado. */\r\n value: K\r\n onChange: (id: K) => void\r\n}\r\n\r\nexport interface TabsMultipleProps<K extends string = string> extends TabsBase<K> {\r\n /**\r\n * Vários escolhidos ao mesmo tempo: um clique escolhe só esse separador;\r\n * Ctrl/⌘/Shift + clique junta-o ou tira-o dos escolhidos.\r\n */\r\n multiple: true\r\n value: K[]\r\n /** Os escolhidos depois do clique, e o separador clicado. */\r\n onChange: (ids: K[], clicked: K) => void\r\n /** O separador \"Todos\": escolhê-lo limpa a escolha, e fica ativo quando não há nenhum escolhido. */\r\n allId?: K\r\n}\r\n\r\nexport type TabsProps<K extends string = string> = TabsSingleProps<K> | TabsMultipleProps<K>\r\n\r\nconst STYLES = {\r\n underline: {\r\n // No telemóvel numa só linha, a deslizar para o lado: partidos em várias linhas comiam o ecrã.\r\n list: 'flex flex-wrap border-b border-border max-md:flex-nowrap max-md:overflow-x-auto max-md:scrollbar-none',\r\n tab: '-mb-px h-11 shrink-0 border-0 border-b-[3px] px-4 max-md:px-3',\r\n active: 'border-primary text-primary dark:border-foreground dark:text-foreground',\r\n idle: 'border-transparent bg-transparent text-muted-foreground hover:text-primary dark:hover:text-foreground',\r\n countActive: 'bg-primary text-primary-foreground dark:bg-proef-blue dark:text-white',\r\n },\r\n // O mesmo aspeto do SegmentedControl (definido num só sítio).\r\n segmented: {\r\n list: SEGMENTED.list,\r\n tab: 'h-[26px] rounded-md border-0',\r\n active: SEGMENTED.active,\r\n idle: SEGMENTED.idle,\r\n countActive: 'bg-primary-foreground/20',\r\n },\r\n}\r\n\r\n/**\r\n * Separadores: escolher uma de várias vistas do mesmo ecrã.\r\n *\r\n * Só desenha os botões; o conteúdo de cada vista fica na app, que mostra o\r\n * que corresponde a `value`. Setas ← → (e Home/End) mudam de separador,\r\n * saltando os desativados. O escolhido fica a navy (não turquesa: turquesa\r\n * sobre branco não tem contraste para texto).\r\n *\r\n * Com `multiple`, vários podem estar escolhidos (ex: estados de uma lista).\r\n */\r\nexport default function Tabs<K extends string = string>(props: TabsProps<K>) {\r\n const { items, label, variant = 'underline', iconOnly = false, className = '' } = props\r\n const refs = useRef<(HTMLButtonElement | null)[]>([])\r\n const s = STYLES[variant]\r\n\r\n const isActive = (id: K) => {\r\n if (!props.multiple) return id === props.value\r\n if (props.allId !== undefined && id === props.allId) return props.value.length === 0\r\n return props.value.includes(id)\r\n }\r\n\r\n const select = (id: K, additive: boolean) => {\r\n if (!props.multiple) return props.onChange(id)\r\n if (props.allId !== undefined && id === props.allId) return props.onChange([], id)\r\n if (!additive) return props.onChange([id], id)\r\n props.onChange(props.value.includes(id) ? props.value.filter((v) => v !== id) : [...props.value, id], id)\r\n }\r\n\r\n const move = (from: number, step: number) => {\r\n const n = items.length\r\n for (let i = 1; i <= n; i++) {\r\n const next = (((from + step * i) % n) + n) % n\r\n if (!items[next].disabled) {\r\n select(items[next].id, false)\r\n refs.current[next]?.focus()\r\n return\r\n }\r\n }\r\n }\r\n\r\n const onKeyDown = (e: React.KeyboardEvent, index: number) => {\r\n if (e.key === 'ArrowRight') move(index, 1)\r\n else if (e.key === 'ArrowLeft') move(index, -1)\r\n else if (e.key === 'Home') move(-1, 1)\r\n else if (e.key === 'End') move(items.length, -1)\r\n else return\r\n e.preventDefault()\r\n }\r\n\r\n // O primeiro escolhido entra no Tab do teclado (ou o primeiro, se nenhum).\r\n const focusIndex = Math.max(0, items.findIndex((it) => isActive(it.id)))\r\n\r\n // A deslizar (telemóvel), o escolhido fica à vista. Só mexe na fila, nunca na página.\r\n const listRef = useRef<HTMLDivElement>(null)\r\n useEffect(() => {\r\n const list = listRef.current\r\n const tab = refs.current[focusIndex]\r\n if (!list || !tab || list.scrollWidth <= list.clientWidth) return\r\n const l = list.getBoundingClientRect()\r\n const r = tab.getBoundingClientRect()\r\n if (r.left < l.left) list.scrollLeft -= l.left - r.left\r\n else if (r.right > l.right) list.scrollLeft += r.right - l.right\r\n }, [focusIndex])\r\n const padding = iconOnly ? 'w-[26px] justify-center px-0' : 'px-3'\r\n\r\n // No telemóvel (fila sem quebra) os separadores que não cabem passam a uma lista de escolha: cortados ou a\r\n // deslizar, os últimos ficavam escondidos. A fila continua desenhada (invisível) só para se medir; ao alargar\r\n // o ecrã, se voltarem a caber, voltam os separadores. Só nos `underline` (os `segmented` são curtos).\r\n const [naoCabe, setNaoCabe] = useState(false)\r\n useLayoutEffect(() => {\r\n const list = listRef.current\r\n if (!list || variant !== 'underline') return\r\n const medir = () => setNaoCabe(list.scrollWidth > list.clientWidth + 1)\r\n medir()\r\n if (typeof ResizeObserver === 'undefined') return\r\n const ro = new ResizeObserver(medir)\r\n ro.observe(list)\r\n return () => ro.disconnect()\r\n }, [variant, items])\r\n\r\n const texto = (item: TabItem<K>) =>\r\n `${typeof item.label === 'string' ? item.label : item.id}${item.count !== undefined ? ` (${item.count})` : ''}`\r\n const lista = naoCabe && (\r\n props.multiple ? (\r\n <SelectInput\r\n aria-label={label}\r\n multiple\r\n values={props.value}\r\n onValuesChange={(ids) => {\r\n const antes = new Set<string>(props.value)\r\n const clicado = (ids.find((id) => !antes.has(id)) ?? props.value.find((id) => !ids.includes(id))) as K | undefined\r\n props.onChange(ids as K[], (clicado ?? props.allId ?? items[0].id) as K)\r\n }}\r\n options={items.filter((it) => it.id !== props.allId).map((it) => ({ value: it.id, label: texto(it), disabled: it.disabled }))}\r\n placeholder={props.allId !== undefined ? texto(items.find((it) => it.id === props.allId)!) : undefined}\r\n />\r\n ) : (\r\n <SelectInput\r\n aria-label={label}\r\n value={props.value}\r\n onValueChange={(v) => props.onChange(v as K)}\r\n options={items.map((it) => ({ value: it.id, label: texto(it), disabled: it.disabled }))}\r\n />\r\n )\r\n )\r\n\r\n return (\r\n <div className={`relative ${className}`}>\r\n {lista}\r\n <div\r\n ref={listRef}\r\n role=\"tablist\"\r\n aria-label={label}\r\n aria-multiselectable={props.multiple || undefined}\r\n aria-hidden={naoCabe || undefined}\r\n className={`${s.list} ${naoCabe ? 'pointer-events-none invisible absolute inset-x-0 top-0' : ''}`}\r\n >\r\n {items.map((item, i) => {\r\n const active = isActive(item.id)\r\n const text = typeof item.label === 'string' ? item.label : undefined\r\n return (\r\n <button\r\n key={item.id}\r\n ref={(el) => {\r\n refs.current[i] = el\r\n }}\r\n type=\"button\"\r\n role=\"tab\"\r\n aria-selected={active}\r\n aria-label={iconOnly ? text : undefined}\r\n data-tip={item.tip ?? (iconOnly ? text : undefined)}\r\n data-tip-title={item.tip ? text : undefined}\r\n tabIndex={i === focusIndex ? 0 : -1}\r\n disabled={item.disabled}\r\n onClick={(e) => select(item.id, e.ctrlKey || e.metaKey || e.shiftKey)}\r\n onKeyDown={(e) => onKeyDown(e, i)}\r\n className={\r\n `inline-flex cursor-pointer items-center gap-2 text-caption font-black tracking-wider whitespace-nowrap uppercase transition-colors ${s.tab} ${variant === 'underline' ? '' : padding} ` +\r\n 'focus-visible:ring-2 focus-visible:ring-primary/30 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-40 ' +\r\n (active ? s.active : s.idle)\r\n }\r\n >\r\n {item.icon}\r\n {!iconOnly && item.label}\r\n {item.count !== undefined && (\r\n <span\r\n className={`min-w-[18px] rounded-full px-1.5 py-px text-center text-micro font-extrabold tracking-normal tabular-nums ${\r\n active ? s.countActive : 'bg-muted text-foreground'\r\n }`}\r\n >\r\n {item.count}\r\n </span>\r\n )}\r\n </button>\r\n )\r\n })}\r\n </div>\r\n </div>\r\n )\r\n}\r\n"],"names":[],"mappings":"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"}
|