@proef/common-ui 2.2.6 → 2.5.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/index.js +6 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/legacy/components/charts/Charts.js +52 -192
- package/dist/esm/legacy/components/charts/Charts.js.map +1 -1
- package/dist/esm/legacy/components/molecules/DataTable/DataTable.js +252 -115
- package/dist/esm/legacy/components/molecules/DataTable/DataTable.js.map +1 -1
- package/dist/esm/legacy/components/organisms/GanttChart/GanttChart.js +7 -4
- package/dist/esm/legacy/components/organisms/GanttChart/GanttChart.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/Charts/Charts.js +301 -0
- package/dist/esm/v1/components/data/Charts/Charts.js.map +1 -0
- 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 +5 -2
- package/dist/esm/v1/components/overlays/Dialog/Dialog.js.map +1 -1
- package/dist/esm/v1/components/tables/Table/ColumnFilter.js +3 -1
- package/dist/esm/v1/components/tables/Table/ColumnFilter.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 +31 -9
- 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/theme.css +28 -0
- package/dist/types/legacy/components/charts/Charts.d.ts +14 -11
- package/dist/types/legacy/components/molecules/DataTable/DataTable.d.ts +12 -1
- package/dist/types/legacy/components/organisms/GanttChart/GanttChart.d.ts +15 -1
- package/dist/types/legacy/exports/organisms.d.ts +1 -1
- package/dist/types/v1/components/data/Charts/Charts.d.ts +103 -0
- package/dist/types/v1/index.d.ts +2 -0
- 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 +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 className={`fixed inset-0 z-[200] items-center justify-center p-dialog-margin ${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 `relative flex max-h-full w-full ${WIDTHS[size]} flex-col overflow-hidden rounded-2xl bg-card text-foreground shadow-modal outline-none ` +\r\n `motion-safe:animate-dialog-in ${height === 'full' ? 'h-full' : ''}`\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\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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"}
|
|
@@ -2,6 +2,7 @@ import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
|
2
2
|
import { useRef, useId, useState, useMemo, useEffect } from "react";
|
|
3
3
|
import { createPortal } from "react-dom";
|
|
4
4
|
import { SlidersIcon, ListIcon, ChevronUpIcon, ChevronDownIcon, CheckIcon } from "../../../foundations/icons/icons.js";
|
|
5
|
+
import { useDialogContainer } from "../../overlays/Dialog/Dialog.js";
|
|
5
6
|
const ALTURA = 420;
|
|
6
7
|
const LARGURA = 320;
|
|
7
8
|
function ColumnFilter({
|
|
@@ -24,6 +25,7 @@ function ColumnFilter({
|
|
|
24
25
|
onFechar
|
|
25
26
|
}) {
|
|
26
27
|
const caixa = useRef(null);
|
|
28
|
+
const dialogContainer = useDialogContainer();
|
|
27
29
|
const idMin = useId();
|
|
28
30
|
const [busca, setBusca] = useState("");
|
|
29
31
|
const [rascunho, setRascunho] = useState(() => new Set(seleccionados));
|
|
@@ -283,7 +285,7 @@ function ColumnFilter({
|
|
|
283
285
|
]
|
|
284
286
|
}
|
|
285
287
|
),
|
|
286
|
-
document.body
|
|
288
|
+
dialogContainer ?? document.body
|
|
287
289
|
);
|
|
288
290
|
}
|
|
289
291
|
export {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ColumnFilter.js","sources":["../../../../../../src/v1/components/tables/Table/ColumnFilter.tsx"],"sourcesContent":["import React, { useEffect, useId, useMemo, useRef, useState } from 'react'\r\nimport { createPortal } from 'react-dom'\r\nimport { CheckIcon, ChevronDownIcon, ChevronUpIcon, ListIcon, SlidersIcon } from '../../../foundations/icons/icons'\r\nimport type { TableLabels } from './labels'\r\n\r\n/** Onde o popup abre e quanto pode crescer. */\r\nconst ALTURA = 420\r\nconst LARGURA = 320\r\n\r\n/**\r\n * O filtro de uma coluna: ordenar, procurar, escolher valores, aplicar.\r\n *\r\n * Vive fora do `Table` de propósito. Lá dentro era uma função declarada no\r\n * corpo do componente, logo uma função NOVA a cada render — e o React\r\n * desmontava e remontava o popup a cada tecla escrita e a cada opção marcada.\r\n * Era isso que devolvia a lista de valores ao topo a meio de uma escolha.\r\n *\r\n * Nada se aplica até ao \"Aplicar\". Com a aplicação imediata, cada visto refazia\r\n * a tabela por baixo: mudava-lhe o tamanho e o conteúdo enquanto se escolhia, e\r\n * a lista de onde se estava a escolher encolhia à medida que se escolhia.\r\n *\r\n * `position: fixed` no `<body>`: como filho da célula, o `overflow` da tabela\r\n * cortava-o — e era por isso que a caixa da tabela tinha de perder o tecto de\r\n * altura enquanto um filtro estivesse aberto, o que empurrava a página inteira.\r\n */\r\nexport function ColumnFilter({\r\n labels: L, label, canSort, tipo, valores, seleccionados, intervalo, numero, ordenadaPor, aCarregar, ancora,\r\n onOrdenar, onAplicar, onAplicarDatas, onAplicarNumeros, onLimpar, onFechar,\r\n}: {\r\n labels: TableLabels\r\n label?: string\r\n canSort?: boolean\r\n /** `texto` = lista de valores; `data` = De/Até; `numero` = Mín/Máx, ou a lista (botão no topo). */\r\n tipo: 'texto' | 'data' | 'numero'\r\n /** Valores distintos da coluna, já contados com os outros filtros activos. */\r\n valores: string[]\r\n /** O que está aplicado neste momento — o ponto de partida do rascunho. */\r\n seleccionados: Set<string>\r\n intervalo: { from?: string; to?: string }\r\n /** Intervalo numérico aplicado, quando `tipo` é `numero`. */\r\n numero?: { min?: string; max?: string }\r\n ordenadaPor: 'asc' | 'desc' | null\r\n aCarregar: boolean\r\n /** Rect do botão que abriu isto. */\r\n ancora: DOMRect\r\n onOrdenar?: (dir: 'asc' | 'desc') => void\r\n onAplicar: (vals: Set<string>) => void\r\n onAplicarDatas?: (de: string, ate: string) => void\r\n onAplicarNumeros?: (min: string, max: string) => void\r\n onLimpar: () => void\r\n onFechar: () => void\r\n}) {\r\n const caixa = useRef<HTMLDivElement>(null)\r\n const idMin = useId()\r\n const [busca, setBusca] = useState('')\r\n /** O que está escolhido AGORA, sem ter saído daqui. Só o \"Aplicar\" o leva lá fora. */\r\n const [rascunho, setRascunho] = useState<Set<string>>(() => new Set(seleccionados))\r\n const [de, setDe] = useState(intervalo.from ?? '')\r\n const [ate, setAte] = useState(intervalo.to ?? '')\r\n const [min, setMin] = useState(numero?.min ?? '')\r\n const [max, setMax] = useState(numero?.max ?? '')\r\n /* Numa coluna de números, Mín/Máx ou a lista de valores. Abre na lista, salvo\r\n se já houver Mín/Máx aplicado: quem filtrou por intervalo e volta a abrir\r\n quer ver o intervalo. */\r\n const [modoLista, setModoLista] = useState(tipo === 'numero' && !numero?.min && !numero?.max)\r\n const lista = tipo === 'texto' || (tipo === 'numero' && modoLista)\r\n\r\n const visiveis = useMemo(() => {\r\n const achados = valores.filter(v => v.toLowerCase().includes(busca.toLowerCase()))\r\n // Por valor e não por texto: \"10\" antes de \"2\" numa lista de números é ruído.\r\n if (tipo !== 'numero') return achados\r\n return [...achados].sort((a, b) => {\r\n const na = Number(a), nb = Number(b)\r\n const va = a !== '—' && Number.isFinite(na), vb = b !== '—' && Number.isFinite(nb)\r\n if (va && vb) return na - nb\r\n return va ? -1 : vb ? 1 : a.localeCompare(b)\r\n })\r\n }, [valores, busca, tipo])\r\n const todosVisiveis = visiveis.length > 0 && visiveis.every(v => rascunho.has(v))\r\n const algunsVisiveis = visiveis.some(v => rascunho.has(v)) && !todosVisiveis\r\n\r\n /**\r\n * Marca ou desmarca os valores À VISTA — os que a pesquisa deixou passar.\r\n *\r\n * Com pesquisa activa, \"tudo\" é o que está no ecrã: escolher de uma vez os\r\n * valores que começam por \"Norte\" é o gesto que isto serve, e mexer nos\r\n * outros quarenta por baixo era o contrário do que se pediu.\r\n */\r\n const alternarTodos = () => setRascunho(prev => {\r\n const n = new Set(prev)\r\n if (todosVisiveis) for (const v of visiveis) n.delete(v)\r\n else for (const v of visiveis) n.add(v)\r\n return n\r\n })\r\n\r\n const alternar = (v: string) => setRascunho(prev => {\r\n const n = new Set(prev)\r\n if (n.has(v)) n.delete(v)\r\n else n.add(v)\r\n return n\r\n })\r\n\r\n const mesmoConjunto = (a: Set<string>, b: Set<string>) =>\r\n a.size === b.size && [...a].every(v => b.has(v))\r\n const sujo = tipo === 'data'\r\n ? de !== (intervalo.from ?? '') || ate !== (intervalo.to ?? '')\r\n : tipo === 'numero' && !modoLista\r\n ? min !== (numero?.min ?? '') || max !== (numero?.max ?? '')\r\n : !mesmoConjunto(rascunho, seleccionados)\r\n const temFiltro = seleccionados.size > 0 || !!intervalo.from || !!intervalo.to || !!numero?.min || !!numero?.max\r\n\r\n useEffect(() => {\r\n const fora = (e: MouseEvent) => {\r\n if (caixa.current && !caixa.current.contains(e.target as Node)) onFechar()\r\n }\r\n const tecla = (e: KeyboardEvent) => { if (e.key === 'Escape') onFechar() }\r\n /*\r\n Rolar FORA fecha; rolar DENTRO da lista não.\r\n A caixa está presa a coordenadas do ecrã, logo tem de fechar quando a\r\n página se move por baixo dela — mas a própria lista rola (podem ser\r\n dezenas de valores), e o `capture` também apanha esse.\r\n */\r\n const aoRolar = (e: Event) => {\r\n if (caixa.current && e.target instanceof Node && caixa.current.contains(e.target)) return\r\n onFechar()\r\n }\r\n document.addEventListener('mousedown', fora)\r\n document.addEventListener('keydown', tecla)\r\n window.addEventListener('scroll', aoRolar, true)\r\n window.addEventListener('resize', onFechar)\r\n return () => {\r\n document.removeEventListener('mousedown', fora)\r\n document.removeEventListener('keydown', tecla)\r\n window.removeEventListener('scroll', aoRolar, true)\r\n window.removeEventListener('resize', onFechar)\r\n }\r\n }, [onFechar])\r\n\r\n /* Troca Mín/Máx ↔ lista. Vai na linha que já existe (a do \"Mínimo\", ou a da\r\n pesquisa) e não numa só para ele: uma linha inteira para um botão pequeno\r\n era espaço vazio à esquerda. */\r\n const botaoModo = tipo === 'numero' && (\r\n <button\r\n type=\"button\"\r\n onClick={e => { e.preventDefault(); setModoLista(m => !m) }}\r\n title={modoLista ? L.showRange : L.showList}\r\n className=\"inline-flex items-center gap-1 text-[9px] font-black uppercase tracking-widest text-slate-400 hover:text-proef-blue flex-shrink-0 whitespace-nowrap\"\r\n >\r\n {modoLista ? <SlidersIcon size={10} /> : <ListIcon size={10} />}\r\n {modoLista ? L.showRange : L.showList}\r\n </button>\r\n )\r\n\r\n // Abre para cima quando não há espaço por baixo — nas colunas da direita e\r\n // com a janela baixa, o que ficava de fora eram os botões de aplicar.\r\n const paraCima = window.innerHeight - ancora.bottom < ALTURA && ancora.top > ALTURA\r\n\r\n return createPortal(\r\n <div\r\n ref={caixa}\r\n style={{\r\n position: 'fixed',\r\n left: Math.max(4, Math.min(ancora.left, window.innerWidth - LARGURA - 4)),\r\n ...(paraCima ? { bottom: window.innerHeight - ancora.top + 2 } : { top: ancora.bottom + 2 }),\r\n width: LARGURA,\r\n maxHeight: ALTURA,\r\n }}\r\n className=\"v1-reset z-[250] flex flex-col overflow-hidden rounded-lg bg-white border border-slate-200 shadow-lg\"\r\n >\r\n {label && (\r\n <div className=\"px-3 pt-2.5 pb-2 border-b border-slate-100 flex-shrink-0\">\r\n <span className=\"text-[9px] font-black uppercase tracking-widest text-proef-blue\">{label}</span>\r\n </div>\r\n )}\r\n {aCarregar && (\r\n <div className=\"px-3 py-1.5 bg-amber-50 border-b border-amber-100 flex items-center gap-1.5 text-micro text-amber-700 flex-shrink-0\">\r\n <span aria-hidden className=\"inline-block h-2.5 w-2.5 animate-spin rounded-full border-2 border-current border-t-transparent \" />\r\n {L.loadingOptions}\r\n </div>\r\n )}\r\n {canSort && onOrdenar && (\r\n <div className=\"border-b border-slate-100 p-1.5 space-y-0.5 flex-shrink-0\">\r\n <button\r\n onClick={() => onOrdenar('asc')}\r\n className={`w-full flex items-center gap-2 rounded-md px-2.5 py-1.5 text-caption font-semibold text-left hover:bg-slate-50 transition-colors ${ordenadaPor === 'asc' ? 'text-proef-blue bg-slate-50' : 'text-slate-600'}`}\r\n >\r\n <ChevronUpIcon size={11} /> {tipo === 'data' ? L.sortDateAsc : tipo === 'numero' ? L.sortNumAsc : L.sortAsc}\r\n </button>\r\n <button\r\n onClick={() => onOrdenar('desc')}\r\n className={`w-full flex items-center gap-2 rounded-md px-2.5 py-1.5 text-caption font-semibold text-left hover:bg-slate-50 transition-colors ${ordenadaPor === 'desc' ? 'text-proef-blue bg-slate-50' : 'text-slate-600'}`}\r\n >\r\n <ChevronDownIcon size={11} /> {tipo === 'data' ? L.sortDateDesc : tipo === 'numero' ? L.sortNumDesc : L.sortDesc}\r\n </button>\r\n </div>\r\n )}\r\n\r\n {tipo === 'data' ? (\r\n <div className=\"p-3 space-y-2\">\r\n <label className=\"block\">\r\n <span className=\"text-[9px] font-black uppercase tracking-widest text-slate-400\">{L.dateFrom}</span>\r\n <input\r\n type=\"date\" value={de} onChange={e => setDe(e.target.value)}\r\n onKeyDown={e => { if (e.key === 'Enter') onAplicarDatas?.(de, ate) }}\r\n className=\"w-full mt-0.5 rounded-md bg-slate-50 border border-slate-100 px-2.5 py-1.5 text-xs outline-none focus:border-proef-blue\"\r\n />\r\n </label>\r\n <label className=\"block\">\r\n <span className=\"text-[9px] font-black uppercase tracking-widest text-slate-400\">{L.dateTo}</span>\r\n <input\r\n type=\"date\" value={ate} onChange={e => setAte(e.target.value)}\r\n onKeyDown={e => { if (e.key === 'Enter') onAplicarDatas?.(de, ate) }}\r\n className=\"w-full mt-0.5 rounded-md bg-slate-50 border border-slate-100 px-2.5 py-1.5 text-xs outline-none focus:border-proef-blue\"\r\n />\r\n </label>\r\n </div>\r\n ) : !lista ? (\r\n /* Mín/Máx em vez da lista: numa coluna de dinheiro ou de contagem há\r\n tantos valores distintos quantas as linhas, e ninguém procura \"10.04\".\r\n Vazio de um dos lados vale \"sem limite desse lado\". */\r\n <div className=\"p-3 space-y-2\">\r\n {/* O botão da lista fica FORA da etiqueta: dentro dela, era o botão\r\n (e não a caixa) que recebia o nome \"Mínimo\", e a caixa ficava sem\r\n nome nenhum para os leitores de ecrã. */}\r\n <div>\r\n <span className=\"flex items-center justify-between\">\r\n <label htmlFor={idMin} className=\"text-[9px] font-black uppercase tracking-widest text-slate-400\">{L.min}</label>\r\n {botaoModo}\r\n </span>\r\n <input\r\n id={idMin}\r\n type=\"number\" value={min} onChange={e => setMin(e.target.value)}\r\n onKeyDown={e => { if (e.key === 'Enter') onAplicarNumeros?.(min, max) }}\r\n className=\"w-full mt-0.5 rounded-md bg-slate-50 border border-slate-100 px-2.5 py-1.5 text-xs outline-none focus:border-proef-blue\"\r\n />\r\n </div>\r\n <label className=\"block\">\r\n <span className=\"text-[9px] font-black uppercase tracking-widest text-slate-400\">{L.max}</span>\r\n <input\r\n type=\"number\" value={max} onChange={e => setMax(e.target.value)}\r\n onKeyDown={e => { if (e.key === 'Enter') onAplicarNumeros?.(min, max) }}\r\n className=\"w-full mt-0.5 rounded-md bg-slate-50 border border-slate-100 px-2.5 py-1.5 text-xs outline-none focus:border-proef-blue\"\r\n />\r\n </label>\r\n </div>\r\n ) : (\r\n <>\r\n <div className=\"p-1.5 flex-shrink-0 flex items-center gap-2\">\r\n <input\r\n autoFocus type=\"text\" value={busca} onChange={e => setBusca(e.target.value)} placeholder={L.search}\r\n className=\"flex-1 min-w-0 rounded-md bg-slate-50 border border-slate-100 px-2.5 py-1.5 text-xs outline-none focus:border-proef-blue\"\r\n />\r\n {botaoModo && <span className=\"pr-1.5\">{botaoModo}</span>}\r\n </div>\r\n {/* \"Tudo\" à cabeça da lista, como no Excel: marcar tudo e tirar o que\r\n não se quer é mais rápido do que marcar quarenta à mão. */}\r\n {visiveis.length > 0 && (\r\n <button\r\n onClick={alternarTodos}\r\n className=\"flex items-center gap-2 px-3 py-1.5 border-y border-slate-100 hover:bg-slate-50 text-left flex-shrink-0\"\r\n >\r\n <div className={`w-3.5 h-3.5 flex items-center justify-center rounded-sm border flex-shrink-0 transition-colors ${\r\n todosVisiveis ? 'bg-primary border-primary'\r\n : algunsVisiveis ? 'bg-primary/30 border-primary'\r\n : 'border-slate-300'\r\n }`}>\r\n {todosVisiveis && <CheckIcon size={9} strokeWidth={3} className=\"text-white\" />}\r\n </div>\r\n <span className=\"text-caption font-semibold text-slate-600\">\r\n {busca ? L.selectAllFound(visiveis.length) : L.selectAll}\r\n </span>\r\n </button>\r\n )}\r\n <div className=\"flex-1 overflow-y-auto p-1.5 space-y-0.5 min-h-0\">\r\n {visiveis.length === 0\r\n ? <p className=\"text-caption text-slate-400 text-center py-3\">{L.noResults}</p>\r\n : visiveis.map(v => {\r\n const escolhido = rascunho.has(v)\r\n return (\r\n <button key={v} onClick={() => alternar(v)} className=\"w-full flex items-center gap-2 rounded-md px-2 py-1.5 hover:bg-slate-50 text-left\">\r\n <div className={`w-3.5 h-3.5 flex items-center justify-center rounded-sm border flex-shrink-0 transition-colors ${escolhido ? 'bg-primary border-primary' : 'border-slate-300'}`}>\r\n {escolhido && <CheckIcon size={9} strokeWidth={3} className=\"text-white\" />}\r\n </div>\r\n <span className=\"text-xs text-slate-700 whitespace-normal break-words leading-snug\">{v}</span>\r\n </button>\r\n )\r\n })}\r\n </div>\r\n </>\r\n )}\r\n\r\n {/* Aplicar/Cancelar fora da zona que rola, sempre à vista: são eles que\r\n dizem que ainda não aconteceu nada. */}\r\n <div className=\"flex items-center gap-1.5 p-1.5 border-t border-slate-100 flex-shrink-0 bg-white\">\r\n <button\r\n onClick={() => {\r\n if (tipo === 'data') onAplicarDatas?.(de, ate)\r\n else if (tipo === 'numero' && !modoLista) onAplicarNumeros?.(min, max)\r\n else onAplicar(rascunho)\r\n }}\r\n disabled={!sujo}\r\n className=\"flex-1 text-micro font-black uppercase tracking-wider text-white bg-primary hover:bg-primary/90 rounded-md py-1.5 disabled:opacity-30\"\r\n >\r\n {L.apply}\r\n </button>\r\n <button\r\n onClick={onFechar}\r\n className=\"text-micro font-black uppercase tracking-wider text-slate-500 hover:text-slate-700 rounded-md px-2 py-1.5\"\r\n >\r\n {L.cancel}\r\n </button>\r\n {temFiltro && (\r\n <button\r\n onClick={onLimpar}\r\n className=\"text-micro font-black uppercase tracking-wider text-red-400 hover:text-red-500 rounded-md px-2 py-1.5\"\r\n >\r\n {L.clearFilter}\r\n </button>\r\n )}\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":"ColumnFilter.js","sources":["../../../../../../src/v1/components/tables/Table/ColumnFilter.tsx"],"sourcesContent":["import React, { useEffect, useId, useMemo, useRef, useState } from 'react'\r\nimport { createPortal } from 'react-dom'\r\nimport { CheckIcon, ChevronDownIcon, ChevronUpIcon, ListIcon, SlidersIcon } from '../../../foundations/icons/icons'\r\nimport type { TableLabels } from './labels'\r\nimport { useDialogContainer } from '../../overlays/Dialog/Dialog'\r\n\r\n/** Onde o popup abre e quanto pode crescer. */\r\nconst ALTURA = 420\r\nconst LARGURA = 320\r\n\r\n/**\r\n * O filtro de uma coluna: ordenar, procurar, escolher valores, aplicar.\r\n *\r\n * Vive fora do `Table` de propósito. Lá dentro era uma função declarada no\r\n * corpo do componente, logo uma função NOVA a cada render — e o React\r\n * desmontava e remontava o popup a cada tecla escrita e a cada opção marcada.\r\n * Era isso que devolvia a lista de valores ao topo a meio de uma escolha.\r\n *\r\n * Nada se aplica até ao \"Aplicar\". Com a aplicação imediata, cada visto refazia\r\n * a tabela por baixo: mudava-lhe o tamanho e o conteúdo enquanto se escolhia, e\r\n * a lista de onde se estava a escolher encolhia à medida que se escolhia.\r\n *\r\n * `position: fixed` no `<body>`: como filho da célula, o `overflow` da tabela\r\n * cortava-o — e era por isso que a caixa da tabela tinha de perder o tecto de\r\n * altura enquanto um filtro estivesse aberto, o que empurrava a página inteira.\r\n */\r\nexport function ColumnFilter({\r\n labels: L, label, canSort, tipo, valores, seleccionados, intervalo, numero, ordenadaPor, aCarregar, ancora,\r\n onOrdenar, onAplicar, onAplicarDatas, onAplicarNumeros, onLimpar, onFechar,\r\n}: {\r\n labels: TableLabels\r\n label?: string\r\n canSort?: boolean\r\n /** `texto` = lista de valores; `data` = De/Até; `numero` = Mín/Máx, ou a lista (botão no topo). */\r\n tipo: 'texto' | 'data' | 'numero'\r\n /** Valores distintos da coluna, já contados com os outros filtros activos. */\r\n valores: string[]\r\n /** O que está aplicado neste momento — o ponto de partida do rascunho. */\r\n seleccionados: Set<string>\r\n intervalo: { from?: string; to?: string }\r\n /** Intervalo numérico aplicado, quando `tipo` é `numero`. */\r\n numero?: { min?: string; max?: string }\r\n ordenadaPor: 'asc' | 'desc' | null\r\n aCarregar: boolean\r\n /** Rect do botão que abriu isto. */\r\n ancora: DOMRect\r\n onOrdenar?: (dir: 'asc' | 'desc') => void\r\n onAplicar: (vals: Set<string>) => void\r\n onAplicarDatas?: (de: string, ate: string) => void\r\n onAplicarNumeros?: (min: string, max: string) => void\r\n onLimpar: () => void\r\n onFechar: () => void\r\n}) {\r\n const caixa = useRef<HTMLDivElement>(null)\r\n // Numa tabela dentro de um Dialog, o filtro abre na janela (o resto da página está inert).\r\n const dialogContainer = useDialogContainer()\r\n const idMin = useId()\r\n const [busca, setBusca] = useState('')\r\n /** O que está escolhido AGORA, sem ter saído daqui. Só o \"Aplicar\" o leva lá fora. */\r\n const [rascunho, setRascunho] = useState<Set<string>>(() => new Set(seleccionados))\r\n const [de, setDe] = useState(intervalo.from ?? '')\r\n const [ate, setAte] = useState(intervalo.to ?? '')\r\n const [min, setMin] = useState(numero?.min ?? '')\r\n const [max, setMax] = useState(numero?.max ?? '')\r\n /* Numa coluna de números, Mín/Máx ou a lista de valores. Abre na lista, salvo\r\n se já houver Mín/Máx aplicado: quem filtrou por intervalo e volta a abrir\r\n quer ver o intervalo. */\r\n const [modoLista, setModoLista] = useState(tipo === 'numero' && !numero?.min && !numero?.max)\r\n const lista = tipo === 'texto' || (tipo === 'numero' && modoLista)\r\n\r\n const visiveis = useMemo(() => {\r\n const achados = valores.filter(v => v.toLowerCase().includes(busca.toLowerCase()))\r\n // Por valor e não por texto: \"10\" antes de \"2\" numa lista de números é ruído.\r\n if (tipo !== 'numero') return achados\r\n return [...achados].sort((a, b) => {\r\n const na = Number(a), nb = Number(b)\r\n const va = a !== '—' && Number.isFinite(na), vb = b !== '—' && Number.isFinite(nb)\r\n if (va && vb) return na - nb\r\n return va ? -1 : vb ? 1 : a.localeCompare(b)\r\n })\r\n }, [valores, busca, tipo])\r\n const todosVisiveis = visiveis.length > 0 && visiveis.every(v => rascunho.has(v))\r\n const algunsVisiveis = visiveis.some(v => rascunho.has(v)) && !todosVisiveis\r\n\r\n /**\r\n * Marca ou desmarca os valores À VISTA — os que a pesquisa deixou passar.\r\n *\r\n * Com pesquisa activa, \"tudo\" é o que está no ecrã: escolher de uma vez os\r\n * valores que começam por \"Norte\" é o gesto que isto serve, e mexer nos\r\n * outros quarenta por baixo era o contrário do que se pediu.\r\n */\r\n const alternarTodos = () => setRascunho(prev => {\r\n const n = new Set(prev)\r\n if (todosVisiveis) for (const v of visiveis) n.delete(v)\r\n else for (const v of visiveis) n.add(v)\r\n return n\r\n })\r\n\r\n const alternar = (v: string) => setRascunho(prev => {\r\n const n = new Set(prev)\r\n if (n.has(v)) n.delete(v)\r\n else n.add(v)\r\n return n\r\n })\r\n\r\n const mesmoConjunto = (a: Set<string>, b: Set<string>) =>\r\n a.size === b.size && [...a].every(v => b.has(v))\r\n const sujo = tipo === 'data'\r\n ? de !== (intervalo.from ?? '') || ate !== (intervalo.to ?? '')\r\n : tipo === 'numero' && !modoLista\r\n ? min !== (numero?.min ?? '') || max !== (numero?.max ?? '')\r\n : !mesmoConjunto(rascunho, seleccionados)\r\n const temFiltro = seleccionados.size > 0 || !!intervalo.from || !!intervalo.to || !!numero?.min || !!numero?.max\r\n\r\n useEffect(() => {\r\n const fora = (e: MouseEvent) => {\r\n if (caixa.current && !caixa.current.contains(e.target as Node)) onFechar()\r\n }\r\n const tecla = (e: KeyboardEvent) => { if (e.key === 'Escape') onFechar() }\r\n /*\r\n Rolar FORA fecha; rolar DENTRO da lista não.\r\n A caixa está presa a coordenadas do ecrã, logo tem de fechar quando a\r\n página se move por baixo dela — mas a própria lista rola (podem ser\r\n dezenas de valores), e o `capture` também apanha esse.\r\n */\r\n const aoRolar = (e: Event) => {\r\n if (caixa.current && e.target instanceof Node && caixa.current.contains(e.target)) return\r\n onFechar()\r\n }\r\n document.addEventListener('mousedown', fora)\r\n document.addEventListener('keydown', tecla)\r\n window.addEventListener('scroll', aoRolar, true)\r\n window.addEventListener('resize', onFechar)\r\n return () => {\r\n document.removeEventListener('mousedown', fora)\r\n document.removeEventListener('keydown', tecla)\r\n window.removeEventListener('scroll', aoRolar, true)\r\n window.removeEventListener('resize', onFechar)\r\n }\r\n }, [onFechar])\r\n\r\n /* Troca Mín/Máx ↔ lista. Vai na linha que já existe (a do \"Mínimo\", ou a da\r\n pesquisa) e não numa só para ele: uma linha inteira para um botão pequeno\r\n era espaço vazio à esquerda. */\r\n const botaoModo = tipo === 'numero' && (\r\n <button\r\n type=\"button\"\r\n onClick={e => { e.preventDefault(); setModoLista(m => !m) }}\r\n title={modoLista ? L.showRange : L.showList}\r\n className=\"inline-flex items-center gap-1 text-[9px] font-black uppercase tracking-widest text-slate-400 hover:text-proef-blue flex-shrink-0 whitespace-nowrap\"\r\n >\r\n {modoLista ? <SlidersIcon size={10} /> : <ListIcon size={10} />}\r\n {modoLista ? L.showRange : L.showList}\r\n </button>\r\n )\r\n\r\n // Abre para cima quando não há espaço por baixo — nas colunas da direita e\r\n // com a janela baixa, o que ficava de fora eram os botões de aplicar.\r\n const paraCima = window.innerHeight - ancora.bottom < ALTURA && ancora.top > ALTURA\r\n\r\n return createPortal(\r\n <div\r\n ref={caixa}\r\n style={{\r\n position: 'fixed',\r\n left: Math.max(4, Math.min(ancora.left, window.innerWidth - LARGURA - 4)),\r\n ...(paraCima ? { bottom: window.innerHeight - ancora.top + 2 } : { top: ancora.bottom + 2 }),\r\n width: LARGURA,\r\n maxHeight: ALTURA,\r\n }}\r\n className=\"v1-reset z-[250] flex flex-col overflow-hidden rounded-lg bg-white border border-slate-200 shadow-lg\"\r\n >\r\n {label && (\r\n <div className=\"px-3 pt-2.5 pb-2 border-b border-slate-100 flex-shrink-0\">\r\n <span className=\"text-[9px] font-black uppercase tracking-widest text-proef-blue\">{label}</span>\r\n </div>\r\n )}\r\n {aCarregar && (\r\n <div className=\"px-3 py-1.5 bg-amber-50 border-b border-amber-100 flex items-center gap-1.5 text-micro text-amber-700 flex-shrink-0\">\r\n <span aria-hidden className=\"inline-block h-2.5 w-2.5 animate-spin rounded-full border-2 border-current border-t-transparent \" />\r\n {L.loadingOptions}\r\n </div>\r\n )}\r\n {canSort && onOrdenar && (\r\n <div className=\"border-b border-slate-100 p-1.5 space-y-0.5 flex-shrink-0\">\r\n <button\r\n onClick={() => onOrdenar('asc')}\r\n className={`w-full flex items-center gap-2 rounded-md px-2.5 py-1.5 text-caption font-semibold text-left hover:bg-slate-50 transition-colors ${ordenadaPor === 'asc' ? 'text-proef-blue bg-slate-50' : 'text-slate-600'}`}\r\n >\r\n <ChevronUpIcon size={11} /> {tipo === 'data' ? L.sortDateAsc : tipo === 'numero' ? L.sortNumAsc : L.sortAsc}\r\n </button>\r\n <button\r\n onClick={() => onOrdenar('desc')}\r\n className={`w-full flex items-center gap-2 rounded-md px-2.5 py-1.5 text-caption font-semibold text-left hover:bg-slate-50 transition-colors ${ordenadaPor === 'desc' ? 'text-proef-blue bg-slate-50' : 'text-slate-600'}`}\r\n >\r\n <ChevronDownIcon size={11} /> {tipo === 'data' ? L.sortDateDesc : tipo === 'numero' ? L.sortNumDesc : L.sortDesc}\r\n </button>\r\n </div>\r\n )}\r\n\r\n {tipo === 'data' ? (\r\n <div className=\"p-3 space-y-2\">\r\n <label className=\"block\">\r\n <span className=\"text-[9px] font-black uppercase tracking-widest text-slate-400\">{L.dateFrom}</span>\r\n <input\r\n type=\"date\" value={de} onChange={e => setDe(e.target.value)}\r\n onKeyDown={e => { if (e.key === 'Enter') onAplicarDatas?.(de, ate) }}\r\n className=\"w-full mt-0.5 rounded-md bg-slate-50 border border-slate-100 px-2.5 py-1.5 text-xs outline-none focus:border-proef-blue\"\r\n />\r\n </label>\r\n <label className=\"block\">\r\n <span className=\"text-[9px] font-black uppercase tracking-widest text-slate-400\">{L.dateTo}</span>\r\n <input\r\n type=\"date\" value={ate} onChange={e => setAte(e.target.value)}\r\n onKeyDown={e => { if (e.key === 'Enter') onAplicarDatas?.(de, ate) }}\r\n className=\"w-full mt-0.5 rounded-md bg-slate-50 border border-slate-100 px-2.5 py-1.5 text-xs outline-none focus:border-proef-blue\"\r\n />\r\n </label>\r\n </div>\r\n ) : !lista ? (\r\n /* Mín/Máx em vez da lista: numa coluna de dinheiro ou de contagem há\r\n tantos valores distintos quantas as linhas, e ninguém procura \"10.04\".\r\n Vazio de um dos lados vale \"sem limite desse lado\". */\r\n <div className=\"p-3 space-y-2\">\r\n {/* O botão da lista fica FORA da etiqueta: dentro dela, era o botão\r\n (e não a caixa) que recebia o nome \"Mínimo\", e a caixa ficava sem\r\n nome nenhum para os leitores de ecrã. */}\r\n <div>\r\n <span className=\"flex items-center justify-between\">\r\n <label htmlFor={idMin} className=\"text-[9px] font-black uppercase tracking-widest text-slate-400\">{L.min}</label>\r\n {botaoModo}\r\n </span>\r\n <input\r\n id={idMin}\r\n type=\"number\" value={min} onChange={e => setMin(e.target.value)}\r\n onKeyDown={e => { if (e.key === 'Enter') onAplicarNumeros?.(min, max) }}\r\n className=\"w-full mt-0.5 rounded-md bg-slate-50 border border-slate-100 px-2.5 py-1.5 text-xs outline-none focus:border-proef-blue\"\r\n />\r\n </div>\r\n <label className=\"block\">\r\n <span className=\"text-[9px] font-black uppercase tracking-widest text-slate-400\">{L.max}</span>\r\n <input\r\n type=\"number\" value={max} onChange={e => setMax(e.target.value)}\r\n onKeyDown={e => { if (e.key === 'Enter') onAplicarNumeros?.(min, max) }}\r\n className=\"w-full mt-0.5 rounded-md bg-slate-50 border border-slate-100 px-2.5 py-1.5 text-xs outline-none focus:border-proef-blue\"\r\n />\r\n </label>\r\n </div>\r\n ) : (\r\n <>\r\n <div className=\"p-1.5 flex-shrink-0 flex items-center gap-2\">\r\n <input\r\n autoFocus type=\"text\" value={busca} onChange={e => setBusca(e.target.value)} placeholder={L.search}\r\n className=\"flex-1 min-w-0 rounded-md bg-slate-50 border border-slate-100 px-2.5 py-1.5 text-xs outline-none focus:border-proef-blue\"\r\n />\r\n {botaoModo && <span className=\"pr-1.5\">{botaoModo}</span>}\r\n </div>\r\n {/* \"Tudo\" à cabeça da lista, como no Excel: marcar tudo e tirar o que\r\n não se quer é mais rápido do que marcar quarenta à mão. */}\r\n {visiveis.length > 0 && (\r\n <button\r\n onClick={alternarTodos}\r\n className=\"flex items-center gap-2 px-3 py-1.5 border-y border-slate-100 hover:bg-slate-50 text-left flex-shrink-0\"\r\n >\r\n <div className={`w-3.5 h-3.5 flex items-center justify-center rounded-sm border flex-shrink-0 transition-colors ${\r\n todosVisiveis ? 'bg-primary border-primary'\r\n : algunsVisiveis ? 'bg-primary/30 border-primary'\r\n : 'border-slate-300'\r\n }`}>\r\n {todosVisiveis && <CheckIcon size={9} strokeWidth={3} className=\"text-white\" />}\r\n </div>\r\n <span className=\"text-caption font-semibold text-slate-600\">\r\n {busca ? L.selectAllFound(visiveis.length) : L.selectAll}\r\n </span>\r\n </button>\r\n )}\r\n <div className=\"flex-1 overflow-y-auto p-1.5 space-y-0.5 min-h-0\">\r\n {visiveis.length === 0\r\n ? <p className=\"text-caption text-slate-400 text-center py-3\">{L.noResults}</p>\r\n : visiveis.map(v => {\r\n const escolhido = rascunho.has(v)\r\n return (\r\n <button key={v} onClick={() => alternar(v)} className=\"w-full flex items-center gap-2 rounded-md px-2 py-1.5 hover:bg-slate-50 text-left\">\r\n <div className={`w-3.5 h-3.5 flex items-center justify-center rounded-sm border flex-shrink-0 transition-colors ${escolhido ? 'bg-primary border-primary' : 'border-slate-300'}`}>\r\n {escolhido && <CheckIcon size={9} strokeWidth={3} className=\"text-white\" />}\r\n </div>\r\n <span className=\"text-xs text-slate-700 whitespace-normal break-words leading-snug\">{v}</span>\r\n </button>\r\n )\r\n })}\r\n </div>\r\n </>\r\n )}\r\n\r\n {/* Aplicar/Cancelar fora da zona que rola, sempre à vista: são eles que\r\n dizem que ainda não aconteceu nada. */}\r\n <div className=\"flex items-center gap-1.5 p-1.5 border-t border-slate-100 flex-shrink-0 bg-white\">\r\n <button\r\n onClick={() => {\r\n if (tipo === 'data') onAplicarDatas?.(de, ate)\r\n else if (tipo === 'numero' && !modoLista) onAplicarNumeros?.(min, max)\r\n else onAplicar(rascunho)\r\n }}\r\n disabled={!sujo}\r\n className=\"flex-1 text-micro font-black uppercase tracking-wider text-white bg-primary hover:bg-primary/90 rounded-md py-1.5 disabled:opacity-30\"\r\n >\r\n {L.apply}\r\n </button>\r\n <button\r\n onClick={onFechar}\r\n className=\"text-micro font-black uppercase tracking-wider text-slate-500 hover:text-slate-700 rounded-md px-2 py-1.5\"\r\n >\r\n {L.cancel}\r\n </button>\r\n {temFiltro && (\r\n <button\r\n onClick={onLimpar}\r\n className=\"text-micro font-black uppercase tracking-wider text-red-400 hover:text-red-500 rounded-md px-2 py-1.5\"\r\n >\r\n {L.clearFilter}\r\n </button>\r\n )}\r\n </div>\r\n </div>,\r\n dialogContainer ?? document.body,\r\n )\r\n}\r\n"],"names":[],"mappings":";;;;;AAOA,MAAM,SAAS;AACf,MAAM,UAAU;AAkBT,SAAS,aAAa;AAAA,EAC3B,QAAQ;AAAA,EAAG;AAAA,EAAO;AAAA,EAAS;AAAA,EAAM;AAAA,EAAS;AAAA,EAAe;AAAA,EAAW;AAAA,EAAQ;AAAA,EAAa;AAAA,EAAW;AAAA,EACpG;AAAA,EAAW;AAAA,EAAW;AAAA,EAAgB;AAAA,EAAkB;AAAA,EAAU;AACpE,GAuBG;AACD,QAAM,QAAQ,OAAuB,IAAI;AAEzC,QAAM,kBAAkB,mBAAA;AACxB,QAAM,QAAQ,MAAA;AACd,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,EAAE;AAErC,QAAM,CAAC,UAAU,WAAW,IAAI,SAAsB,MAAM,IAAI,IAAI,aAAa,CAAC;AAClF,QAAM,CAAC,IAAI,KAAK,IAAI,SAAS,UAAU,QAAQ,EAAE;AACjD,QAAM,CAAC,KAAK,MAAM,IAAI,SAAS,UAAU,MAAM,EAAE;AACjD,QAAM,CAAC,KAAK,MAAM,IAAI,SAAS,QAAQ,OAAO,EAAE;AAChD,QAAM,CAAC,KAAK,MAAM,IAAI,SAAS,QAAQ,OAAO,EAAE;AAIhD,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,SAAS,YAAY,CAAC,QAAQ,OAAO,CAAC,QAAQ,GAAG;AAC5F,QAAM,QAAQ,SAAS,WAAY,SAAS,YAAY;AAExD,QAAM,WAAW,QAAQ,MAAM;AAC7B,UAAM,UAAU,QAAQ,OAAO,CAAA,MAAK,EAAE,YAAA,EAAc,SAAS,MAAM,YAAA,CAAa,CAAC;AAEjF,QAAI,SAAS,SAAU,QAAO;AAC9B,WAAO,CAAC,GAAG,OAAO,EAAE,KAAK,CAAC,GAAG,MAAM;AACjC,YAAM,KAAK,OAAO,CAAC,GAAG,KAAK,OAAO,CAAC;AACnC,YAAM,KAAK,MAAM,OAAO,OAAO,SAAS,EAAE,GAAG,KAAK,MAAM,OAAO,OAAO,SAAS,EAAE;AACjF,UAAI,MAAM,GAAI,QAAO,KAAK;AAC1B,aAAO,KAAK,KAAK,KAAK,IAAI,EAAE,cAAc,CAAC;AAAA,IAC7C,CAAC;AAAA,EACH,GAAG,CAAC,SAAS,OAAO,IAAI,CAAC;AACzB,QAAM,gBAAgB,SAAS,SAAS,KAAK,SAAS,MAAM,CAAA,MAAK,SAAS,IAAI,CAAC,CAAC;AAChF,QAAM,iBAAiB,SAAS,KAAK,CAAA,MAAK,SAAS,IAAI,CAAC,CAAC,KAAK,CAAC;AAS/D,QAAM,gBAAgB,MAAM,YAAY,CAAA,SAAQ;AAC9C,UAAM,IAAI,IAAI,IAAI,IAAI;AACtB,QAAI,cAAe,YAAW,KAAK,SAAU,GAAE,OAAO,CAAC;AAAA,QAClD,YAAW,KAAK,SAAU,GAAE,IAAI,CAAC;AACtC,WAAO;AAAA,EACT,CAAC;AAED,QAAM,WAAW,CAAC,MAAc,YAAY,CAAA,SAAQ;AAClD,UAAM,IAAI,IAAI,IAAI,IAAI;AACtB,QAAI,EAAE,IAAI,CAAC,EAAG,GAAE,OAAO,CAAC;AAAA,QACnB,GAAE,IAAI,CAAC;AACZ,WAAO;AAAA,EACT,CAAC;AAED,QAAM,gBAAgB,CAAC,GAAgB,MACrC,EAAE,SAAS,EAAE,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,OAAK,EAAE,IAAI,CAAC,CAAC;AACjD,QAAM,OAAO,SAAS,SAClB,QAAQ,UAAU,QAAQ,OAAO,SAAS,UAAU,MAAM,MAC1D,SAAS,YAAY,CAAC,YACpB,SAAS,QAAQ,OAAO,OAAO,SAAS,QAAQ,OAAO,MACvD,CAAC,cAAc,UAAU,aAAa;AAC5C,QAAM,YAAY,cAAc,OAAO,KAAK,CAAC,CAAC,UAAU,QAAQ,CAAC,CAAC,UAAU,MAAM,CAAC,CAAC,QAAQ,OAAO,CAAC,CAAC,QAAQ;AAE7G,YAAU,MAAM;AACd,UAAM,OAAO,CAAC,MAAkB;AAC9B,UAAI,MAAM,WAAW,CAAC,MAAM,QAAQ,SAAS,EAAE,MAAc,EAAG,UAAA;AAAA,IAClE;AACA,UAAM,QAAQ,CAAC,MAAqB;AAAE,UAAI,EAAE,QAAQ,SAAU,UAAA;AAAA,IAAW;AAOzE,UAAM,UAAU,CAAC,MAAa;AAC5B,UAAI,MAAM,WAAW,EAAE,kBAAkB,QAAQ,MAAM,QAAQ,SAAS,EAAE,MAAM,EAAG;AACnF,eAAA;AAAA,IACF;AACA,aAAS,iBAAiB,aAAa,IAAI;AAC3C,aAAS,iBAAiB,WAAW,KAAK;AAC1C,WAAO,iBAAiB,UAAU,SAAS,IAAI;AAC/C,WAAO,iBAAiB,UAAU,QAAQ;AAC1C,WAAO,MAAM;AACX,eAAS,oBAAoB,aAAa,IAAI;AAC9C,eAAS,oBAAoB,WAAW,KAAK;AAC7C,aAAO,oBAAoB,UAAU,SAAS,IAAI;AAClD,aAAO,oBAAoB,UAAU,QAAQ;AAAA,IAC/C;AAAA,EACF,GAAG,CAAC,QAAQ,CAAC;AAKb,QAAM,YAAY,SAAS,YACzB;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,MAAK;AAAA,MACL,SAAS,CAAA,MAAK;AAAE,UAAE,eAAA;AAAkB,qBAAa,CAAA,MAAK,CAAC,CAAC;AAAA,MAAE;AAAA,MAC1D,OAAO,YAAY,EAAE,YAAY,EAAE;AAAA,MACnC,WAAU;AAAA,MAET,UAAA;AAAA,QAAA,YAAY,oBAAC,eAAY,MAAM,GAAA,CAAI,IAAK,oBAAC,UAAA,EAAS,MAAM,GAAA,CAAI;AAAA,QAC5D,YAAY,EAAE,YAAY,EAAE;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAMjC,QAAM,WAAW,OAAO,cAAc,OAAO,SAAS,UAAU,OAAO,MAAM;AAE7E,SAAO;AAAA,IACL;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,KAAK;AAAA,QACL,OAAO;AAAA,UACL,UAAU;AAAA,UACV,MAAM,KAAK,IAAI,GAAG,KAAK,IAAI,OAAO,MAAM,OAAO,aAAa,UAAU,CAAC,CAAC;AAAA,UACxE,GAAI,WAAW,EAAE,QAAQ,OAAO,cAAc,OAAO,MAAM,EAAA,IAAM,EAAE,KAAK,OAAO,SAAS,EAAA;AAAA,UACxF,OAAO;AAAA,UACP,WAAW;AAAA,QAAA;AAAA,QAEb,WAAU;AAAA,QAET,UAAA;AAAA,UAAA,SACC,oBAAC,SAAI,WAAU,4DACb,8BAAC,QAAA,EAAK,WAAU,mEAAmE,UAAA,MAAA,CAAM,EAAA,CAC3F;AAAA,UAED,aACC,qBAAC,OAAA,EAAI,WAAU,uHACb,UAAA;AAAA,YAAA,oBAAC,QAAA,EAAK,eAAW,MAAC,WAAU,oGAAmG;AAAA,YAC9H,EAAE;AAAA,UAAA,GACL;AAAA,UAED,WAAW,aACV,qBAAC,OAAA,EAAI,WAAU,6DACb,UAAA;AAAA,YAAA;AAAA,cAAC;AAAA,cAAA;AAAA,gBACC,SAAS,MAAM,UAAU,KAAK;AAAA,gBAC9B,WAAW,oIAAoI,gBAAgB,QAAQ,gCAAgC,gBAAgB;AAAA,gBAEvN,UAAA;AAAA,kBAAA,oBAAC,eAAA,EAAc,MAAM,GAAA,CAAI;AAAA,kBAAE;AAAA,kBAAE,SAAS,SAAS,EAAE,cAAc,SAAS,WAAW,EAAE,aAAa,EAAE;AAAA,gBAAA;AAAA,cAAA;AAAA,YAAA;AAAA,YAEtG;AAAA,cAAC;AAAA,cAAA;AAAA,gBACC,SAAS,MAAM,UAAU,MAAM;AAAA,gBAC/B,WAAW,oIAAoI,gBAAgB,SAAS,gCAAgC,gBAAgB;AAAA,gBAExN,UAAA;AAAA,kBAAA,oBAAC,iBAAA,EAAgB,MAAM,GAAA,CAAI;AAAA,kBAAE;AAAA,kBAAE,SAAS,SAAS,EAAE,eAAe,SAAS,WAAW,EAAE,cAAc,EAAE;AAAA,gBAAA;AAAA,cAAA;AAAA,YAAA;AAAA,UAC1G,GACF;AAAA,UAGD,SAAS,SACR,qBAAC,OAAA,EAAI,WAAU,iBACb,UAAA;AAAA,YAAA,qBAAC,SAAA,EAAM,WAAU,SACf,UAAA;AAAA,cAAA,oBAAC,QAAA,EAAK,WAAU,kEAAkE,UAAA,EAAE,UAAS;AAAA,cAC7F;AAAA,gBAAC;AAAA,gBAAA;AAAA,kBACC,MAAK;AAAA,kBAAO,OAAO;AAAA,kBAAI,UAAU,CAAA,MAAK,MAAM,EAAE,OAAO,KAAK;AAAA,kBAC1D,WAAW,CAAA,MAAK;AAAE,wBAAI,EAAE,QAAQ,QAAS,kBAAiB,IAAI,GAAG;AAAA,kBAAE;AAAA,kBACnE,WAAU;AAAA,gBAAA;AAAA,cAAA;AAAA,YACZ,GACF;AAAA,YACA,qBAAC,SAAA,EAAM,WAAU,SACf,UAAA;AAAA,cAAA,oBAAC,QAAA,EAAK,WAAU,kEAAkE,UAAA,EAAE,QAAO;AAAA,cAC3F;AAAA,gBAAC;AAAA,gBAAA;AAAA,kBACC,MAAK;AAAA,kBAAO,OAAO;AAAA,kBAAK,UAAU,CAAA,MAAK,OAAO,EAAE,OAAO,KAAK;AAAA,kBAC5D,WAAW,CAAA,MAAK;AAAE,wBAAI,EAAE,QAAQ,QAAS,kBAAiB,IAAI,GAAG;AAAA,kBAAE;AAAA,kBACnE,WAAU;AAAA,gBAAA;AAAA,cAAA;AAAA,YACZ,EAAA,CACF;AAAA,UAAA,EAAA,CACF,IACE,CAAC;AAAA;AAAA;AAAA;AAAA,YAIH,qBAAC,OAAA,EAAI,WAAU,iBAIb,UAAA;AAAA,cAAA,qBAAC,OAAA,EACC,UAAA;AAAA,gBAAA,qBAAC,QAAA,EAAK,WAAU,qCACd,UAAA;AAAA,kBAAA,oBAAC,WAAM,SAAS,OAAO,WAAU,kEAAkE,YAAE,KAAI;AAAA,kBACxG;AAAA,gBAAA,GACH;AAAA,gBACA;AAAA,kBAAC;AAAA,kBAAA;AAAA,oBACC,IAAI;AAAA,oBACJ,MAAK;AAAA,oBAAS,OAAO;AAAA,oBAAK,UAAU,CAAA,MAAK,OAAO,EAAE,OAAO,KAAK;AAAA,oBAC9D,WAAW,CAAA,MAAK;AAAE,0BAAI,EAAE,QAAQ,QAAS,oBAAmB,KAAK,GAAG;AAAA,oBAAE;AAAA,oBACtE,WAAU;AAAA,kBAAA;AAAA,gBAAA;AAAA,cACZ,GACF;AAAA,cACA,qBAAC,SAAA,EAAM,WAAU,SACf,UAAA;AAAA,gBAAA,oBAAC,QAAA,EAAK,WAAU,kEAAkE,UAAA,EAAE,KAAI;AAAA,gBACxF;AAAA,kBAAC;AAAA,kBAAA;AAAA,oBACC,MAAK;AAAA,oBAAS,OAAO;AAAA,oBAAK,UAAU,CAAA,MAAK,OAAO,EAAE,OAAO,KAAK;AAAA,oBAC9D,WAAW,CAAA,MAAK;AAAE,0BAAI,EAAE,QAAQ,QAAS,oBAAmB,KAAK,GAAG;AAAA,oBAAE;AAAA,oBACtE,WAAU;AAAA,kBAAA;AAAA,gBAAA;AAAA,cACZ,EAAA,CACF;AAAA,YAAA,EAAA,CACF;AAAA,cAEA,qBAAA,UAAA,EACE,UAAA;AAAA,YAAA,qBAAC,OAAA,EAAI,WAAU,+CACb,UAAA;AAAA,cAAA;AAAA,gBAAC;AAAA,gBAAA;AAAA,kBACC,WAAS;AAAA,kBAAC,MAAK;AAAA,kBAAO,OAAO;AAAA,kBAAO,UAAU,CAAA,MAAK,SAAS,EAAE,OAAO,KAAK;AAAA,kBAAG,aAAa,EAAE;AAAA,kBAC5F,WAAU;AAAA,gBAAA;AAAA,cAAA;AAAA,cAEX,aAAa,oBAAC,QAAA,EAAK,WAAU,UAAU,UAAA,UAAA,CAAU;AAAA,YAAA,GACpD;AAAA,YAGC,SAAS,SAAS,KACjB;AAAA,cAAC;AAAA,cAAA;AAAA,gBACC,SAAS;AAAA,gBACT,WAAU;AAAA,gBAEV,UAAA;AAAA,kBAAA,oBAAC,SAAI,WAAW,kGACd,gBAAgB,8BACZ,iBAAiB,iCACf,kBACR,IACG,UAAA,iBAAiB,oBAAC,aAAU,MAAM,GAAG,aAAa,GAAG,WAAU,cAAa,EAAA,CAC/E;AAAA,kBACA,oBAAC,QAAA,EAAK,WAAU,6CACb,UAAA,QAAQ,EAAE,eAAe,SAAS,MAAM,IAAI,EAAE,UAAA,CACjD;AAAA,gBAAA;AAAA,cAAA;AAAA,YAAA;AAAA,gCAGH,OAAA,EAAI,WAAU,oDACZ,UAAA,SAAS,WAAW,IACjB,oBAAC,KAAA,EAAE,WAAU,gDAAgD,UAAA,EAAE,UAAA,CAAU,IACzE,SAAS,IAAI,CAAA,MAAK;AAClB,oBAAM,YAAY,SAAS,IAAI,CAAC;AAChC,qBACE,qBAAC,YAAe,SAAS,MAAM,SAAS,CAAC,GAAG,WAAU,qFACpD,UAAA;AAAA,gBAAA,oBAAC,SAAI,WAAW,kGAAkG,YAAY,8BAA8B,kBAAkB,IAC3K,UAAA,aAAa,oBAAC,WAAA,EAAU,MAAM,GAAG,aAAa,GAAG,WAAU,cAAa,GAC3E;AAAA,gBACA,oBAAC,QAAA,EAAK,WAAU,qEAAqE,UAAA,EAAA,CAAE;AAAA,cAAA,EAAA,GAJ5E,CAKb;AAAA,YAEJ,CAAC,EAAA,CACL;AAAA,UAAA,GACF;AAAA,UAKF,qBAAC,OAAA,EAAI,WAAU,oFACb,UAAA;AAAA,YAAA;AAAA,cAAC;AAAA,cAAA;AAAA,gBACC,SAAS,MAAM;AACb,sBAAI,SAAS,OAAQ,kBAAiB,IAAI,GAAG;AAAA,2BACpC,SAAS,YAAY,CAAC,UAAW,oBAAmB,KAAK,GAAG;AAAA,iCACtD,QAAQ;AAAA,gBACzB;AAAA,gBACA,UAAU,CAAC;AAAA,gBACX,WAAU;AAAA,gBAET,UAAA,EAAE;AAAA,cAAA;AAAA,YAAA;AAAA,YAEL;AAAA,cAAC;AAAA,cAAA;AAAA,gBACC,SAAS;AAAA,gBACT,WAAU;AAAA,gBAET,UAAA,EAAE;AAAA,cAAA;AAAA,YAAA;AAAA,YAEJ,aACC;AAAA,cAAC;AAAA,cAAA;AAAA,gBACC,SAAS;AAAA,gBACT,WAAU;AAAA,gBAET,UAAA,EAAE;AAAA,cAAA;AAAA,YAAA;AAAA,UACL,EAAA,CAEJ;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA;AAAA,IAEF,mBAAmB,SAAS;AAAA,EAAA;AAEhC;"}
|
|
@@ -31,9 +31,9 @@ function SegmentedControl({
|
|
|
31
31
|
0,
|
|
32
32
|
options.findIndex((o) => o.value === value)
|
|
33
33
|
);
|
|
34
|
-
return /* @__PURE__ */ jsxs("div", { className: `flex flex-wrap items-center gap-3 ${className}`, children: [
|
|
34
|
+
return /* @__PURE__ */ jsxs("div", { className: `flex flex-wrap items-center gap-3 max-md:w-full ${className}`, children: [
|
|
35
35
|
label && /* @__PURE__ */ jsx("span", { className: "text-[10px] font-black tracking-wider whitespace-nowrap text-muted-foreground uppercase", children: label }),
|
|
36
|
-
/* @__PURE__ */ jsx("div", { role: "radiogroup", "aria-label": label ?? ariaLabel, className: SEGMENTED.list
|
|
36
|
+
/* @__PURE__ */ jsx("div", { role: "radiogroup", "aria-label": label ?? ariaLabel, className: `${SEGMENTED.list} max-md:flex max-md:min-w-0 max-md:flex-1 max-md:flex-nowrap max-md:items-stretch`, children: options.map((o, i) => {
|
|
37
37
|
const active = o.value === value;
|
|
38
38
|
return /* @__PURE__ */ jsxs(
|
|
39
39
|
"button",
|
|
@@ -48,7 +48,7 @@ function SegmentedControl({
|
|
|
48
48
|
disabled: o.disabled,
|
|
49
49
|
onClick: () => onChange(o.value),
|
|
50
50
|
onKeyDown: (e) => onKeyDown(e, i),
|
|
51
|
-
className: `${SEGMENTED.item} ${active ? SEGMENTED.active : SEGMENTED.idle}`,
|
|
51
|
+
className: `${SEGMENTED.item} max-md:h-auto max-md:min-h-[26px] max-md:min-w-0 max-md:flex-1 max-md:px-1.5 max-md:py-1 max-md:text-center max-md:leading-tight max-md:tracking-wider max-md:whitespace-normal ${active ? SEGMENTED.active : SEGMENTED.idle}`,
|
|
52
52
|
children: [
|
|
53
53
|
o.icon,
|
|
54
54
|
o.label
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SegmentedControl.js","sources":["../../../../../../src/v1/primitives/buttons/SegmentedControl/SegmentedControl.tsx"],"sourcesContent":["import React, { useRef } from 'react'\r\nimport { SEGMENTED } from './segmentedStyles'\r\n\r\nexport interface SegmentedOption<V extends string = string> {\r\n value: V\r\n label: React.ReactNode\r\n /** Ícone antes do texto (ex: um ícone do lucide com `size={12}`). */\r\n icon?: React.ReactNode\r\n disabled?: boolean\r\n}\r\n\r\nexport interface SegmentedControlProps<V extends string = string> {\r\n /** A opção escolhida. A app guarda este estado. */\r\n value: V\r\n options: SegmentedOption<V>[]\r\n onChange: (value: V) => void\r\n /** Rótulo visível à esquerda (ex: \"Situação\"). Também é o nome do grupo para leitores de ecrã. */\r\n label?: string\r\n /** Nome do grupo para leitores de ecrã, quando não há `label` visível. */\r\n ariaLabel?: string\r\n className?: string\r\n}\r\n\r\n/**\r\n * Escolha de uma opção entre poucas, em botões lado a lado (ex: Dia · Semana ·\r\n * Mês, ou Todos · Atribuição · Devolução). Para filtros e modos de vista numa\r\n * barra; para trocar de vista com conteúdo próprio, usar `Tabs`.\r\n *\r\n * Mesmo aspeto dos `Tabs` segmentados: botões juntos numa caixa, o escolhido\r\n * a navy. Setas ← → mudam de opção.\r\n */\r\nexport default function SegmentedControl<V extends string = string>({\r\n value,\r\n options,\r\n onChange,\r\n label,\r\n ariaLabel,\r\n className = '',\r\n}: SegmentedControlProps<V>) {\r\n const refs = useRef<(HTMLButtonElement | null)[]>([])\r\n\r\n const move = (from: number, step: number) => {\r\n const n = options.length\r\n for (let i = 1; i <= n; i++) {\r\n const next = (((from + step * i) % n) + n) % n\r\n if (!options[next].disabled) {\r\n onChange(options[next].value)\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' || e.key === 'ArrowDown') move(index, 1)\r\n else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') move(index, -1)\r\n else return\r\n e.preventDefault()\r\n }\r\n\r\n const focusIndex = Math.max(\r\n 0,\r\n options.findIndex((o) => o.value === value),\r\n )\r\n\r\n return (\r\n <div className={`flex flex-wrap items-center gap-3 ${className}`}>\r\n {label && <span className=\"text-[10px] font-black tracking-wider whitespace-nowrap text-muted-foreground uppercase\">{label}</span>}\r\n <div role=\"radiogroup\" aria-label={label ?? ariaLabel} className={SEGMENTED.list}>\r\n {options.map((o, i) => {\r\n const active = o.value === value\r\n return (\r\n <button\r\n key={o.value}\r\n ref={(el) => {\r\n refs.current[i] = el\r\n }}\r\n type=\"button\"\r\n role=\"radio\"\r\n aria-checked={active}\r\n tabIndex={i === focusIndex ? 0 : -1}\r\n disabled={o.disabled}\r\n onClick={() => onChange(o.value)}\r\n onKeyDown={(e) => onKeyDown(e, i)}\r\n className={`${SEGMENTED.item} ${active ? SEGMENTED.active : SEGMENTED.idle}`}\r\n >\r\n {o.icon}\r\n {o.label}\r\n </button>\r\n )\r\n })}\r\n </div>\r\n </div>\r\n )\r\n}\r\n"],"names":[],"mappings":";;;AA+BA,SAAwB,iBAA4C;AAAA,EAClE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AACd,GAA6B;AAC3B,QAAM,OAAO,OAAqC,EAAE;AAEpD,QAAM,OAAO,CAAC,MAAc,SAAiB;AAC3C,UAAM,IAAI,QAAQ;AAClB,aAAS,IAAI,GAAG,KAAK,GAAG,KAAK;AAC3B,YAAM,SAAU,OAAO,OAAO,KAAK,IAAK,KAAK;AAC7C,UAAI,CAAC,QAAQ,IAAI,EAAE,UAAU;AAC3B,iBAAS,QAAQ,IAAI,EAAE,KAAK;AAC5B,aAAK,QAAQ,IAAI,GAAG,MAAA;AACpB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,QAAM,YAAY,CAAC,GAAwB,UAAkB;AAC3D,QAAI,EAAE,QAAQ,gBAAgB,EAAE,QAAQ,YAAa,MAAK,OAAO,CAAC;AAAA,aACzD,EAAE,QAAQ,eAAe,EAAE,QAAQ,UAAW,MAAK,OAAO,EAAE;AAAA,QAChE;AACL,MAAE,eAAA;AAAA,EACJ;AAEA,QAAM,aAAa,KAAK;AAAA,IACtB;AAAA,IACA,QAAQ,UAAU,CAAC,MAAM,EAAE,UAAU,KAAK;AAAA,EAAA;AAG5C,SACE,qBAAC,OAAA,EAAI,WAAW,
|
|
1
|
+
{"version":3,"file":"SegmentedControl.js","sources":["../../../../../../src/v1/primitives/buttons/SegmentedControl/SegmentedControl.tsx"],"sourcesContent":["import React, { useRef } from 'react'\r\nimport { SEGMENTED } from './segmentedStyles'\r\n\r\nexport interface SegmentedOption<V extends string = string> {\r\n value: V\r\n label: React.ReactNode\r\n /** Ícone antes do texto (ex: um ícone do lucide com `size={12}`). */\r\n icon?: React.ReactNode\r\n disabled?: boolean\r\n}\r\n\r\nexport interface SegmentedControlProps<V extends string = string> {\r\n /** A opção escolhida. A app guarda este estado. */\r\n value: V\r\n options: SegmentedOption<V>[]\r\n onChange: (value: V) => void\r\n /** Rótulo visível à esquerda (ex: \"Situação\"). Também é o nome do grupo para leitores de ecrã. */\r\n label?: string\r\n /** Nome do grupo para leitores de ecrã, quando não há `label` visível. */\r\n ariaLabel?: string\r\n className?: string\r\n}\r\n\r\n/**\r\n * Escolha de uma opção entre poucas, em botões lado a lado (ex: Dia · Semana ·\r\n * Mês, ou Todos · Atribuição · Devolução). Para filtros e modos de vista numa\r\n * barra; para trocar de vista com conteúdo próprio, usar `Tabs`.\r\n *\r\n * Mesmo aspeto dos `Tabs` segmentados: botões juntos numa caixa, o escolhido\r\n * a navy. Setas ← → mudam de opção.\r\n */\r\nexport default function SegmentedControl<V extends string = string>({\r\n value,\r\n options,\r\n onChange,\r\n label,\r\n ariaLabel,\r\n className = '',\r\n}: SegmentedControlProps<V>) {\r\n const refs = useRef<(HTMLButtonElement | null)[]>([])\r\n\r\n const move = (from: number, step: number) => {\r\n const n = options.length\r\n for (let i = 1; i <= n; i++) {\r\n const next = (((from + step * i) % n) + n) % n\r\n if (!options[next].disabled) {\r\n onChange(options[next].value)\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' || e.key === 'ArrowDown') move(index, 1)\r\n else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') move(index, -1)\r\n else return\r\n e.preventDefault()\r\n }\r\n\r\n const focusIndex = Math.max(\r\n 0,\r\n options.findIndex((o) => o.value === value),\r\n )\r\n\r\n return (\r\n <div className={`flex flex-wrap items-center gap-3 max-md:w-full ${className}`}>\r\n {label && <span className=\"text-[10px] font-black tracking-wider whitespace-nowrap text-muted-foreground uppercase\">{label}</span>}\r\n {/* No telemóvel a toda a largura e numa só linha, com as opções em partes iguais: um nome comprido parte-se\r\n dentro do seu botão. Partido em duas linhas parecia dois controlos; a deslizar, ficava cortado. */}\r\n <div role=\"radiogroup\" aria-label={label ?? ariaLabel} className={`${SEGMENTED.list} max-md:flex max-md:min-w-0 max-md:flex-1 max-md:flex-nowrap max-md:items-stretch`}>\r\n {options.map((o, i) => {\r\n const active = o.value === value\r\n return (\r\n <button\r\n key={o.value}\r\n ref={(el) => {\r\n refs.current[i] = el\r\n }}\r\n type=\"button\"\r\n role=\"radio\"\r\n aria-checked={active}\r\n tabIndex={i === focusIndex ? 0 : -1}\r\n disabled={o.disabled}\r\n onClick={() => onChange(o.value)}\r\n onKeyDown={(e) => onKeyDown(e, i)}\r\n className={`${SEGMENTED.item} max-md:h-auto max-md:min-h-[26px] max-md:min-w-0 max-md:flex-1 max-md:px-1.5 max-md:py-1 max-md:text-center max-md:leading-tight max-md:tracking-wider max-md:whitespace-normal ${active ? SEGMENTED.active : SEGMENTED.idle}`}\r\n >\r\n {o.icon}\r\n {o.label}\r\n </button>\r\n )\r\n })}\r\n </div>\r\n </div>\r\n )\r\n}\r\n"],"names":[],"mappings":";;;AA+BA,SAAwB,iBAA4C;AAAA,EAClE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AACd,GAA6B;AAC3B,QAAM,OAAO,OAAqC,EAAE;AAEpD,QAAM,OAAO,CAAC,MAAc,SAAiB;AAC3C,UAAM,IAAI,QAAQ;AAClB,aAAS,IAAI,GAAG,KAAK,GAAG,KAAK;AAC3B,YAAM,SAAU,OAAO,OAAO,KAAK,IAAK,KAAK;AAC7C,UAAI,CAAC,QAAQ,IAAI,EAAE,UAAU;AAC3B,iBAAS,QAAQ,IAAI,EAAE,KAAK;AAC5B,aAAK,QAAQ,IAAI,GAAG,MAAA;AACpB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,QAAM,YAAY,CAAC,GAAwB,UAAkB;AAC3D,QAAI,EAAE,QAAQ,gBAAgB,EAAE,QAAQ,YAAa,MAAK,OAAO,CAAC;AAAA,aACzD,EAAE,QAAQ,eAAe,EAAE,QAAQ,UAAW,MAAK,OAAO,EAAE;AAAA,QAChE;AACL,MAAE,eAAA;AAAA,EACJ;AAEA,QAAM,aAAa,KAAK;AAAA,IACtB;AAAA,IACA,QAAQ,UAAU,CAAC,MAAM,EAAE,UAAU,KAAK;AAAA,EAAA;AAG5C,SACE,qBAAC,OAAA,EAAI,WAAW,mDAAmD,SAAS,IACzE,UAAA;AAAA,IAAA,SAAS,oBAAC,QAAA,EAAK,WAAU,2FAA2F,UAAA,OAAM;AAAA,wBAG1H,OAAA,EAAI,MAAK,cAAa,cAAY,SAAS,WAAW,WAAW,GAAG,UAAU,IAAI,qFAChF,UAAA,QAAQ,IAAI,CAAC,GAAG,MAAM;AACrB,YAAM,SAAS,EAAE,UAAU;AAC3B,aACE;AAAA,QAAC;AAAA,QAAA;AAAA,UAEC,KAAK,CAAC,OAAO;AACX,iBAAK,QAAQ,CAAC,IAAI;AAAA,UACpB;AAAA,UACA,MAAK;AAAA,UACL,MAAK;AAAA,UACL,gBAAc;AAAA,UACd,UAAU,MAAM,aAAa,IAAI;AAAA,UACjC,UAAU,EAAE;AAAA,UACZ,SAAS,MAAM,SAAS,EAAE,KAAK;AAAA,UAC/B,WAAW,CAAC,MAAM,UAAU,GAAG,CAAC;AAAA,UAChC,WAAW,GAAG,UAAU,IAAI,oLAAoL,SAAS,UAAU,SAAS,UAAU,IAAI;AAAA,UAEzP,UAAA;AAAA,YAAA,EAAE;AAAA,YACF,EAAE;AAAA,UAAA;AAAA,QAAA;AAAA,QAdE,EAAE;AAAA,MAAA;AAAA,IAiBb,CAAC,EAAA,CACH;AAAA,EAAA,GACF;AAEJ;"}
|
|
@@ -60,7 +60,10 @@ function Kpi({
|
|
|
60
60
|
const tinted = tintedProp ?? bgTint ?? false;
|
|
61
61
|
const bar = barProp ?? (Array.isArray(barra) ? barra.map((x) => ({ weight: x.peso, color: x.cor })) : barra);
|
|
62
62
|
const surface = tinted ? TINTS[tone] : TINTS.default;
|
|
63
|
-
const layout = dense ? "rounded-lg px-2.5 py-1.5 flex flex-col justify-center text-left gap-0.5" :
|
|
63
|
+
const layout = dense ? "rounded-lg px-2.5 py-1.5 flex flex-col justify-center text-left gap-0.5" : (
|
|
64
|
+
// No telemóvel mais baixo e estreito: dois ou três lado a lado sem cada um ocupar meio ecrã de altura.
|
|
65
|
+
"rounded-xl px-4 py-3 min-h-[88px] flex flex-col items-center justify-center text-center gap-1.5 max-sm:min-h-[64px] max-sm:px-2 max-sm:py-2 max-sm:gap-1"
|
|
66
|
+
);
|
|
64
67
|
const base = `relative border ${surface} ${layout} ${className}`;
|
|
65
68
|
const totalBar = Array.isArray(bar) ? bar.reduce((s, x) => s + x.weight, 0) : 0;
|
|
66
69
|
const content = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
@@ -69,7 +72,7 @@ function Kpi({
|
|
|
69
72
|
/* @__PURE__ */ jsx("span", { className: "truncate", children: label })
|
|
70
73
|
] }),
|
|
71
74
|
/* @__PURE__ */ jsxs("div", { className: dense ? "flex items-baseline gap-1.5" : "", children: [
|
|
72
|
-
/* @__PURE__ */ jsx("span", { className: `${dense ? "text-xl" : "text-2xl"} font-semibold ${VALUE_TONES[tone]}`, children: value }),
|
|
75
|
+
/* @__PURE__ */ jsx("span", { className: `${dense ? "text-xl" : "text-2xl max-sm:text-xl"} font-semibold ${VALUE_TONES[tone]}`, children: value }),
|
|
73
76
|
detail && dense && /* @__PURE__ */ jsx("span", { className: "text-[11px] text-slate-400 truncate", children: detail })
|
|
74
77
|
] }),
|
|
75
78
|
detail && !dense && /* @__PURE__ */ jsx("div", { className: "text-[11.5px] text-slate-400", children: detail }),
|
|
@@ -89,7 +92,7 @@ function Kpi({
|
|
|
89
92
|
className: `${base} w-full cursor-pointer shadow-sm transition hover:-translate-y-0.5 hover:border-proef-blue dark:hover:border-sky-400 hover:shadow-md focus:outline-none focus-visible:ring-2 focus-visible:ring-proef-blue ${active ? "ring-2 ring-proef-blue border-proef-blue dark:border-sky-400" : ""}`,
|
|
90
93
|
children: [
|
|
91
94
|
/* @__PURE__ */ (() => {
|
|
92
|
-
const cls = `absolute right-2.5 top-2.5 h-3.5 w-3.5 ${active ? "text-proef-blue dark:text-sky-300" : "text-slate-300 dark:text-slate-600"}`;
|
|
95
|
+
const cls = `absolute right-2.5 top-2.5 h-3.5 w-3.5 max-sm:hidden ${active ? "text-proef-blue dark:text-sky-300" : "text-slate-300 dark:text-slate-600"}`;
|
|
93
96
|
return clickAction === "open" ? /* @__PURE__ */ jsx(OpenIcon, { className: cls }) : /* @__PURE__ */ jsx(FunnelIcon, { className: cls });
|
|
94
97
|
})(),
|
|
95
98
|
content
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Kpi.js","sources":["../../../../../../src/v1/primitives/display/Kpi/Kpi.tsx"],"sourcesContent":["import React from 'react'\r\n\r\n/**\r\n * Estados com significado: `ok` (verde), `risco` (âmbar), `incumprido` (vermelho).\r\n * Sem estado: `default` (texto normal) e `brand` (azul da marca).\r\n * A cor é a do número — não usar cores só para enfeitar: um indicador sem\r\n * significado bom/mau fica em `default`.\r\n */\r\nexport type KpiTone = 'default' | 'brand' | 'ok' | 'risco' | 'incumprido'\r\n\r\nexport interface KpiProps {\r\n label: React.ReactNode\r\n value: React.ReactNode\r\n tone?: KpiTone\r\n /** Linha por baixo do valor (ex: \"de 120\", \"+12% do compromisso\"). */\r\n detail?: React.ReactNode\r\n /** Ícone antes do rótulo (ex: `<Inbox size={14} />`). */\r\n icon?: React.ReactNode\r\n /** Ponto colorido antes do rótulo — liga o indicador à cor que ele tem no mapa ou na legenda. */\r\n dot?: string\r\n /**\r\n * Barra por baixo do valor. Um número é uma percentagem; uma lista é uma\r\n * barra repartida, com cada troço proporcional ao seu peso.\r\n *\r\n * Serve para o indicador dizer a PROPORÇÃO e não só o valor: \"253 concluídas\"\r\n * não diz nada sem saber de quantas.\r\n */\r\n bar?: number | { weight: number; color: string }[]\r\n /** Linha pequena no fim, para um dado que acompanha mas não é o indicador. */\r\n footer?: React.ReactNode\r\n /**\r\n * Faixa fina em vez de cartão: alinha à esquerda e corta a altura para metade.\r\n * Para muitos indicadores que empurrariam o conteúdo para fora do ecrã.\r\n */\r\n dense?: boolean\r\n /** Fundo no tom claro da cor, em vez de só o número colorido. */\r\n tinted?: boolean\r\n /** Clicável (ex: filtra a tabela). Mostra o funil no canto (ou a seta, com `clickAction=\"open\"`). */\r\n onClick?: () => void\r\n /** O que o clique faz: `filter` (funil, por omissão) ou `open` (seta: abre uma lista ou um detalhe, não filtra). */\r\n clickAction?: 'filter' | 'open'\r\n /** Filtro ativo (Kpi clicável selecionado): contorno azul. */\r\n active?: boolean\r\n /** Explicação no tooltip (camada `data-tip`), ex: \"Filtrar a tabela\" ou como o valor é calculado. */\r\n hint?: string\r\n className?: string\r\n /** @deprecated Nome antigo (Legacy) de `detail`. */\r\n delta?: React.ReactNode\r\n /** @deprecated Nome antigo (Legacy) de `tinted`. */\r\n bgTint?: boolean\r\n /** @deprecated Nome antigo (Legacy) de `bar`. */\r\n barra?: number | { peso: number; cor: string }[]\r\n /** @deprecated Nome antigo (Legacy) de `footer`. */\r\n rodape?: React.ReactNode\r\n /** @deprecated Nome antigo (Legacy) de `dense`. */\r\n denso?: boolean\r\n}\r\n\r\nconst VALUE_TONES: Record<KpiTone, string> = {\r\n default: 'text-slate-800 dark:text-slate-100',\r\n brand: 'text-proef-blue dark:text-sky-300',\r\n ok: 'text-ok',\r\n risco: 'text-risco',\r\n incumprido: 'text-incumprido',\r\n}\r\n\r\nconst TINTS: Record<KpiTone, string> = {\r\n default: 'bg-white border-slate-200 dark:bg-slate-900 dark:border-slate-700',\r\n brand: 'bg-proef-blue/10 border-transparent dark:bg-sky-500/15',\r\n ok: 'bg-ok-bg border-transparent',\r\n risco: 'bg-risco-bg border-transparent',\r\n incumprido: 'bg-incumprido-bg border-transparent',\r\n}\r\n\r\nfunction OpenIcon({ className }: { className: string }) {\r\n return (\r\n <svg aria-hidden viewBox=\"0 0 24 24\" className={className} fill=\"none\" stroke=\"currentColor\" strokeWidth={2} strokeLinecap=\"round\" strokeLinejoin=\"round\">\r\n <path d=\"M7 17 17 7M7 7h10v10\" />\r\n </svg>\r\n )\r\n}\r\n\r\nfunction FunnelIcon({ className }: { className: string }) {\r\n return (\r\n <svg\r\n aria-hidden\r\n viewBox=\"0 0 24 24\"\r\n className={className}\r\n fill=\"none\"\r\n stroke=\"currentColor\"\r\n strokeWidth={2}\r\n strokeLinecap=\"round\"\r\n strokeLinejoin=\"round\"\r\n >\r\n <path d=\"M3 4h18l-7 8.5V19l-4 2v-8.5L3 4z\" />\r\n </svg>\r\n )\r\n}\r\n\r\n/**\r\n * Indicador: um número com o seu rótulo (ex: \"Em progresso 13\"). Quando é\r\n * clicável filtra a vista e mostra-o com o funil; `active` marca o filtro\r\n * aplicado. Dispor vários numa grelha (`grid gap-3`).\r\n */\r\nexport default function Kpi({\r\n label,\r\n value,\r\n tone = 'default',\r\n detail: detailProp,\r\n icon,\r\n dot,\r\n bar: barProp,\r\n footer: footerProp,\r\n dense: denseProp,\r\n tinted: tintedProp,\r\n onClick,\r\n clickAction = 'filter',\r\n active = false,\r\n hint,\r\n className = '',\r\n delta,\r\n bgTint,\r\n barra,\r\n rodape,\r\n denso,\r\n}: KpiProps) {\r\n // A Legacy usa este mesmo Kpi com os nomes antigos.\r\n const detail = detailProp ?? delta\r\n const footer = footerProp ?? rodape\r\n const dense = denseProp ?? denso ?? false\r\n const tinted = tintedProp ?? bgTint ?? false\r\n const bar = barProp ?? (Array.isArray(barra) ? barra.map((x) => ({ weight: x.peso, color: x.cor })) : barra)\r\n const surface = tinted ? TINTS[tone] : TINTS.default\r\n const layout = dense\r\n ? 'rounded-lg px-2.5 py-1.5 flex flex-col justify-center text-left gap-0.5'\r\n : 'rounded-xl px-4 py-3 min-h-[88px] flex flex-col items-center justify-center text-center gap-1.5'\r\n const base = `relative border ${surface} ${layout} ${className}`\r\n const totalBar = Array.isArray(bar) ? bar.reduce((s, x) => s + x.weight, 0) : 0\r\n\r\n const content = (\r\n <>\r\n <div className={`${dense ? 'text-[10px] uppercase tracking-wide' : 'text-xs'} text-slate-500 dark:text-slate-400 flex items-center gap-1.5 max-w-full`}>\r\n {icon ? (\r\n <span aria-hidden className=\"shrink-0 flex\">{icon}</span>\r\n ) : (\r\n dot && <i aria-hidden className=\"w-2 h-2 rounded-full inline-block shrink-0\" style={{ background: dot }} />\r\n )}\r\n <span className=\"truncate\">{label}</span>\r\n </div>\r\n {/* Em denso o valor e o secundário partilham a linha: é o que poupa altura sem tirar informação. */}\r\n <div className={dense ? 'flex items-baseline gap-1.5' : ''}>\r\n <span className={`${dense ? 'text-xl' : 'text-2xl'} font-semibold ${VALUE_TONES[tone]}`}>{value}</span>\r\n {detail && dense && <span className=\"text-[11px] text-slate-400 truncate\">{detail}</span>}\r\n </div>\r\n {detail && !dense && <div className=\"text-[11.5px] text-slate-400\">{detail}</div>}\r\n {bar != null && (\r\n <div className=\"w-full h-1 bg-slate-100 dark:bg-slate-800 rounded overflow-hidden flex\">\r\n {Array.isArray(bar) ? (\r\n bar.map((x, i) => (\r\n <i key={i} style={{ width: totalBar ? `${(x.weight / totalBar) * 100}%` : 0, background: x.color }} />\r\n ))\r\n ) : (\r\n <i style={{ width: `${Math.min(100, Math.max(0, bar))}%`, background: 'currentColor' }} className={VALUE_TONES[tone]} />\r\n )}\r\n </div>\r\n )}\r\n {footer && <div className=\"text-[11px] text-slate-400\">{footer}</div>}\r\n </>\r\n )\r\n\r\n if (!onClick) {\r\n return (\r\n <div data-tip={hint} className={`${base} ${hint ? 'cursor-help' : ''}`}>\r\n {content}\r\n </div>\r\n )\r\n }\r\n return (\r\n <button\r\n type=\"button\"\r\n onClick={onClick}\r\n data-tip={hint}\r\n aria-pressed={active}\r\n className={`${base} w-full cursor-pointer shadow-sm transition hover:-translate-y-0.5 hover:border-proef-blue dark:hover:border-sky-400 hover:shadow-md focus:outline-none focus-visible:ring-2 focus-visible:ring-proef-blue ${\r\n active ? 'ring-2 ring-proef-blue border-proef-blue dark:border-sky-400' : ''\r\n }`}\r\n >\r\n {(() => {\r\n const cls = `absolute right-2.5 top-2.5 h-3.5 w-3.5 ${active ? 'text-proef-blue dark:text-sky-300' : 'text-slate-300 dark:text-slate-600'}`\r\n return clickAction === 'open' ? <OpenIcon className={cls} /> : <FunnelIcon className={cls} />\r\n })()}\r\n {content}\r\n </button>\r\n )\r\n}\r\n\r\nexport { Kpi }\r\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Kpi.js","sources":["../../../../../../src/v1/primitives/display/Kpi/Kpi.tsx"],"sourcesContent":["import React from 'react'\r\n\r\n/**\r\n * Estados com significado: `ok` (verde), `risco` (âmbar), `incumprido` (vermelho).\r\n * Sem estado: `default` (texto normal) e `brand` (azul da marca).\r\n * A cor é a do número — não usar cores só para enfeitar: um indicador sem\r\n * significado bom/mau fica em `default`.\r\n */\r\nexport type KpiTone = 'default' | 'brand' | 'ok' | 'risco' | 'incumprido'\r\n\r\nexport interface KpiProps {\r\n label: React.ReactNode\r\n value: React.ReactNode\r\n tone?: KpiTone\r\n /** Linha por baixo do valor (ex: \"de 120\", \"+12% do compromisso\"). */\r\n detail?: React.ReactNode\r\n /** Ícone antes do rótulo (ex: `<Inbox size={14} />`). */\r\n icon?: React.ReactNode\r\n /** Ponto colorido antes do rótulo — liga o indicador à cor que ele tem no mapa ou na legenda. */\r\n dot?: string\r\n /**\r\n * Barra por baixo do valor. Um número é uma percentagem; uma lista é uma\r\n * barra repartida, com cada troço proporcional ao seu peso.\r\n *\r\n * Serve para o indicador dizer a PROPORÇÃO e não só o valor: \"253 concluídas\"\r\n * não diz nada sem saber de quantas.\r\n */\r\n bar?: number | { weight: number; color: string }[]\r\n /** Linha pequena no fim, para um dado que acompanha mas não é o indicador. */\r\n footer?: React.ReactNode\r\n /**\r\n * Faixa fina em vez de cartão: alinha à esquerda e corta a altura para metade.\r\n * Para muitos indicadores que empurrariam o conteúdo para fora do ecrã.\r\n */\r\n dense?: boolean\r\n /** Fundo no tom claro da cor, em vez de só o número colorido. */\r\n tinted?: boolean\r\n /** Clicável (ex: filtra a tabela). Mostra o funil no canto (ou a seta, com `clickAction=\"open\"`). */\r\n onClick?: () => void\r\n /** O que o clique faz: `filter` (funil, por omissão) ou `open` (seta: abre uma lista ou um detalhe, não filtra). */\r\n clickAction?: 'filter' | 'open'\r\n /** Filtro ativo (Kpi clicável selecionado): contorno azul. */\r\n active?: boolean\r\n /** Explicação no tooltip (camada `data-tip`), ex: \"Filtrar a tabela\" ou como o valor é calculado. */\r\n hint?: string\r\n className?: string\r\n /** @deprecated Nome antigo (Legacy) de `detail`. */\r\n delta?: React.ReactNode\r\n /** @deprecated Nome antigo (Legacy) de `tinted`. */\r\n bgTint?: boolean\r\n /** @deprecated Nome antigo (Legacy) de `bar`. */\r\n barra?: number | { peso: number; cor: string }[]\r\n /** @deprecated Nome antigo (Legacy) de `footer`. */\r\n rodape?: React.ReactNode\r\n /** @deprecated Nome antigo (Legacy) de `dense`. */\r\n denso?: boolean\r\n}\r\n\r\nconst VALUE_TONES: Record<KpiTone, string> = {\r\n default: 'text-slate-800 dark:text-slate-100',\r\n brand: 'text-proef-blue dark:text-sky-300',\r\n ok: 'text-ok',\r\n risco: 'text-risco',\r\n incumprido: 'text-incumprido',\r\n}\r\n\r\nconst TINTS: Record<KpiTone, string> = {\r\n default: 'bg-white border-slate-200 dark:bg-slate-900 dark:border-slate-700',\r\n brand: 'bg-proef-blue/10 border-transparent dark:bg-sky-500/15',\r\n ok: 'bg-ok-bg border-transparent',\r\n risco: 'bg-risco-bg border-transparent',\r\n incumprido: 'bg-incumprido-bg border-transparent',\r\n}\r\n\r\nfunction OpenIcon({ className }: { className: string }) {\r\n return (\r\n <svg aria-hidden viewBox=\"0 0 24 24\" className={className} fill=\"none\" stroke=\"currentColor\" strokeWidth={2} strokeLinecap=\"round\" strokeLinejoin=\"round\">\r\n <path d=\"M7 17 17 7M7 7h10v10\" />\r\n </svg>\r\n )\r\n}\r\n\r\nfunction FunnelIcon({ className }: { className: string }) {\r\n return (\r\n <svg\r\n aria-hidden\r\n viewBox=\"0 0 24 24\"\r\n className={className}\r\n fill=\"none\"\r\n stroke=\"currentColor\"\r\n strokeWidth={2}\r\n strokeLinecap=\"round\"\r\n strokeLinejoin=\"round\"\r\n >\r\n <path d=\"M3 4h18l-7 8.5V19l-4 2v-8.5L3 4z\" />\r\n </svg>\r\n )\r\n}\r\n\r\n/**\r\n * Indicador: um número com o seu rótulo (ex: \"Em progresso 13\"). Quando é\r\n * clicável filtra a vista e mostra-o com o funil; `active` marca o filtro\r\n * aplicado. Dispor vários numa grelha (`grid gap-3`).\r\n */\r\nexport default function Kpi({\r\n label,\r\n value,\r\n tone = 'default',\r\n detail: detailProp,\r\n icon,\r\n dot,\r\n bar: barProp,\r\n footer: footerProp,\r\n dense: denseProp,\r\n tinted: tintedProp,\r\n onClick,\r\n clickAction = 'filter',\r\n active = false,\r\n hint,\r\n className = '',\r\n delta,\r\n bgTint,\r\n barra,\r\n rodape,\r\n denso,\r\n}: KpiProps) {\r\n // A Legacy usa este mesmo Kpi com os nomes antigos.\r\n const detail = detailProp ?? delta\r\n const footer = footerProp ?? rodape\r\n const dense = denseProp ?? denso ?? false\r\n const tinted = tintedProp ?? bgTint ?? false\r\n const bar = barProp ?? (Array.isArray(barra) ? barra.map((x) => ({ weight: x.peso, color: x.cor })) : barra)\r\n const surface = tinted ? TINTS[tone] : TINTS.default\r\n const layout = dense\r\n ? 'rounded-lg px-2.5 py-1.5 flex flex-col justify-center text-left gap-0.5'\r\n : // No telemóvel mais baixo e estreito: dois ou três lado a lado sem cada um ocupar meio ecrã de altura.\r\n 'rounded-xl px-4 py-3 min-h-[88px] flex flex-col items-center justify-center text-center gap-1.5 max-sm:min-h-[64px] max-sm:px-2 max-sm:py-2 max-sm:gap-1'\r\n const base = `relative border ${surface} ${layout} ${className}`\r\n const totalBar = Array.isArray(bar) ? bar.reduce((s, x) => s + x.weight, 0) : 0\r\n\r\n const content = (\r\n <>\r\n <div className={`${dense ? 'text-[10px] uppercase tracking-wide' : 'text-xs'} text-slate-500 dark:text-slate-400 flex items-center gap-1.5 max-w-full`}>\r\n {icon ? (\r\n <span aria-hidden className=\"shrink-0 flex\">{icon}</span>\r\n ) : (\r\n dot && <i aria-hidden className=\"w-2 h-2 rounded-full inline-block shrink-0\" style={{ background: dot }} />\r\n )}\r\n <span className=\"truncate\">{label}</span>\r\n </div>\r\n {/* Em denso o valor e o secundário partilham a linha: é o que poupa altura sem tirar informação. */}\r\n <div className={dense ? 'flex items-baseline gap-1.5' : ''}>\r\n <span className={`${dense ? 'text-xl' : 'text-2xl max-sm:text-xl'} font-semibold ${VALUE_TONES[tone]}`}>{value}</span>\r\n {detail && dense && <span className=\"text-[11px] text-slate-400 truncate\">{detail}</span>}\r\n </div>\r\n {detail && !dense && <div className=\"text-[11.5px] text-slate-400\">{detail}</div>}\r\n {bar != null && (\r\n <div className=\"w-full h-1 bg-slate-100 dark:bg-slate-800 rounded overflow-hidden flex\">\r\n {Array.isArray(bar) ? (\r\n bar.map((x, i) => (\r\n <i key={i} style={{ width: totalBar ? `${(x.weight / totalBar) * 100}%` : 0, background: x.color }} />\r\n ))\r\n ) : (\r\n <i style={{ width: `${Math.min(100, Math.max(0, bar))}%`, background: 'currentColor' }} className={VALUE_TONES[tone]} />\r\n )}\r\n </div>\r\n )}\r\n {footer && <div className=\"text-[11px] text-slate-400\">{footer}</div>}\r\n </>\r\n )\r\n\r\n if (!onClick) {\r\n return (\r\n <div data-tip={hint} className={`${base} ${hint ? 'cursor-help' : ''}`}>\r\n {content}\r\n </div>\r\n )\r\n }\r\n return (\r\n <button\r\n type=\"button\"\r\n onClick={onClick}\r\n data-tip={hint}\r\n aria-pressed={active}\r\n className={`${base} w-full cursor-pointer shadow-sm transition hover:-translate-y-0.5 hover:border-proef-blue dark:hover:border-sky-400 hover:shadow-md focus:outline-none focus-visible:ring-2 focus-visible:ring-proef-blue ${\r\n active ? 'ring-2 ring-proef-blue border-proef-blue dark:border-sky-400' : ''\r\n }`}\r\n >\r\n {(() => {\r\n // No telemóvel sem o ícone do canto: em cartões estreitos ficava por cima do rótulo; o filtro aplicado\r\n // continua a ver-se pelo contorno azul.\r\n const cls = `absolute right-2.5 top-2.5 h-3.5 w-3.5 max-sm:hidden ${active ? 'text-proef-blue dark:text-sky-300' : 'text-slate-300 dark:text-slate-600'}`\r\n return clickAction === 'open' ? <OpenIcon className={cls} /> : <FunnelIcon className={cls} />\r\n })()}\r\n {content}\r\n </button>\r\n )\r\n}\r\n\r\nexport { Kpi }\r\n"],"names":[],"mappings":"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"}
|
|
@@ -4,6 +4,7 @@ import { createPortal } from "react-dom";
|
|
|
4
4
|
import { ChevronDownIcon } from "../../../foundations/icons/icons.js";
|
|
5
5
|
import { useField, fieldBoxClasses } from "../field.js";
|
|
6
6
|
import { useFloatingPanel, PANEL_CLASS, itemClass } from "../../../capabilities/overlay/useFloatingPanel.js";
|
|
7
|
+
import { useDialogContainer } from "../../../components/overlays/Dialog/Dialog.js";
|
|
7
8
|
const DEFAULT_SELECT_LABELS = {
|
|
8
9
|
search: "Search…",
|
|
9
10
|
searchIn: "Search in {label}",
|
|
@@ -44,19 +45,28 @@ const SelectInput = forwardRef(function SelectInput2({
|
|
|
44
45
|
pillClassName = "bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-200",
|
|
45
46
|
tall = false,
|
|
46
47
|
labels,
|
|
47
|
-
className = ""
|
|
48
|
+
className = "",
|
|
49
|
+
multiple = false,
|
|
50
|
+
values: controlledValues,
|
|
51
|
+
defaultValues = [],
|
|
52
|
+
onValuesChange,
|
|
53
|
+
allLabel
|
|
48
54
|
}, ref) {
|
|
49
55
|
const L = { ...DEFAULT_SELECT_LABELS, ...labels };
|
|
50
56
|
const field = useField();
|
|
51
57
|
const p = useFloatingPanel("left");
|
|
58
|
+
const dialogContainer = useDialogContainer();
|
|
52
59
|
useImperativeHandle(ref, () => p.triggerRef.current);
|
|
53
60
|
const [own, setOwn] = useState(defaultValue);
|
|
54
61
|
const value = controlled ?? own;
|
|
62
|
+
const [ownValues, setOwnValues] = useState(defaultValues);
|
|
63
|
+
const picked = controlledValues ?? ownValues;
|
|
55
64
|
const list = useMemo(() => {
|
|
56
65
|
const base = options ? options.map((o) => typeof o === "string" ? { value: o, label: o } : o) : optionsFromChildren(children);
|
|
66
|
+
if (multiple) return base;
|
|
57
67
|
const empty = emptyOption ?? (placeholder !== void 0 && placeholderOption ? placeholder : void 0);
|
|
58
68
|
return empty !== void 0 && !base.some((o) => o.value === "") ? [{ value: "", label: empty }, ...base] : base;
|
|
59
|
-
}, [options, children, emptyOption, placeholder, placeholderOption]);
|
|
69
|
+
}, [options, children, emptyOption, placeholder, placeholderOption, multiple]);
|
|
60
70
|
const [query, setQuery] = useState("");
|
|
61
71
|
const withSearch = searchable ?? list.length > 10;
|
|
62
72
|
const visible = useMemo(() => {
|
|
@@ -69,7 +79,7 @@ const SelectInput = forwardRef(function SelectInput2({
|
|
|
69
79
|
useEffect(() => {
|
|
70
80
|
if (!p.open) return;
|
|
71
81
|
setQuery("");
|
|
72
|
-
setActive(Math.max(0, list.findIndex((o) => o.value === value)));
|
|
82
|
+
setActive(Math.max(0, list.findIndex((o) => multiple ? picked.includes(o.value) : o.value === value)));
|
|
73
83
|
if (withSearch) setTimeout(() => searchRef.current?.focus(), 0);
|
|
74
84
|
}, [p.open]);
|
|
75
85
|
useEffect(() => {
|
|
@@ -77,6 +87,15 @@ const SelectInput = forwardRef(function SelectInput2({
|
|
|
77
87
|
}, [active, p.open, p.panelRef]);
|
|
78
88
|
const choose = (o) => {
|
|
79
89
|
if (o.disabled) return;
|
|
90
|
+
if (multiple) {
|
|
91
|
+
const set = new Set(picked);
|
|
92
|
+
if (set.has(o.value)) set.delete(o.value);
|
|
93
|
+
else set.add(o.value);
|
|
94
|
+
const next = list.filter((x) => set.has(x.value)).map((x) => x.value);
|
|
95
|
+
if (controlledValues === void 0) setOwnValues(next);
|
|
96
|
+
onValuesChange?.(next);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
80
99
|
p.close();
|
|
81
100
|
p.triggerRef.current?.focus();
|
|
82
101
|
if (o.value === value) return;
|
|
@@ -124,7 +143,10 @@ const SelectInput = forwardRef(function SelectInput2({
|
|
|
124
143
|
}
|
|
125
144
|
};
|
|
126
145
|
const pill = variant === "pill";
|
|
127
|
-
const
|
|
146
|
+
const pickedOptions = list.filter((o) => picked.includes(o.value));
|
|
147
|
+
const multiText = allLabel && list.length > 0 && pickedOptions.length === list.length ? allLabel : pickedOptions.map((o) => o.label).join(", ");
|
|
148
|
+
const showsPlaceholder = multiple ? pickedOptions.length === 0 : !chosen || chosen.value === "" && placeholder !== void 0 && emptyOption === void 0;
|
|
149
|
+
const buttonText = multiple ? showsPlaceholder ? placeholder ?? "" : multiText : showsPlaceholder ? placeholder ?? chosen?.label ?? "" : chosen?.label;
|
|
128
150
|
return /* @__PURE__ */ jsxs("div", { className: `relative min-w-0 ${className}`, onKeyDown: keys, children: [
|
|
129
151
|
/* @__PURE__ */ jsxs(
|
|
130
152
|
"button",
|
|
@@ -145,12 +167,12 @@ const SelectInput = forwardRef(function SelectInput2({
|
|
|
145
167
|
},
|
|
146
168
|
className: pill ? `inline-flex cursor-pointer appearance-none items-center gap-1 rounded-full px-2.5 py-1 text-xs font-bold whitespace-nowrap transition hover:brightness-95 focus:outline-none focus-visible:ring-2 focus-visible:ring-proef-blue disabled:cursor-not-allowed disabled:opacity-50 ${pillClassName}` : `flex w-full min-w-0 cursor-pointer appearance-none items-center justify-between gap-2 text-left ${tall ? "h-[38px]" : "h-control"} ${fieldBoxClasses}`,
|
|
147
169
|
children: [
|
|
148
|
-
/* @__PURE__ */ jsx("span", { className: `truncate ${showsPlaceholder && !pill ? "text-muted-foreground/70" : ""}`, children:
|
|
170
|
+
/* @__PURE__ */ jsx("span", { className: `truncate ${showsPlaceholder && !pill ? "text-muted-foreground/70" : ""}`, children: buttonText }),
|
|
149
171
|
/* @__PURE__ */ jsx(ChevronDownIcon, { size: 14, className: `shrink-0 ${pill ? "opacity-60" : "text-muted-foreground"}` })
|
|
150
172
|
]
|
|
151
173
|
}
|
|
152
174
|
),
|
|
153
|
-
name && /* @__PURE__ */ jsx("input", { type: "hidden", name, value }),
|
|
175
|
+
name && /* @__PURE__ */ jsx("input", { type: "hidden", name, value: multiple ? picked.join(",") : value }),
|
|
154
176
|
p.open && createPortal(
|
|
155
177
|
/* @__PURE__ */ jsxs("div", { ref: p.panelRef, style: p.style, className: PANEL_CLASS, onClick: (e) => e.stopPropagation(), onKeyDown: keys, children: [
|
|
156
178
|
withSearch && /* @__PURE__ */ jsx("div", { className: "px-2 pt-0.5 pb-1", children: /* @__PURE__ */ jsx(
|
|
@@ -168,10 +190,10 @@ const SelectInput = forwardRef(function SelectInput2({
|
|
|
168
190
|
className: "h-8 w-full rounded-lg border border-slate-300 bg-white px-2.5 text-sm outline-none focus:border-proef-blue dark:border-slate-600 dark:bg-slate-800 dark:text-slate-100"
|
|
169
191
|
}
|
|
170
192
|
) }),
|
|
171
|
-
/* @__PURE__ */ jsxs("ul", { role: "listbox", "aria-label": ariaLabel, children: [
|
|
193
|
+
/* @__PURE__ */ jsxs("ul", { role: "listbox", "aria-label": ariaLabel, "aria-multiselectable": multiple || void 0, children: [
|
|
172
194
|
visible.length === 0 && /* @__PURE__ */ jsx("li", { className: "px-3 py-2 text-slate-400", children: L.noResults }),
|
|
173
195
|
visible.map((o, i) => {
|
|
174
|
-
const selected = o.value === value;
|
|
196
|
+
const selected = multiple ? picked.includes(o.value) : o.value === value;
|
|
175
197
|
return /* @__PURE__ */ jsxs(
|
|
176
198
|
"li",
|
|
177
199
|
{
|
|
@@ -195,7 +217,7 @@ const SelectInput = forwardRef(function SelectInput2({
|
|
|
195
217
|
})
|
|
196
218
|
] })
|
|
197
219
|
] }),
|
|
198
|
-
document.body
|
|
220
|
+
dialogContainer ?? document.body
|
|
199
221
|
)
|
|
200
222
|
] });
|
|
201
223
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SelectInput.js","sources":["../../../../../../src/v1/primitives/forms/SelectInput/SelectInput.tsx"],"sourcesContent":["import React, { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'\r\nimport { createPortal } from 'react-dom'\r\nimport { ChevronDownIcon } from '../../../foundations/icons/icons'\r\nimport { fieldBoxClasses, useField } from '../field'\r\nimport { PANEL_CLASS, itemClass, useFloatingPanel } from '../../../capabilities/overlay/useFloatingPanel'\r\n\r\nexport interface SelectOption {\r\n value: string\r\n label: string\r\n /** Texto pequeno por baixo do rótulo, na lista. */\r\n description?: string\r\n disabled?: boolean\r\n}\r\n\r\nexport interface SelectInputLabels {\r\n /** Texto da caixa de pesquisa. */\r\n search: string\r\n /** Nome da caixa de pesquisa para leitores de ecrã; \"{label}\" é o nome do campo. */\r\n searchIn: string\r\n noResults: string\r\n}\r\n\r\nexport const DEFAULT_SELECT_LABELS: SelectInputLabels = {\r\n search: 'Search…',\r\n searchIn: 'Search in {label}',\r\n noResults: 'No results',\r\n}\r\n\r\nexport interface SelectInputProps {\r\n /** A opção escolhida (controlado). `''` = nenhuma. */\r\n value?: string\r\n defaultValue?: string\r\n /** As opções: texto simples (valor = rótulo) ou `{ value, label, description }`. */\r\n options?: readonly (string | SelectOption)[]\r\n /** Em alternativa a `options`: `<option>` como num `<select>` (compatível com o uso antigo). */\r\n children?: React.ReactNode\r\n /** Recebe o valor escolhido. */\r\n onValueChange?: (value: string) => void\r\n /** Como no `<select>`: `e.target.value` é o valor escolhido. */\r\n onChange?: (e: React.ChangeEvent<HTMLSelectElement>) => void\r\n /** Texto do botão sem nada escolhido; com `placeholderOption`, também a primeira opção (vazia). */\r\n placeholder?: string\r\n /** O `placeholder` é também uma opção (vazia) na lista. Por omissão sim, como no `<select>`. */\r\n placeholderOption?: boolean\r\n /** Primeira opção vazia para desfazer a escolha (ex: \"Todas\"). */\r\n emptyOption?: string\r\n /** Caixa de pesquisa no topo da lista. Por omissão, só com mais de 10 opções. */\r\n searchable?: boolean\r\n disabled?: boolean\r\n required?: boolean\r\n /** Marca o campo a vermelho (ex: obrigatório por escolher ao submeter). */\r\n invalid?: boolean\r\n id?: string\r\n /** Nome no formulário (vai num campo escondido). */\r\n name?: string\r\n 'aria-label'?: string\r\n 'aria-labelledby'?: string\r\n /** `field` (por omissão): campo. `pill`: etiqueta arredondada (ex: o estado numa linha de tabela). A lista é a mesma. */\r\n variant?: 'field' | 'pill'\r\n /** Cores da etiqueta (`pill`), normalmente conforme o valor escolhido; pode trazer contorno (ex: `border border-dashed`). */\r\n pillClassName?: string\r\n /** 38px em vez da altura dos controlos (para alinhar com campos de formulários mais altos). */\r\n tall?: boolean\r\n labels?: Partial<SelectInputLabels>\r\n /** Classes da caixa à volta (ex: a largura). */\r\n className?: string\r\n}\r\n\r\nconst Check = () => (\r\n <svg viewBox=\"0 0 24 24\" className=\"h-4 w-4 shrink-0 text-proef-blue dark:text-sky-300\" fill=\"none\" stroke=\"currentColor\" strokeWidth={2.5} strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\r\n <path d=\"M20 6 9 17l-5-5\" />\r\n </svg>\r\n)\r\n\r\n/** As `<option>` dos filhos como opções (aceita também `<optgroup>`, achatado). */\r\nfunction optionsFromChildren(children: React.ReactNode): SelectOption[] {\r\n const out: SelectOption[] = []\r\n React.Children.forEach(children, (child) => {\r\n if (!React.isValidElement(child)) return\r\n const props = child.props as { value?: unknown; children?: React.ReactNode; disabled?: boolean }\r\n if (child.type === 'optgroup') return out.push(...optionsFromChildren(props.children))\r\n if (child.type !== 'option') return\r\n const text = React.Children.toArray(props.children).join('')\r\n out.push({ value: props.value !== undefined ? String(props.value) : text, label: text, disabled: props.disabled })\r\n })\r\n return out\r\n}\r\n\r\n/**\r\n * Lista de escolha: um campo que abre uma lista desenhada pela biblioteca (não a do sistema, que não segue o\r\n * tema nem o aspeto dos portais). A opção escolhida tem ✓; com muitas opções há pesquisa.\r\n *\r\n * Teclado: ↑/↓, Home/End, Enter escolhe, Esc fecha; sem pesquisa, uma letra salta para a opção.\r\n *\r\n * Dentro de um [FormField](?path=/docs/v1-components-forms-formfield--docs) fica ligado à etiqueta\r\n * automaticamente. Clicar não chega a uma linha clicável por baixo (ex: numa tabela).\r\n *\r\n * Usa-se como um `<select>`: `<option>` como filhos e `onChange` com `e.target.value`; `placeholder`\r\n * acrescenta uma primeira opção vazia.\r\n */\r\nconst SelectInput = forwardRef<HTMLButtonElement, SelectInputProps>(function SelectInput(\r\n {\r\n value: controlled,\r\n defaultValue = '',\r\n options,\r\n children,\r\n onValueChange,\r\n onChange,\r\n placeholder,\r\n placeholderOption = true,\r\n emptyOption,\r\n searchable,\r\n disabled = false,\r\n required,\r\n invalid,\r\n id,\r\n name,\r\n 'aria-label': ariaLabel,\r\n 'aria-labelledby': ariaLabelledBy,\r\n variant = 'field',\r\n pillClassName = 'bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-200',\r\n tall = false,\r\n labels,\r\n className = '',\r\n },\r\n ref,\r\n) {\r\n const L = { ...DEFAULT_SELECT_LABELS, ...labels }\r\n const field = useField()\r\n const p = useFloatingPanel('left')\r\n useImperativeHandle(ref, () => p.triggerRef.current as HTMLButtonElement)\r\n\r\n const [own, setOwn] = useState(defaultValue)\r\n const value = controlled ?? own\r\n\r\n const list = useMemo<SelectOption[]>(() => {\r\n const base = options ? options.map((o) => (typeof o === 'string' ? { value: o, label: o } : o)) : optionsFromChildren(children)\r\n const empty = emptyOption ?? (placeholder !== undefined && placeholderOption ? placeholder : undefined)\r\n return empty !== undefined && !base.some((o) => o.value === '') ? [{ value: '', label: empty }, ...base] : base\r\n }, [options, children, emptyOption, placeholder, placeholderOption])\r\n\r\n const [query, setQuery] = useState('')\r\n const withSearch = searchable ?? list.length > 10\r\n const visible = useMemo(() => {\r\n const q = query.trim().toLowerCase()\r\n return q ? list.filter((o) => o.label.toLowerCase().includes(q) || o.description?.toLowerCase().includes(q)) : list\r\n }, [list, query])\r\n const [active, setActive] = useState(0)\r\n const searchRef = useRef<HTMLInputElement>(null)\r\n const chosen = list.find((o) => o.value === value)\r\n\r\n // Ao abrir: limpa a pesquisa e põe o teclado na opção escolhida.\r\n useEffect(() => {\r\n if (!p.open) return\r\n setQuery('')\r\n setActive(Math.max(0, list.findIndex((o) => o.value === value)))\r\n if (withSearch) setTimeout(() => searchRef.current?.focus(), 0)\r\n }, [p.open]) // eslint-disable-line react-hooks/exhaustive-deps\r\n\r\n // O item ativo fica sempre à vista ao navegar com o teclado.\r\n useEffect(() => {\r\n if (p.open) p.panelRef.current?.querySelector<HTMLElement>(`[data-index=\"${active}\"]`)?.scrollIntoView?.({ block: 'nearest' })\r\n }, [active, p.open, p.panelRef])\r\n\r\n const choose = (o: SelectOption) => {\r\n if (o.disabled) return\r\n p.close()\r\n p.triggerRef.current?.focus()\r\n if (o.value === value) return\r\n if (controlled === undefined) setOwn(o.value)\r\n onValueChange?.(o.value)\r\n if (onChange) {\r\n const target = { value: o.value, name } as HTMLSelectElement\r\n onChange({ target, currentTarget: target } as React.ChangeEvent<HTMLSelectElement>)\r\n }\r\n }\r\n\r\n const keys = (e: React.KeyboardEvent) => {\r\n if (!p.open) {\r\n if (['ArrowDown', 'ArrowUp', 'Enter', ' '].includes(e.key)) {\r\n e.preventDefault()\r\n p.setOpen(true)\r\n }\r\n return\r\n }\r\n if (e.key === 'Escape') {\r\n e.preventDefault()\r\n e.stopPropagation()\r\n p.close()\r\n p.triggerRef.current?.focus()\r\n } else if (e.key === 'ArrowDown') {\r\n e.preventDefault()\r\n setActive((i) => Math.min(visible.length - 1, i + 1))\r\n } else if (e.key === 'ArrowUp') {\r\n e.preventDefault()\r\n setActive((i) => Math.max(0, i - 1))\r\n } else if (e.key === 'Home') {\r\n e.preventDefault()\r\n setActive(0)\r\n } else if (e.key === 'End') {\r\n e.preventDefault()\r\n setActive(visible.length - 1)\r\n } else if (e.key === 'Enter') {\r\n e.preventDefault()\r\n const o = visible[active]\r\n if (o) choose(o)\r\n } else if (e.key === 'Tab') {\r\n p.close()\r\n } else if (!withSearch && e.key.length === 1) {\r\n // Sem pesquisa: a letra salta para a primeira opção que começa por ela.\r\n const i = visible.findIndex((o) => o.label.toLowerCase().startsWith(e.key.toLowerCase()))\r\n if (i >= 0) setActive(i)\r\n }\r\n }\r\n\r\n const pill = variant === 'pill'\r\n const showsPlaceholder = !chosen || (chosen.value === '' && placeholder !== undefined && emptyOption === undefined)\r\n\r\n return (\r\n <div className={`relative min-w-0 ${className}`} onKeyDown={keys}>\r\n <button\r\n ref={p.triggerRef}\r\n id={id ?? field.id}\r\n type=\"button\"\r\n disabled={disabled}\r\n aria-haspopup=\"listbox\"\r\n aria-expanded={p.open}\r\n aria-label={ariaLabel}\r\n aria-labelledby={ariaLabelledBy}\r\n aria-invalid={invalid || undefined}\r\n aria-required={(required ?? field.required) || undefined}\r\n onClick={(e) => {\r\n // Dentro de uma linha clicável (tabelas), abrir a lista não abre a linha.\r\n e.stopPropagation()\r\n p.setOpen((o) => !o)\r\n }}\r\n className={\r\n pill\r\n ? `inline-flex cursor-pointer appearance-none items-center gap-1 rounded-full px-2.5 py-1 text-xs font-bold whitespace-nowrap transition hover:brightness-95 focus:outline-none focus-visible:ring-2 focus-visible:ring-proef-blue disabled:cursor-not-allowed disabled:opacity-50 ${pillClassName}`\r\n : `flex w-full min-w-0 cursor-pointer appearance-none items-center justify-between gap-2 text-left ${tall ? 'h-[38px]' : 'h-control'} ${fieldBoxClasses}`\r\n }\r\n >\r\n <span className={`truncate ${showsPlaceholder && !pill ? 'text-muted-foreground/70' : ''}`}>\r\n {showsPlaceholder ? (placeholder ?? chosen?.label ?? '') : chosen?.label}\r\n </span>\r\n <ChevronDownIcon size={14} className={`shrink-0 ${pill ? 'opacity-60' : 'text-muted-foreground'}`} />\r\n </button>\r\n {name && <input type=\"hidden\" name={name} value={value} />}\r\n {p.open &&\r\n createPortal(\r\n <div ref={p.panelRef} style={p.style} className={PANEL_CLASS} onClick={(e) => e.stopPropagation()} onKeyDown={keys}>\r\n {withSearch && (\r\n <div className=\"px-2 pt-0.5 pb-1\">\r\n <input\r\n ref={searchRef}\r\n type=\"search\"\r\n value={query}\r\n onChange={(e) => {\r\n setQuery(e.target.value)\r\n setActive(0)\r\n }}\r\n placeholder={L.search}\r\n aria-label={L.searchIn.replace('{label}', ariaLabel ?? '')}\r\n className=\"h-8 w-full rounded-lg border border-slate-300 bg-white px-2.5 text-sm outline-none focus:border-proef-blue dark:border-slate-600 dark:bg-slate-800 dark:text-slate-100\"\r\n />\r\n </div>\r\n )}\r\n <ul role=\"listbox\" aria-label={ariaLabel}>\r\n {visible.length === 0 && <li className=\"px-3 py-2 text-slate-400\">{L.noResults}</li>}\r\n {visible.map((o, i) => {\r\n const selected = o.value === value\r\n return (\r\n <li\r\n key={o.value || '__empty'}\r\n role=\"option\"\r\n aria-selected={selected}\r\n aria-disabled={o.disabled || undefined}\r\n data-index={i}\r\n onMouseEnter={() => setActive(i)}\r\n onClick={() => choose(o)}\r\n className={`${itemClass(i === active)} ${o.disabled ? 'cursor-not-allowed opacity-50' : ''}`}\r\n >\r\n <span className=\"min-w-0 flex-1\">\r\n <span className={`block truncate ${selected ? 'font-semibold' : ''} ${o.value === '' ? 'text-slate-500 dark:text-slate-400' : ''}`}>{o.label}</span>\r\n {o.description && <span className=\"block truncate text-[11px] text-slate-500 dark:text-slate-400\">{o.description}</span>}\r\n </span>\r\n <span className=\"w-4 shrink-0\">{selected && <Check />}</span>\r\n </li>\r\n )\r\n })}\r\n </ul>\r\n </div>,\r\n document.body,\r\n )}\r\n </div>\r\n )\r\n})\r\n\r\nexport default SelectInput\r\nexport { SelectInput }\r\n"],"names":["SelectInput"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SelectInput.js","sources":["../../../../../../src/v1/primitives/forms/SelectInput/SelectInput.tsx"],"sourcesContent":["import React, { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'\r\nimport { createPortal } from 'react-dom'\r\nimport { ChevronDownIcon } from '../../../foundations/icons/icons'\r\nimport { fieldBoxClasses, useField } from '../field'\r\nimport { PANEL_CLASS, itemClass, useFloatingPanel } from '../../../capabilities/overlay/useFloatingPanel'\r\nimport { useDialogContainer } from '../../../components/overlays/Dialog/Dialog'\r\n\r\nexport interface SelectOption {\r\n value: string\r\n label: string\r\n /** Texto pequeno por baixo do rótulo, na lista. */\r\n description?: string\r\n disabled?: boolean\r\n}\r\n\r\nexport interface SelectInputLabels {\r\n /** Texto da caixa de pesquisa. */\r\n search: string\r\n /** Nome da caixa de pesquisa para leitores de ecrã; \"{label}\" é o nome do campo. */\r\n searchIn: string\r\n noResults: string\r\n}\r\n\r\nexport const DEFAULT_SELECT_LABELS: SelectInputLabels = {\r\n search: 'Search…',\r\n searchIn: 'Search in {label}',\r\n noResults: 'No results',\r\n}\r\n\r\nexport interface SelectInputProps {\r\n /** A opção escolhida (controlado). `''` = nenhuma. */\r\n value?: string\r\n defaultValue?: string\r\n /** As opções: texto simples (valor = rótulo) ou `{ value, label, description }`. */\r\n options?: readonly (string | SelectOption)[]\r\n /** Em alternativa a `options`: `<option>` como num `<select>` (compatível com o uso antigo). */\r\n children?: React.ReactNode\r\n /** Recebe o valor escolhido. */\r\n onValueChange?: (value: string) => void\r\n /** Como no `<select>`: `e.target.value` é o valor escolhido. */\r\n onChange?: (e: React.ChangeEvent<HTMLSelectElement>) => void\r\n /** Texto do botão sem nada escolhido; com `placeholderOption`, também a primeira opção (vazia). */\r\n placeholder?: string\r\n /** O `placeholder` é também uma opção (vazia) na lista. Por omissão sim, como no `<select>`. */\r\n placeholderOption?: boolean\r\n /** Primeira opção vazia para desfazer a escolha (ex: \"Todas\"). */\r\n emptyOption?: string\r\n /** Caixa de pesquisa no topo da lista. Por omissão, só com mais de 10 opções. */\r\n searchable?: boolean\r\n disabled?: boolean\r\n required?: boolean\r\n /** Marca o campo a vermelho (ex: obrigatório por escolher ao submeter). */\r\n invalid?: boolean\r\n id?: string\r\n /** Nome no formulário (vai num campo escondido). */\r\n name?: string\r\n 'aria-label'?: string\r\n 'aria-labelledby'?: string\r\n /** `field` (por omissão): campo. `pill`: etiqueta arredondada (ex: o estado numa linha de tabela). A lista é a mesma. */\r\n variant?: 'field' | 'pill'\r\n /** Cores da etiqueta (`pill`), normalmente conforme o valor escolhido; pode trazer contorno (ex: `border border-dashed`). */\r\n pillClassName?: string\r\n /** 38px em vez da altura dos controlos (para alinhar com campos de formulários mais altos). */\r\n tall?: boolean\r\n labels?: Partial<SelectInputLabels>\r\n /** Classes da caixa à volta (ex: a largura). */\r\n className?: string\r\n /**\r\n * Escolha múltipla: cada opção leva ✓ e a lista fica aberta a marcar e desmarcar.\r\n * Usa `values` / `onValuesChange` em vez de `value` / `onValueChange`.\r\n */\r\n multiple?: boolean\r\n /** As escolhidas, com `multiple` (controlado). */\r\n values?: string[]\r\n defaultValues?: string[]\r\n /** Recebe as escolhidas, com `multiple`, pela ordem das opções. */\r\n onValuesChange?: (values: string[]) => void\r\n /** Com `multiple`: o texto do botão quando estão todas escolhidas (ex: \"Todos os tipos\"). */\r\n allLabel?: string\r\n}\r\n\r\nconst Check = () => (\r\n <svg viewBox=\"0 0 24 24\" className=\"h-4 w-4 shrink-0 text-proef-blue dark:text-sky-300\" fill=\"none\" stroke=\"currentColor\" strokeWidth={2.5} strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\r\n <path d=\"M20 6 9 17l-5-5\" />\r\n </svg>\r\n)\r\n\r\n/** As `<option>` dos filhos como opções (aceita também `<optgroup>`, achatado). */\r\nfunction optionsFromChildren(children: React.ReactNode): SelectOption[] {\r\n const out: SelectOption[] = []\r\n React.Children.forEach(children, (child) => {\r\n if (!React.isValidElement(child)) return\r\n const props = child.props as { value?: unknown; children?: React.ReactNode; disabled?: boolean }\r\n if (child.type === 'optgroup') return out.push(...optionsFromChildren(props.children))\r\n if (child.type !== 'option') return\r\n const text = React.Children.toArray(props.children).join('')\r\n out.push({ value: props.value !== undefined ? String(props.value) : text, label: text, disabled: props.disabled })\r\n })\r\n return out\r\n}\r\n\r\n/**\r\n * Lista de escolha: um campo que abre uma lista desenhada pela biblioteca (não a do sistema, que não segue o\r\n * tema nem o aspeto dos portais). A opção escolhida tem ✓; com muitas opções há pesquisa.\r\n *\r\n * Teclado: ↑/↓, Home/End, Enter escolhe, Esc fecha; sem pesquisa, uma letra salta para a opção.\r\n *\r\n * Dentro de um [FormField](?path=/docs/v1-components-forms-formfield--docs) fica ligado à etiqueta\r\n * automaticamente. Clicar não chega a uma linha clicável por baixo (ex: numa tabela).\r\n *\r\n * Usa-se como um `<select>`: `<option>` como filhos e `onChange` com `e.target.value`; `placeholder`\r\n * acrescenta uma primeira opção vazia.\r\n */\r\nconst SelectInput = forwardRef<HTMLButtonElement, SelectInputProps>(function SelectInput(\r\n {\r\n value: controlled,\r\n defaultValue = '',\r\n options,\r\n children,\r\n onValueChange,\r\n onChange,\r\n placeholder,\r\n placeholderOption = true,\r\n emptyOption,\r\n searchable,\r\n disabled = false,\r\n required,\r\n invalid,\r\n id,\r\n name,\r\n 'aria-label': ariaLabel,\r\n 'aria-labelledby': ariaLabelledBy,\r\n variant = 'field',\r\n pillClassName = 'bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-200',\r\n tall = false,\r\n labels,\r\n className = '',\r\n multiple = false,\r\n values: controlledValues,\r\n defaultValues = [],\r\n onValuesChange,\r\n allLabel,\r\n },\r\n ref,\r\n) {\r\n const L = { ...DEFAULT_SELECT_LABELS, ...labels }\r\n const field = useField()\r\n const p = useFloatingPanel('left')\r\n // Dentro de um Dialog a lista abre na janela: no body ficava por baixo dela (z-200)\r\n // e o resto da página está inert — a lista abria mas não se via nem se clicava.\r\n const dialogContainer = useDialogContainer()\r\n useImperativeHandle(ref, () => p.triggerRef.current as HTMLButtonElement)\r\n\r\n const [own, setOwn] = useState(defaultValue)\r\n const value = controlled ?? own\r\n const [ownValues, setOwnValues] = useState(defaultValues)\r\n const picked = controlledValues ?? ownValues\r\n\r\n const list = useMemo<SelectOption[]>(() => {\r\n const base = options ? options.map((o) => (typeof o === 'string' ? { value: o, label: o } : o)) : optionsFromChildren(children)\r\n // Na escolha múltipla não há opção vazia: desfazer é desmarcar.\r\n if (multiple) return base\r\n const empty = emptyOption ?? (placeholder !== undefined && placeholderOption ? placeholder : undefined)\r\n return empty !== undefined && !base.some((o) => o.value === '') ? [{ value: '', label: empty }, ...base] : base\r\n }, [options, children, emptyOption, placeholder, placeholderOption, multiple])\r\n\r\n const [query, setQuery] = useState('')\r\n const withSearch = searchable ?? list.length > 10\r\n const visible = useMemo(() => {\r\n const q = query.trim().toLowerCase()\r\n return q ? list.filter((o) => o.label.toLowerCase().includes(q) || o.description?.toLowerCase().includes(q)) : list\r\n }, [list, query])\r\n const [active, setActive] = useState(0)\r\n const searchRef = useRef<HTMLInputElement>(null)\r\n const chosen = list.find((o) => o.value === value)\r\n\r\n // Ao abrir: limpa a pesquisa e põe o teclado na opção escolhida.\r\n useEffect(() => {\r\n if (!p.open) return\r\n setQuery('')\r\n setActive(Math.max(0, list.findIndex((o) => (multiple ? picked.includes(o.value) : o.value === value))))\r\n if (withSearch) setTimeout(() => searchRef.current?.focus(), 0)\r\n }, [p.open]) // eslint-disable-line react-hooks/exhaustive-deps\r\n\r\n // O item ativo fica sempre à vista ao navegar com o teclado.\r\n useEffect(() => {\r\n if (p.open) p.panelRef.current?.querySelector<HTMLElement>(`[data-index=\"${active}\"]`)?.scrollIntoView?.({ block: 'nearest' })\r\n }, [active, p.open, p.panelRef])\r\n\r\n const choose = (o: SelectOption) => {\r\n if (o.disabled) return\r\n if (multiple) {\r\n // Marca ou desmarca, sem fechar; a ordem é a das opções.\r\n const set = new Set(picked)\r\n if (set.has(o.value)) set.delete(o.value)\r\n else set.add(o.value)\r\n const next = list.filter((x) => set.has(x.value)).map((x) => x.value)\r\n if (controlledValues === undefined) setOwnValues(next)\r\n onValuesChange?.(next)\r\n return\r\n }\r\n p.close()\r\n p.triggerRef.current?.focus()\r\n if (o.value === value) return\r\n if (controlled === undefined) setOwn(o.value)\r\n onValueChange?.(o.value)\r\n if (onChange) {\r\n const target = { value: o.value, name } as HTMLSelectElement\r\n onChange({ target, currentTarget: target } as React.ChangeEvent<HTMLSelectElement>)\r\n }\r\n }\r\n\r\n const keys = (e: React.KeyboardEvent) => {\r\n if (!p.open) {\r\n if (['ArrowDown', 'ArrowUp', 'Enter', ' '].includes(e.key)) {\r\n e.preventDefault()\r\n p.setOpen(true)\r\n }\r\n return\r\n }\r\n if (e.key === 'Escape') {\r\n e.preventDefault()\r\n e.stopPropagation()\r\n p.close()\r\n p.triggerRef.current?.focus()\r\n } else if (e.key === 'ArrowDown') {\r\n e.preventDefault()\r\n setActive((i) => Math.min(visible.length - 1, i + 1))\r\n } else if (e.key === 'ArrowUp') {\r\n e.preventDefault()\r\n setActive((i) => Math.max(0, i - 1))\r\n } else if (e.key === 'Home') {\r\n e.preventDefault()\r\n setActive(0)\r\n } else if (e.key === 'End') {\r\n e.preventDefault()\r\n setActive(visible.length - 1)\r\n } else if (e.key === 'Enter') {\r\n e.preventDefault()\r\n const o = visible[active]\r\n if (o) choose(o)\r\n } else if (e.key === 'Tab') {\r\n p.close()\r\n } else if (!withSearch && e.key.length === 1) {\r\n // Sem pesquisa: a letra salta para a primeira opção que começa por ela.\r\n const i = visible.findIndex((o) => o.label.toLowerCase().startsWith(e.key.toLowerCase()))\r\n if (i >= 0) setActive(i)\r\n }\r\n }\r\n\r\n const pill = variant === 'pill'\r\n const pickedOptions = list.filter((o) => picked.includes(o.value))\r\n const multiText =\r\n allLabel && list.length > 0 && pickedOptions.length === list.length ? allLabel : pickedOptions.map((o) => o.label).join(', ')\r\n const showsPlaceholder = multiple\r\n ? pickedOptions.length === 0\r\n : !chosen || (chosen.value === '' && placeholder !== undefined && emptyOption === undefined)\r\n const buttonText = multiple\r\n ? showsPlaceholder ? (placeholder ?? '') : multiText\r\n : showsPlaceholder ? (placeholder ?? chosen?.label ?? '') : chosen?.label\r\n\r\n return (\r\n <div className={`relative min-w-0 ${className}`} onKeyDown={keys}>\r\n <button\r\n ref={p.triggerRef}\r\n id={id ?? field.id}\r\n type=\"button\"\r\n disabled={disabled}\r\n aria-haspopup=\"listbox\"\r\n aria-expanded={p.open}\r\n aria-label={ariaLabel}\r\n aria-labelledby={ariaLabelledBy}\r\n aria-invalid={invalid || undefined}\r\n aria-required={(required ?? field.required) || undefined}\r\n onClick={(e) => {\r\n // Dentro de uma linha clicável (tabelas), abrir a lista não abre a linha.\r\n e.stopPropagation()\r\n p.setOpen((o) => !o)\r\n }}\r\n className={\r\n pill\r\n ? `inline-flex cursor-pointer appearance-none items-center gap-1 rounded-full px-2.5 py-1 text-xs font-bold whitespace-nowrap transition hover:brightness-95 focus:outline-none focus-visible:ring-2 focus-visible:ring-proef-blue disabled:cursor-not-allowed disabled:opacity-50 ${pillClassName}`\r\n : `flex w-full min-w-0 cursor-pointer appearance-none items-center justify-between gap-2 text-left ${tall ? 'h-[38px]' : 'h-control'} ${fieldBoxClasses}`\r\n }\r\n >\r\n <span className={`truncate ${showsPlaceholder && !pill ? 'text-muted-foreground/70' : ''}`}>\r\n {buttonText}\r\n </span>\r\n <ChevronDownIcon size={14} className={`shrink-0 ${pill ? 'opacity-60' : 'text-muted-foreground'}`} />\r\n </button>\r\n {name && <input type=\"hidden\" name={name} value={multiple ? picked.join(',') : value} />}\r\n {p.open &&\r\n createPortal(\r\n <div ref={p.panelRef} style={p.style} className={PANEL_CLASS} onClick={(e) => e.stopPropagation()} onKeyDown={keys}>\r\n {withSearch && (\r\n <div className=\"px-2 pt-0.5 pb-1\">\r\n <input\r\n ref={searchRef}\r\n type=\"search\"\r\n value={query}\r\n onChange={(e) => {\r\n setQuery(e.target.value)\r\n setActive(0)\r\n }}\r\n placeholder={L.search}\r\n aria-label={L.searchIn.replace('{label}', ariaLabel ?? '')}\r\n className=\"h-8 w-full rounded-lg border border-slate-300 bg-white px-2.5 text-sm outline-none focus:border-proef-blue dark:border-slate-600 dark:bg-slate-800 dark:text-slate-100\"\r\n />\r\n </div>\r\n )}\r\n <ul role=\"listbox\" aria-label={ariaLabel} aria-multiselectable={multiple || undefined}>\r\n {visible.length === 0 && <li className=\"px-3 py-2 text-slate-400\">{L.noResults}</li>}\r\n {visible.map((o, i) => {\r\n const selected = multiple ? picked.includes(o.value) : o.value === value\r\n return (\r\n <li\r\n key={o.value || '__empty'}\r\n role=\"option\"\r\n aria-selected={selected}\r\n aria-disabled={o.disabled || undefined}\r\n data-index={i}\r\n onMouseEnter={() => setActive(i)}\r\n onClick={() => choose(o)}\r\n className={`${itemClass(i === active)} ${o.disabled ? 'cursor-not-allowed opacity-50' : ''}`}\r\n >\r\n <span className=\"min-w-0 flex-1\">\r\n <span className={`block truncate ${selected ? 'font-semibold' : ''} ${o.value === '' ? 'text-slate-500 dark:text-slate-400' : ''}`}>{o.label}</span>\r\n {o.description && <span className=\"block truncate text-[11px] text-slate-500 dark:text-slate-400\">{o.description}</span>}\r\n </span>\r\n <span className=\"w-4 shrink-0\">{selected && <Check />}</span>\r\n </li>\r\n )\r\n })}\r\n </ul>\r\n </div>,\r\n dialogContainer ?? document.body,\r\n )}\r\n </div>\r\n )\r\n})\r\n\r\nexport default SelectInput\r\nexport { SelectInput }\r\n"],"names":["SelectInput"],"mappings":";;;;;;;AAuBO,MAAM,wBAA2C;AAAA,EACtD,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,WAAW;AACb;AAsDA,MAAM,QAAQ,MACZ,oBAAC,OAAA,EAAI,SAAQ,aAAY,WAAU,sDAAqD,MAAK,QAAO,QAAO,gBAAe,aAAa,KAAK,eAAc,SAAQ,gBAAe,SAAQ,eAAW,MAClM,UAAA,oBAAC,QAAA,EAAK,GAAE,kBAAA,CAAkB,EAAA,CAC5B;AAIF,SAAS,oBAAoB,UAA2C;AACtE,QAAM,MAAsB,CAAA;AAC5B,QAAM,SAAS,QAAQ,UAAU,CAAC,UAAU;AAC1C,QAAI,CAAC,MAAM,eAAe,KAAK,EAAG;AAClC,UAAM,QAAQ,MAAM;AACpB,QAAI,MAAM,SAAS,WAAY,QAAO,IAAI,KAAK,GAAG,oBAAoB,MAAM,QAAQ,CAAC;AACrF,QAAI,MAAM,SAAS,SAAU;AAC7B,UAAM,OAAO,MAAM,SAAS,QAAQ,MAAM,QAAQ,EAAE,KAAK,EAAE;AAC3D,QAAI,KAAK,EAAE,OAAO,MAAM,UAAU,SAAY,OAAO,MAAM,KAAK,IAAI,MAAM,OAAO,MAAM,UAAU,MAAM,UAAU;AAAA,EACnH,CAAC;AACD,SAAO;AACT;AAcA,MAAM,cAAc,WAAgD,SAASA,aAC3E;AAAA,EACE,OAAO;AAAA,EACP,eAAe;AAAA,EACf;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,oBAAoB;AAAA,EACpB;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd,mBAAmB;AAAA,EACnB,UAAU;AAAA,EACV,gBAAgB;AAAA,EAChB,OAAO;AAAA,EACP;AAAA,EACA,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,gBAAgB,CAAA;AAAA,EAChB;AAAA,EACA;AACF,GACA,KACA;AACA,QAAM,IAAI,EAAE,GAAG,uBAAuB,GAAG,OAAA;AACzC,QAAM,QAAQ,SAAA;AACd,QAAM,IAAI,iBAAiB,MAAM;AAGjC,QAAM,kBAAkB,mBAAA;AACxB,sBAAoB,KAAK,MAAM,EAAE,WAAW,OAA4B;AAExE,QAAM,CAAC,KAAK,MAAM,IAAI,SAAS,YAAY;AAC3C,QAAM,QAAQ,cAAc;AAC5B,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,aAAa;AACxD,QAAM,SAAS,oBAAoB;AAEnC,QAAM,OAAO,QAAwB,MAAM;AACzC,UAAM,OAAO,UAAU,QAAQ,IAAI,CAAC,MAAO,OAAO,MAAM,WAAW,EAAE,OAAO,GAAG,OAAO,EAAA,IAAM,CAAE,IAAI,oBAAoB,QAAQ;AAE9H,QAAI,SAAU,QAAO;AACrB,UAAM,QAAQ,gBAAgB,gBAAgB,UAAa,oBAAoB,cAAc;AAC7F,WAAO,UAAU,UAAa,CAAC,KAAK,KAAK,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,CAAC,EAAE,OAAO,IAAI,OAAO,SAAS,GAAG,IAAI,IAAI;AAAA,EAC7G,GAAG,CAAC,SAAS,UAAU,aAAa,aAAa,mBAAmB,QAAQ,CAAC;AAE7E,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,EAAE;AACrC,QAAM,aAAa,cAAc,KAAK,SAAS;AAC/C,QAAM,UAAU,QAAQ,MAAM;AAC5B,UAAM,IAAI,MAAM,KAAA,EAAO,YAAA;AACvB,WAAO,IAAI,KAAK,OAAO,CAAC,MAAM,EAAE,MAAM,YAAA,EAAc,SAAS,CAAC,KAAK,EAAE,aAAa,YAAA,EAAc,SAAS,CAAC,CAAC,IAAI;AAAA,EACjH,GAAG,CAAC,MAAM,KAAK,CAAC;AAChB,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,CAAC;AACtC,QAAM,YAAY,OAAyB,IAAI;AAC/C,QAAM,SAAS,KAAK,KAAK,CAAC,MAAM,EAAE,UAAU,KAAK;AAGjD,YAAU,MAAM;AACd,QAAI,CAAC,EAAE,KAAM;AACb,aAAS,EAAE;AACX,cAAU,KAAK,IAAI,GAAG,KAAK,UAAU,CAAC,MAAO,WAAW,OAAO,SAAS,EAAE,KAAK,IAAI,EAAE,UAAU,KAAM,CAAC,CAAC;AACvG,QAAI,WAAY,YAAW,MAAM,UAAU,SAAS,MAAA,GAAS,CAAC;AAAA,EAChE,GAAG,CAAC,EAAE,IAAI,CAAC;AAGX,YAAU,MAAM;AACd,QAAI,EAAE,KAAM,GAAE,SAAS,SAAS,cAA2B,gBAAgB,MAAM,IAAI,GAAG,iBAAiB,EAAE,OAAO,WAAW;AAAA,EAC/H,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC;AAE/B,QAAM,SAAS,CAAC,MAAoB;AAClC,QAAI,EAAE,SAAU;AAChB,QAAI,UAAU;AAEZ,YAAM,MAAM,IAAI,IAAI,MAAM;AAC1B,UAAI,IAAI,IAAI,EAAE,KAAK,EAAG,KAAI,OAAO,EAAE,KAAK;AAAA,UACnC,KAAI,IAAI,EAAE,KAAK;AACpB,YAAM,OAAO,KAAK,OAAO,CAAC,MAAM,IAAI,IAAI,EAAE,KAAK,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK;AACpE,UAAI,qBAAqB,OAAW,cAAa,IAAI;AACrD,uBAAiB,IAAI;AACrB;AAAA,IACF;AACA,MAAE,MAAA;AACF,MAAE,WAAW,SAAS,MAAA;AACtB,QAAI,EAAE,UAAU,MAAO;AACvB,QAAI,eAAe,OAAW,QAAO,EAAE,KAAK;AAC5C,oBAAgB,EAAE,KAAK;AACvB,QAAI,UAAU;AACZ,YAAM,SAAS,EAAE,OAAO,EAAE,OAAO,KAAA;AACjC,eAAS,EAAE,QAAQ,eAAe,OAAA,CAAgD;AAAA,IACpF;AAAA,EACF;AAEA,QAAM,OAAO,CAAC,MAA2B;AACvC,QAAI,CAAC,EAAE,MAAM;AACX,UAAI,CAAC,aAAa,WAAW,SAAS,GAAG,EAAE,SAAS,EAAE,GAAG,GAAG;AAC1D,UAAE,eAAA;AACF,UAAE,QAAQ,IAAI;AAAA,MAChB;AACA;AAAA,IACF;AACA,QAAI,EAAE,QAAQ,UAAU;AACtB,QAAE,eAAA;AACF,QAAE,gBAAA;AACF,QAAE,MAAA;AACF,QAAE,WAAW,SAAS,MAAA;AAAA,IACxB,WAAW,EAAE,QAAQ,aAAa;AAChC,QAAE,eAAA;AACF,gBAAU,CAAC,MAAM,KAAK,IAAI,QAAQ,SAAS,GAAG,IAAI,CAAC,CAAC;AAAA,IACtD,WAAW,EAAE,QAAQ,WAAW;AAC9B,QAAE,eAAA;AACF,gBAAU,CAAC,MAAM,KAAK,IAAI,GAAG,IAAI,CAAC,CAAC;AAAA,IACrC,WAAW,EAAE,QAAQ,QAAQ;AAC3B,QAAE,eAAA;AACF,gBAAU,CAAC;AAAA,IACb,WAAW,EAAE,QAAQ,OAAO;AAC1B,QAAE,eAAA;AACF,gBAAU,QAAQ,SAAS,CAAC;AAAA,IAC9B,WAAW,EAAE,QAAQ,SAAS;AAC5B,QAAE,eAAA;AACF,YAAM,IAAI,QAAQ,MAAM;AACxB,UAAI,UAAU,CAAC;AAAA,IACjB,WAAW,EAAE,QAAQ,OAAO;AAC1B,QAAE,MAAA;AAAA,IACJ,WAAW,CAAC,cAAc,EAAE,IAAI,WAAW,GAAG;AAE5C,YAAM,IAAI,QAAQ,UAAU,CAAC,MAAM,EAAE,MAAM,YAAA,EAAc,WAAW,EAAE,IAAI,YAAA,CAAa,CAAC;AACxF,UAAI,KAAK,EAAG,WAAU,CAAC;AAAA,IACzB;AAAA,EACF;AAEA,QAAM,OAAO,YAAY;AACzB,QAAM,gBAAgB,KAAK,OAAO,CAAC,MAAM,OAAO,SAAS,EAAE,KAAK,CAAC;AACjE,QAAM,YACJ,YAAY,KAAK,SAAS,KAAK,cAAc,WAAW,KAAK,SAAS,WAAW,cAAc,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,IAAI;AAC9H,QAAM,mBAAmB,WACrB,cAAc,WAAW,IACzB,CAAC,UAAW,OAAO,UAAU,MAAM,gBAAgB,UAAa,gBAAgB;AACpF,QAAM,aAAa,WACf,mBAAoB,eAAe,KAAM,YACzC,mBAAoB,eAAe,QAAQ,SAAS,KAAM,QAAQ;AAEtE,8BACG,OAAA,EAAI,WAAW,oBAAoB,SAAS,IAAI,WAAW,MAC1D,UAAA;AAAA,IAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,KAAK,EAAE;AAAA,QACP,IAAI,MAAM,MAAM;AAAA,QAChB,MAAK;AAAA,QACL;AAAA,QACA,iBAAc;AAAA,QACd,iBAAe,EAAE;AAAA,QACjB,cAAY;AAAA,QACZ,mBAAiB;AAAA,QACjB,gBAAc,WAAW;AAAA,QACzB,kBAAgB,YAAY,MAAM,aAAa;AAAA,QAC/C,SAAS,CAAC,MAAM;AAEd,YAAE,gBAAA;AACF,YAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;AAAA,QACrB;AAAA,QACA,WACE,OACI,mRAAmR,aAAa,KAChS,mGAAmG,OAAO,aAAa,WAAW,IAAI,eAAe;AAAA,QAG3J,UAAA;AAAA,UAAA,oBAAC,QAAA,EAAK,WAAW,YAAY,oBAAoB,CAAC,OAAO,6BAA6B,EAAE,IACrF,UAAA,WAAA,CACH;AAAA,UACA,oBAAC,mBAAgB,MAAM,IAAI,WAAW,YAAY,OAAO,eAAe,uBAAuB,GAAA,CAAI;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA;AAAA,IAEpG,QAAQ,oBAAC,SAAA,EAAM,MAAK,UAAS,MAAY,OAAO,WAAW,OAAO,KAAK,GAAG,IAAI,MAAA,CAAO;AAAA,IACrF,EAAE,QACD;AAAA,2BACG,OAAA,EAAI,KAAK,EAAE,UAAU,OAAO,EAAE,OAAO,WAAW,aAAa,SAAS,CAAC,MAAM,EAAE,gBAAA,GAAmB,WAAW,MAC3G,UAAA;AAAA,QAAA,cACC,oBAAC,OAAA,EAAI,WAAU,oBACb,UAAA;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,KAAK;AAAA,YACL,MAAK;AAAA,YACL,OAAO;AAAA,YACP,UAAU,CAAC,MAAM;AACf,uBAAS,EAAE,OAAO,KAAK;AACvB,wBAAU,CAAC;AAAA,YACb;AAAA,YACA,aAAa,EAAE;AAAA,YACf,cAAY,EAAE,SAAS,QAAQ,WAAW,aAAa,EAAE;AAAA,YACzD,WAAU;AAAA,UAAA;AAAA,QAAA,GAEd;AAAA,QAEF,qBAAC,QAAG,MAAK,WAAU,cAAY,WAAW,wBAAsB,YAAY,QACzE,UAAA;AAAA,UAAA,QAAQ,WAAW,KAAK,oBAAC,QAAG,WAAU,4BAA4B,YAAE,UAAA,CAAU;AAAA,UAC9E,QAAQ,IAAI,CAAC,GAAG,MAAM;AACrB,kBAAM,WAAW,WAAW,OAAO,SAAS,EAAE,KAAK,IAAI,EAAE,UAAU;AACnE,mBACE;AAAA,cAAC;AAAA,cAAA;AAAA,gBAEC,MAAK;AAAA,gBACL,iBAAe;AAAA,gBACf,iBAAe,EAAE,YAAY;AAAA,gBAC7B,cAAY;AAAA,gBACZ,cAAc,MAAM,UAAU,CAAC;AAAA,gBAC/B,SAAS,MAAM,OAAO,CAAC;AAAA,gBACvB,WAAW,GAAG,UAAU,MAAM,MAAM,CAAC,IAAI,EAAE,WAAW,kCAAkC,EAAE;AAAA,gBAE1F,UAAA;AAAA,kBAAA,qBAAC,QAAA,EAAK,WAAU,kBACd,UAAA;AAAA,oBAAA,oBAAC,QAAA,EAAK,WAAW,kBAAkB,WAAW,kBAAkB,EAAE,IAAI,EAAE,UAAU,KAAK,uCAAuC,EAAE,IAAK,YAAE,OAAM;AAAA,oBAC5I,EAAE,eAAe,oBAAC,UAAK,WAAU,iEAAiE,YAAE,YAAA,CAAY;AAAA,kBAAA,GACnH;AAAA,sCACC,QAAA,EAAK,WAAU,gBAAgB,UAAA,YAAY,oBAAC,SAAM,EAAA,CAAG;AAAA,gBAAA;AAAA,cAAA;AAAA,cAbjD,EAAE,SAAS;AAAA,YAAA;AAAA,UAgBtB,CAAC;AAAA,QAAA,EAAA,CACH;AAAA,MAAA,GACF;AAAA,MACA,mBAAmB,SAAS;AAAA,IAAA;AAAA,EAC9B,GACJ;AAEJ,CAAC;"}
|