@proef/common-ui 2.2.1 → 2.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"scanQRCodes.js","sources":["../../../../../../src/v1/capabilities/documents/QRScanning/scanQRCodes.ts"],"sourcesContent":["/**\n * Leitura de QR codes em imagens e PDFs, no browser.\n *\n * Só encontra os códigos e devolve o texto de cada um; o que fazer com eles\n * (ex: interpretar o QR de uma fatura) é da app.\n *\n * Adaptado de uma app interna. Os leitores\n * automáticos falham em imagens de baixa resolução (miniaturas arrastadas do\n * email ou do explorador), por isso tenta-se, por ordem do mais rápido ao mais\n * lento, três motores sobre quatro versões da imagem:\n * - motores: qr-scanner, zxing-wasm (3 estratégias de binarização) e\n * @zxing/library (o mais lento, último recurso);\n * - imagem: original, ampliada, e ampliada a cinzento com contraste ×2,5 e ×5.\n * Os PDFs são primeiro desenhados (1.ª página) e lidos como imagem.\n *\n * As bibliotecas (~1 MB, com o WebAssembly) só são carregadas na primeira leitura.\n */\nimport { loadPdfjs } from '../../../primitives/documents/PdfPreview/pdfjs'\n\nexport interface ScanQRCodesOptions {\n /**\n * Continuar a procurar até um código satisfazer esta condição (ex: ser o QR\n * de uma fatura). Um documento pode ter vários QR codes e o primeiro a ser\n * lido é arbitrário. Por omissão, pára no primeiro código encontrado.\n */\n until?: (code: string) => boolean\n}\n\nlet wasmUrl: string | null = null\nlet enginesPromise: Promise<Engines> | null = null\n\n/**\n * Indica onde está o `zxing_reader.wasm` servido pela app. Chamar uma vez no\n * arranque, antes da primeira leitura. Sem isto, o ficheiro é descarregado da\n * CDN jsDelivr (pode ser bloqueado por redes ou políticas de segurança).\n */\nexport function configureQRScanner(options: { wasmUrl: string }) {\n wasmUrl = options.wasmUrl\n}\n\ntype Engines = {\n QrScanner: typeof import('qr-scanner').default\n readBarcodes: typeof import('zxing-wasm/reader').readBarcodes\n zxingJs: typeof import('@zxing/library')\n}\n\nfunction loadEngines(): Promise<Engines> {\n if (!enginesPromise) {\n enginesPromise = Promise.all([import('qr-scanner'), import('zxing-wasm/reader'), import('@zxing/library')])\n .then(([qrScanner, zxingWasm, zxingJs]) => {\n if (wasmUrl) {\n const url = wasmUrl\n zxingWasm.prepareZXingModule({ overrides: { locateFile: (path: string, prefix: string) => (path.endsWith('.wasm') ? url : prefix + path) } })\n }\n return { QrScanner: qrScanner.default, readBarcodes: zxingWasm.readBarcodes, zxingJs }\n })\n .catch((e) => {\n enginesPromise = null\n throw e\n })\n }\n return enginesPromise\n}\n\n// Abaixo deste lado mais curto (px), a imagem é ampliada antes de ler.\nconst UPSCALE_TARGET = 800\nconst BINARIZERS = ['LocalAverage', 'GlobalHistogram', 'FixedThreshold'] as const\n\nfunction loadImage(file: Blob): Promise<HTMLImageElement> {\n return new Promise((resolve, reject) => {\n const img = new Image()\n const url = URL.createObjectURL(file)\n img.onload = () => {\n URL.revokeObjectURL(url)\n resolve(img)\n }\n img.onerror = () => {\n URL.revokeObjectURL(url)\n reject(new Error('Imagem ilegível'))\n }\n img.src = url\n })\n}\n\n/** Imagem ampliada; com `contrast`, também a cinzento com o contraste esticado à volta de 128. */\nasync function drawVariant(file: Blob, contrast?: number): Promise<HTMLCanvasElement> {\n const img = await loadImage(file)\n const scale = Math.max(1, UPSCALE_TARGET / Math.min(img.naturalWidth, img.naturalHeight))\n const canvas = document.createElement('canvas')\n canvas.width = Math.round(img.naturalWidth * scale)\n canvas.height = Math.round(img.naturalHeight * scale)\n const ctx = canvas.getContext('2d', { willReadFrequently: true })!\n ctx.imageSmoothingEnabled = true\n ctx.imageSmoothingQuality = 'high'\n ctx.drawImage(img, 0, 0, canvas.width, canvas.height)\n if (contrast) {\n const d = ctx.getImageData(0, 0, canvas.width, canvas.height)\n const px = d.data\n for (let i = 0; i < px.length; i += 4) {\n const g = 0.299 * px[i] + 0.587 * px[i + 1] + 0.114 * px[i + 2]\n px[i] = px[i + 1] = px[i + 2] = Math.max(0, Math.min(255, 128 + contrast * (g - 128)))\n }\n ctx.putImageData(d, 0, 0)\n }\n return canvas\n}\n\nconst pixels = (canvas: HTMLCanvasElement) => canvas.getContext('2d', { willReadFrequently: true })!.getImageData(0, 0, canvas.width, canvas.height)\n\n/** 1.ª página do PDF como PNG, numa escala que mantém o QR legível (lado maior ≈ 2200 px). */\nasync function renderPdfFirstPage(file: File): Promise<Blob> {\n const { getDocument } = await loadPdfjs()\n const pdf = await getDocument({ data: new Uint8Array(await file.arrayBuffer()) }).promise\n try {\n const page = await pdf.getPage(1)\n const base = page.getViewport({ scale: 1 })\n const viewport = page.getViewport({ scale: Math.min(4, Math.max(1, 2200 / Math.max(base.width, base.height))) })\n const canvas = document.createElement('canvas')\n canvas.width = Math.ceil(viewport.width)\n canvas.height = Math.ceil(viewport.height)\n const ctx = canvas.getContext('2d', { willReadFrequently: true })!\n ctx.fillStyle = '#fff'\n ctx.fillRect(0, 0, canvas.width, canvas.height)\n await page.render({ canvasContext: ctx, viewport }).promise\n return await new Promise<Blob>((resolve, reject) => canvas.toBlob((b) => (b ? resolve(b) : reject(new Error('toBlob falhou'))), 'image/png'))\n } finally {\n pdf.destroy()\n }\n}\n\n/**\n * Lê os QR codes de uma imagem ou PDF (1.ª página).\n *\n * Devolve o texto de cada código encontrado, sem repetidos, pela ordem em que\n * foram lidos; vazio se não houver nenhum ou se o ficheiro não se puder ler.\n * Depois de encontrar o que procura, faz ainda uma passagem para recolher os\n * outros códigos da página (ex: QR do banco ao lado do QR fiscal).\n */\nexport async function scanQRCodes(file: File, options: ScanQRCodesOptions = {}): Promise<string[]> {\n const until = options.until ?? (() => true)\n const { QrScanner, readBarcodes, zxingJs } = await loadEngines()\n\n let source: Blob = file\n if (file.type === 'application/pdf') {\n try {\n source = await renderPdfFirstPage(file)\n } catch {\n return []\n }\n }\n\n const codes: string[] = []\n const add = (text: string | null | undefined) => {\n if (text && !codes.includes(text)) codes.push(text)\n }\n const done = () => codes.some(until)\n\n const qrScanner = async (input: Blob | HTMLCanvasElement) => {\n try {\n add((await QrScanner.scanImage(input, { returnDetailedScanResult: true })).data)\n } catch {\n /* nada encontrado */\n }\n }\n // O único motor que devolve todos os códigos da imagem. `all`: correr todas as\n // estratégias e juntar o que cada uma encontra (uma pode ver um código e falhar outro).\n const zxingWasm = async (input: Blob | ImageData, all = false) => {\n for (const binarizer of BINARIZERS) {\n try {\n const texts = (await readBarcodes(input, { formats: ['QRCode'], tryHarder: true, tryDenoise: true, binarizer }))\n .filter((r) => r.isValid)\n .map((r) => r.text)\n texts.forEach(add)\n if (texts.length && !all) return\n } catch {\n /* estratégia seguinte */\n }\n }\n }\n const zxingJsDecode = (canvas: HTMLCanvasElement) => {\n try {\n const { data, width, height } = pixels(canvas)\n const bitmap = new zxingJs.BinaryBitmap(new zxingJs.HybridBinarizer(new zxingJs.RGBLuminanceSource(data, width, height)))\n add(new zxingJs.MultiFormatReader().decode(bitmap).getText())\n } catch {\n /* nada encontrado */\n }\n }\n\n let upscaled: HTMLCanvasElement | null = null\n const collectRemaining = async () => {\n await zxingWasm(source, true)\n upscaled ??= await drawVariant(source).catch(() => null)\n if (upscaled) await zxingWasm(pixels(upscaled), true)\n return codes\n }\n\n try {\n // 1–2. Original: o mais rápido primeiro.\n await qrScanner(source)\n if (done()) return collectRemaining()\n await zxingWasm(source)\n if (done()) return collectRemaining()\n\n // 3. Ampliada (cores intactas): ajuda nas miniaturas.\n upscaled = await drawVariant(source)\n await qrScanner(upscaled)\n if (done()) return collectRemaining()\n await zxingWasm(pixels(upscaled))\n if (done()) return collectRemaining()\n zxingJsDecode(upscaled)\n if (done()) return collectRemaining()\n\n // 4. Ampliada a cinzento com contraste: para imagens difíceis.\n for (const contrast of [2.5, 5]) {\n const canvas = await drawVariant(source, contrast)\n await qrScanner(canvas)\n if (done()) return collectRemaining()\n await zxingWasm(pixels(canvas))\n if (done()) return collectRemaining()\n zxingJsDecode(canvas)\n if (done()) return collectRemaining()\n }\n } catch {\n // Imagem ilegível: devolve o que já tiver encontrado.\n return codes\n }\n\n return codes.length ? collectRemaining() : codes\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"scanQRCodes.js","sources":["../../../../../../src/v1/capabilities/documents/QRScanning/scanQRCodes.ts"],"sourcesContent":["/**\r\n * Leitura de QR codes em imagens e PDFs, no browser.\r\n *\r\n * Só encontra os códigos e devolve o texto de cada um; o que fazer com eles\r\n * (ex: interpretar o QR de uma fatura) é da app.\r\n *\r\n * Adaptado de uma app interna. Os leitores\r\n * automáticos falham em imagens de baixa resolução (miniaturas arrastadas do\r\n * email ou do explorador), por isso tenta-se, por ordem do mais rápido ao mais\r\n * lento, três motores sobre quatro versões da imagem:\r\n * - motores: qr-scanner, zxing-wasm (3 estratégias de binarização) e\r\n * @zxing/library (o mais lento, último recurso);\r\n * - imagem: original, ampliada, e ampliada a cinzento com contraste ×2,5 e ×5.\r\n * Os PDFs são primeiro desenhados (1.ª página) e lidos como imagem.\r\n *\r\n * As bibliotecas (~1 MB, com o WebAssembly) só são carregadas na primeira leitura.\r\n */\r\nimport { loadPdfjs } from '../../../primitives/documents/PdfPreview/pdfjs'\r\n\r\nexport interface ScanQRCodesOptions {\r\n /**\r\n * Continuar a procurar até um código satisfazer esta condição (ex: ser o QR\r\n * de uma fatura). Um documento pode ter vários QR codes e o primeiro a ser\r\n * lido é arbitrário. Por omissão, pára no primeiro código encontrado.\r\n */\r\n until?: (code: string) => boolean\r\n}\r\n\r\nlet wasmUrl: string | null = null\r\nlet enginesPromise: Promise<Engines> | null = null\r\n\r\n/**\r\n * Indica onde está o `zxing_reader.wasm` servido pela app. Chamar uma vez no\r\n * arranque, antes da primeira leitura. Sem isto, o ficheiro é descarregado da\r\n * CDN jsDelivr (pode ser bloqueado por redes ou políticas de segurança).\r\n */\r\nexport function configureQRScanner(options: { wasmUrl: string }) {\r\n wasmUrl = options.wasmUrl\r\n}\r\n\r\ntype Engines = {\r\n QrScanner: typeof import('qr-scanner').default\r\n readBarcodes: typeof import('zxing-wasm/reader').readBarcodes\r\n zxingJs: typeof import('@zxing/library')\r\n}\r\n\r\nfunction loadEngines(): Promise<Engines> {\r\n if (!enginesPromise) {\r\n enginesPromise = Promise.all([import('qr-scanner'), import('zxing-wasm/reader'), import('@zxing/library')])\r\n .then(([qrScanner, zxingWasm, zxingJs]) => {\r\n if (wasmUrl) {\r\n const url = wasmUrl\r\n zxingWasm.prepareZXingModule({ overrides: { locateFile: (path: string, prefix: string) => (path.endsWith('.wasm') ? url : prefix + path) } })\r\n }\r\n return { QrScanner: qrScanner.default, readBarcodes: zxingWasm.readBarcodes, zxingJs }\r\n })\r\n .catch((e) => {\r\n enginesPromise = null\r\n throw e\r\n })\r\n }\r\n return enginesPromise\r\n}\r\n\r\n// Abaixo deste lado mais curto (px), a imagem é ampliada antes de ler.\r\nconst UPSCALE_TARGET = 800\r\nconst BINARIZERS = ['LocalAverage', 'GlobalHistogram', 'FixedThreshold'] as const\r\n\r\nfunction loadImage(file: Blob): Promise<HTMLImageElement> {\r\n return new Promise((resolve, reject) => {\r\n const img = new Image()\r\n const url = URL.createObjectURL(file)\r\n img.onload = () => {\r\n URL.revokeObjectURL(url)\r\n resolve(img)\r\n }\r\n img.onerror = () => {\r\n URL.revokeObjectURL(url)\r\n reject(new Error('Imagem ilegível'))\r\n }\r\n img.src = url\r\n })\r\n}\r\n\r\n/** Imagem ampliada; com `contrast`, também a cinzento com o contraste esticado à volta de 128. */\r\nasync function drawVariant(file: Blob, contrast?: number): Promise<HTMLCanvasElement> {\r\n const img = await loadImage(file)\r\n const scale = Math.max(1, UPSCALE_TARGET / Math.min(img.naturalWidth, img.naturalHeight))\r\n const canvas = document.createElement('canvas')\r\n canvas.width = Math.round(img.naturalWidth * scale)\r\n canvas.height = Math.round(img.naturalHeight * scale)\r\n const ctx = canvas.getContext('2d', { willReadFrequently: true })!\r\n ctx.imageSmoothingEnabled = true\r\n ctx.imageSmoothingQuality = 'high'\r\n ctx.drawImage(img, 0, 0, canvas.width, canvas.height)\r\n if (contrast) {\r\n const d = ctx.getImageData(0, 0, canvas.width, canvas.height)\r\n const px = d.data\r\n for (let i = 0; i < px.length; i += 4) {\r\n const g = 0.299 * px[i] + 0.587 * px[i + 1] + 0.114 * px[i + 2]\r\n px[i] = px[i + 1] = px[i + 2] = Math.max(0, Math.min(255, 128 + contrast * (g - 128)))\r\n }\r\n ctx.putImageData(d, 0, 0)\r\n }\r\n return canvas\r\n}\r\n\r\nconst pixels = (canvas: HTMLCanvasElement) => canvas.getContext('2d', { willReadFrequently: true })!.getImageData(0, 0, canvas.width, canvas.height)\r\n\r\n/** 1.ª página do PDF como PNG, numa escala que mantém o QR legível (lado maior ≈ 2200 px). */\r\nasync function renderPdfFirstPage(file: File): Promise<Blob> {\r\n const { getDocument } = await loadPdfjs()\r\n const pdf = await getDocument({ data: new Uint8Array(await file.arrayBuffer()) }).promise\r\n try {\r\n const page = await pdf.getPage(1)\r\n const base = page.getViewport({ scale: 1 })\r\n const viewport = page.getViewport({ scale: Math.min(4, Math.max(1, 2200 / Math.max(base.width, base.height))) })\r\n const canvas = document.createElement('canvas')\r\n canvas.width = Math.ceil(viewport.width)\r\n canvas.height = Math.ceil(viewport.height)\r\n const ctx = canvas.getContext('2d', { willReadFrequently: true })!\r\n ctx.fillStyle = '#fff'\r\n ctx.fillRect(0, 0, canvas.width, canvas.height)\r\n await page.render({ canvasContext: ctx, viewport }).promise\r\n return await new Promise<Blob>((resolve, reject) => canvas.toBlob((b) => (b ? resolve(b) : reject(new Error('toBlob falhou'))), 'image/png'))\r\n } finally {\r\n pdf.destroy()\r\n }\r\n}\r\n\r\n/**\r\n * Lê os QR codes de uma imagem ou PDF (1.ª página).\r\n *\r\n * Devolve o texto de cada código encontrado, sem repetidos, pela ordem em que\r\n * foram lidos; vazio se não houver nenhum ou se o ficheiro não se puder ler.\r\n * Depois de encontrar o que procura, faz ainda uma passagem para recolher os\r\n * outros códigos da página (ex: QR do banco ao lado do QR fiscal).\r\n */\r\nexport async function scanQRCodes(file: File, options: ScanQRCodesOptions = {}): Promise<string[]> {\r\n const until = options.until ?? (() => true)\r\n const { QrScanner, readBarcodes, zxingJs } = await loadEngines()\r\n\r\n let source: Blob = file\r\n if (file.type === 'application/pdf') {\r\n try {\r\n source = await renderPdfFirstPage(file)\r\n } catch {\r\n return []\r\n }\r\n }\r\n\r\n const codes: string[] = []\r\n const add = (text: string | null | undefined) => {\r\n if (text && !codes.includes(text)) codes.push(text)\r\n }\r\n const done = () => codes.some(until)\r\n\r\n const qrScanner = async (input: Blob | HTMLCanvasElement) => {\r\n try {\r\n add((await QrScanner.scanImage(input, { returnDetailedScanResult: true })).data)\r\n } catch {\r\n /* nada encontrado */\r\n }\r\n }\r\n // O único motor que devolve todos os códigos da imagem. `all`: correr todas as\r\n // estratégias e juntar o que cada uma encontra (uma pode ver um código e falhar outro).\r\n const zxingWasm = async (input: Blob | ImageData, all = false) => {\r\n for (const binarizer of BINARIZERS) {\r\n try {\r\n const texts = (await readBarcodes(input, { formats: ['QRCode'], tryHarder: true, tryDenoise: true, binarizer }))\r\n .filter((r) => r.isValid)\r\n .map((r) => r.text)\r\n texts.forEach(add)\r\n if (texts.length && !all) return\r\n } catch {\r\n /* estratégia seguinte */\r\n }\r\n }\r\n }\r\n const zxingJsDecode = (canvas: HTMLCanvasElement) => {\r\n try {\r\n const { data, width, height } = pixels(canvas)\r\n const bitmap = new zxingJs.BinaryBitmap(new zxingJs.HybridBinarizer(new zxingJs.RGBLuminanceSource(data, width, height)))\r\n add(new zxingJs.MultiFormatReader().decode(bitmap).getText())\r\n } catch {\r\n /* nada encontrado */\r\n }\r\n }\r\n\r\n let upscaled: HTMLCanvasElement | null = null\r\n const collectRemaining = async () => {\r\n await zxingWasm(source, true)\r\n upscaled ??= await drawVariant(source).catch(() => null)\r\n if (upscaled) await zxingWasm(pixels(upscaled), true)\r\n return codes\r\n }\r\n\r\n try {\r\n // 1–2. Original: o mais rápido primeiro.\r\n await qrScanner(source)\r\n if (done()) return collectRemaining()\r\n await zxingWasm(source)\r\n if (done()) return collectRemaining()\r\n\r\n // 3. Ampliada (cores intactas): ajuda nas miniaturas.\r\n upscaled = await drawVariant(source)\r\n await qrScanner(upscaled)\r\n if (done()) return collectRemaining()\r\n await zxingWasm(pixels(upscaled))\r\n if (done()) return collectRemaining()\r\n zxingJsDecode(upscaled)\r\n if (done()) return collectRemaining()\r\n\r\n // 4. Ampliada a cinzento com contraste: para imagens difíceis.\r\n for (const contrast of [2.5, 5]) {\r\n const canvas = await drawVariant(source, contrast)\r\n await qrScanner(canvas)\r\n if (done()) return collectRemaining()\r\n await zxingWasm(pixels(canvas))\r\n if (done()) return collectRemaining()\r\n zxingJsDecode(canvas)\r\n if (done()) return collectRemaining()\r\n }\r\n } catch {\r\n // Imagem ilegível: devolve o que já tiver encontrado.\r\n return codes\r\n }\r\n\r\n return codes.length ? collectRemaining() : codes\r\n}\r\n"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"DocumentDropzone.js","sources":["../../../../../../src/v1/components/documents/DocumentDropzone/DocumentDropzone.tsx"],"sourcesContent":["import React, { useEffect, useRef, useState } from 'react'\nimport PdfPreview from '../../../primitives/documents/PdfPreview/PdfPreview'\n\nexport interface DocumentDropzoneLabels {\n title: string\n hint?: string\n change: string\n remove: string\n /** Texto enquanto procura QR codes (só com `onQRCodes`), ex: \"A procurar QR codes…\". */\n scanningQR?: string\n /**\n * Mensagem quando um ficheiro não é permitido pelo `accept`, ex: \"Formato não\n * permitido. Aceita PDF, JPG ou PNG.\" Aparece logo ao arrastar e depois de largar.\n */\n rejected: string\n}\n\nexport interface DocumentDropzoneProps {\n file: File | null\n onFileChange: (file: File | null) => void\n /**\n * Tipos permitidos: extensões e/ou tipos MIME, separados por vírgulas, ex:\n * \".pdf,.jpg,.jpeg,.png\". Os restantes são recusados (ver `labels.rejected` e\n * `onReject`). Sem `accept`, aceita qualquer ficheiro.\n *\n * - `image/*` inclui SVG (que pode conter código) e HEIC (sem pré-visualização\n * na maioria dos browsers). Para documentos, prefira uma lista explícita.\n * - Só se verificam o nome e o tipo declarado, não o conteúdo: um ficheiro\n * renomeado passa. O servidor deve validar o que recebe.\n */\n accept?: string\n disabled?: boolean\n labels: DocumentDropzoneLabels\n /** Pré-visualização própria. Se devolver null/undefined, usa a de omissão (imagem, PDF ou ícone). */\n renderPreview?: (file: File, url: string) => React.ReactNode\n /**\n * Procura QR codes em cada ficheiro escolhido (imagem ou 1.ª página do PDF) e\n * entrega o texto de todos os encontrados (vazio se nenhum). Sem esta prop não\n * se procura nada. Usa a capacidade [QR Scanning](?path=/docs/v1-capabilities-documents-qr-scanning--docs).\n */\n onQRCodes?: (codes: string[], file: File) => void\n /** Chamado com os ficheiros recusados pelo `accept` (quando nenhum dos largados/escolhidos serve). */\n onReject?: (files: File[]) => void\n /** Continuar a procurar até um QR code satisfazer isto (ex: ser o QR de uma fatura). Por omissão, pára no primeiro. */\n qrCodeWanted?: (code: string) => boolean\n className?: string\n}\n\n// Tipos MIME das extensões mais comuns. Ao arrastar, o browser só revela o tipo\n// do ficheiro (não o nome), por isso as regras \".ext\" são traduzidas por aqui.\nconst EXT_TYPES: Record<string, string[]> = {\n '.pdf': ['application/pdf'],\n '.jpg': ['image/jpeg'],\n '.jpeg': ['image/jpeg'],\n '.png': ['image/png'],\n '.webp': ['image/webp'],\n '.gif': ['image/gif'],\n '.heic': ['image/heic'],\n '.svg': ['image/svg+xml'],\n '.doc': ['application/msword'],\n '.docx': ['application/vnd.openxmlformats-officedocument.wordprocessingml.document'],\n '.xls': ['application/vnd.ms-excel'],\n '.xlsx': ['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'],\n '.csv': ['text/csv', 'application/vnd.ms-excel'],\n '.txt': ['text/plain'],\n '.zip': ['application/zip', 'application/x-zip-compressed'],\n}\n\n/**\n * Se um ficheiro deste tipo é permitido, sabendo só o tipo (durante o arrastar).\n * `null` quando não dá para saber (tipo desconhecido, ou extensão fora da tabela).\n */\nexport function acceptsType(type: string, accept?: string): boolean | null {\n const rules = (accept ?? '')\n .split(',')\n .map((s) => s.trim().toLowerCase())\n .filter(Boolean)\n if (rules.length === 0) return true\n const t = type.toLowerCase()\n if (!t) return null\n let unknown = false\n for (const rule of rules) {\n if (rule.startsWith('.')) {\n const types = EXT_TYPES[rule]\n if (!types) unknown = true\n else if (types.includes(t)) return true\n } else if (rule.endsWith('/*')) {\n if (t.startsWith(rule.slice(0, -1))) return true\n } else if (t === rule) return true\n }\n return unknown ? null : false\n}\n\nexport function accepts(file: File, accept?: string): boolean {\n if (!accept) return true\n const name = file.name.toLowerCase()\n const type = file.type.toLowerCase()\n // Regras vazias (ex: vírgula a mais em \".pdf,\") são ignoradas: senão aceitavam\n // qualquer ficheiro cujo tipo o browser não conhece (type === '').\n const rules = accept\n .split(',')\n .map((s) => s.trim().toLowerCase())\n .filter(Boolean)\n if (rules.length === 0) return true\n return rules.some((rule) => {\n if (rule.startsWith('.')) return name.endsWith(rule)\n if (rule.endsWith('/*')) return type.startsWith(rule.slice(0, -1))\n return type === rule\n })\n}\n\nfunction UploadIcon() {\n return (\n <svg width=\"36\" height=\"36\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.9\" aria-hidden>\n <path d=\"M12 15V4M7.5 8.5L12 4l4.5 4.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n <path d=\"M4.5 14v3.5a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2V14\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n </svg>\n )\n}\n\nfunction DocumentIcon({ size }: { size: number }) {\n return (\n <svg width={size} height={size} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.8\" aria-hidden>\n <path d=\"M14 2H7a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7z\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n <path d=\"M14 2v5h5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n </svg>\n )\n}\n\nfunction ImageIcon() {\n return (\n <svg width=\"17\" height=\"17\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.8\" aria-hidden>\n <rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\" />\n <circle cx=\"8.5\" cy=\"8.5\" r=\"1.5\" />\n <path d=\"M21 15l-5-5L4 20\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n </svg>\n )\n}\n\n// Documento com etiqueta do formato (PDF, XLS, CSV). As letras são recortadas\n// da etiqueta (máscara), para lerem bem sobre qualquer fundo, claro ou escuro.\nfunction FileTypeIcon({ label, size = 20 }: { label: string; size?: number }) {\n const id = React.useId()\n return (\n <svg width={size} height={size} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.7\" aria-hidden>\n <mask id={`${id}-doc`} stroke=\"none\">\n <rect width=\"24\" height=\"24\" fill=\"#fff\" />\n <rect x=\"1\" y=\"11\" width=\"17\" height=\"9.5\" rx=\"2\" fill=\"#000\" />\n </mask>\n <mask id={`${id}-label`} stroke=\"none\">\n <rect x=\"2\" y=\"12\" width=\"15\" height=\"7.5\" rx=\"1.6\" fill=\"#fff\" />\n <text x=\"9.5\" y=\"17.9\" textAnchor=\"middle\" fontFamily=\"Arial, Helvetica, sans-serif\" fontWeight=\"700\" fontSize=\"6\" fill=\"#000\">\n {label}\n </text>\n </mask>\n <g mask={`url(#${id}-doc)`}>\n <path d=\"M14 2H7a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7z\" strokeLinejoin=\"round\" />\n <path d=\"M14 2v5h5\" strokeLinejoin=\"round\" />\n </g>\n <rect x=\"2\" y=\"12\" width=\"15\" height=\"7.5\" rx=\"1.6\" fill=\"currentColor\" stroke=\"none\" mask={`url(#${id}-label)`} />\n </svg>\n )\n}\n\nconst spreadsheetLabel = (file: File) => (/.csv$/i.test(file.name) || file.type.toLowerCase() === 'text/csv' ? 'CSV' : 'XLS')\n\nconst isPdf = (file: File) => file.type === 'application/pdf'\n\nconst SPREADSHEET_TYPES = [\n 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',\n 'application/vnd.ms-excel.sheet.macroenabled.12',\n 'application/vnd.ms-excel',\n 'text/csv',\n]\n/** Folhas de cálculo (Excel, CSV): pela extensão ou pelo tipo, que o browser às vezes não conhece. */\nexport const isSpreadsheet = (file: File) => /\\.(xlsx|xlsm|xls|csv)$/i.test(file.name) || SPREADSHEET_TYPES.includes(file.type.toLowerCase())\n\nfunction DefaultPreview({ file, url }: { file: File; url: string }) {\n if (file.type.startsWith('image/')) {\n return <img src={url} alt={file.name} className=\"block max-h-full max-w-full object-contain p-3\" />\n }\n if (isPdf(file)) {\n return <PdfPreview file={file} label={file.name} />\n }\n return (\n <div className=\"flex h-24 w-24 items-center justify-center rounded-3xl bg-primary text-primary-foreground shadow-tile\">\n {isSpreadsheet(file) ? <FileTypeIcon label={spreadsheetLabel(file)} size={42} /> : <DocumentIcon size={42} />}\n </div>\n )\n}\n\n/**\n * Caixa para anexar um ficheiro a um formulário: arrastar ou clicar para\n * escolher. Mostra o nome, o tamanho e uma pré-visualização do ficheiro.\n *\n * **Usa:** [PdfPreview](?path=/docs/v1-primitives-documents-pdfpreview--docs) para mostrar PDFs; imagens com `<img>`.\n * Com `onQRCodes`, a capacidade [QR Scanning](?path=/docs/v1-capabilities-documents-qr-scanning--docs).\n */\nexport default function DocumentDropzone({\n file,\n onFileChange,\n accept,\n disabled = false,\n labels,\n renderPreview,\n onQRCodes,\n qrCodeWanted,\n onReject,\n className = '',\n}: DocumentDropzoneProps) {\n const input = useRef<HTMLInputElement>(null)\n // Durante o arrastar: 'ok' se o ficheiro serve (ou não dá para saber), 'rejected' se não.\n const [drag, setDrag] = useState<'none' | 'ok' | 'rejected'>('none')\n const [url, setUrl] = useState('')\n const [scanning, setScanning] = useState(false)\n const [rejected, setRejected] = useState(false)\n\n // A mensagem de recusa desaparece sozinha passado uns segundos.\n useEffect(() => {\n if (!rejected) return\n const t = window.setTimeout(() => setRejected(false), 5000)\n return () => window.clearTimeout(t)\n }, [rejected])\n\n // Sempre a versão mais recente das funções da app, sem recomeçar a leitura\n // quando a app passa uma função nova em cada render.\n const onQRCodesRef = useRef(onQRCodes)\n const qrCodeWantedRef = useRef(qrCodeWanted)\n onQRCodesRef.current = onQRCodes\n qrCodeWantedRef.current = qrCodeWanted\n const scanQR = Boolean(onQRCodes)\n\n useEffect(() => {\n if (!file || !scanQR) {\n setScanning(false)\n return\n }\n let alive = true\n setScanning(true)\n import('../../../capabilities/documents/QRScanning/scanQRCodes')\n .then(({ scanQRCodes }) => scanQRCodes(file, { until: (code) => qrCodeWantedRef.current?.(code) ?? true }))\n .catch(() => [] as string[])\n .then((codes) => {\n // Um resultado de um ficheiro que entretanto mudou é descartado.\n if (!alive) return\n setScanning(false)\n onQRCodesRef.current?.(codes, file)\n })\n return () => {\n alive = false\n }\n }, [file, scanQR])\n\n useEffect(() => {\n if (!file) {\n setUrl('')\n return\n }\n const created = URL.createObjectURL(file)\n setUrl(created)\n return () => URL.revokeObjectURL(created)\n }, [file])\n\n const pick = () => {\n if (!disabled) input.current?.click()\n }\n\n const receive = (list: FileList | null) => {\n if (disabled || !list || list.length === 0) return\n const files = Array.from(list)\n const first = files.find((f) => accepts(f, accept))\n if (first) {\n setRejected(false)\n onFileChange(first)\n return\n }\n setRejected(true)\n onReject?.(files)\n }\n\n const onKeyDown = (e: React.KeyboardEvent) => {\n if (e.key !== 'Enter' && e.key !== ' ') return\n e.preventDefault()\n pick()\n }\n\n let frame = 'border-2 border-dashed border-primary/30 bg-card shadow-card hover:border-primary hover:bg-muted hover:shadow-card-hover'\n if (file) frame = 'border border-border bg-card shadow-card'\n if (drag === 'ok') frame = 'border-2 border-dashed border-primary bg-primary/5 shadow-card-active'\n if (drag === 'rejected') frame = 'border-2 border-dashed border-destructive bg-destructive/5 shadow-card'\n if (disabled) frame += ' cursor-not-allowed opacity-60'\n\n const sizeMb = file ? (file.size / 1024 / 1024).toFixed(2) : ''\n const custom = file && url && renderPreview ? renderPreview(file, url) : null\n const preview = file && url ? custom ?? <DefaultPreview file={file} url={url} /> : null\n\n return (\n <div\n onDragOver={(e) => {\n e.preventDefault()\n if (disabled) {\n e.dataTransfer.dropEffect = 'none'\n return\n }\n const types = Array.from(e.dataTransfer.items ?? [])\n .filter((item) => item.kind === 'file')\n .map((item) => item.type)\n const verdict = types.length && types.every((t) => acceptsType(t, accept) === false) ? 'rejected' : 'ok'\n e.dataTransfer.dropEffect = verdict === 'rejected' ? 'none' : 'copy'\n setDrag(verdict)\n }}\n onDragLeave={(e) => {\n // Passar por cima do ícone ou do texto da caixa não é sair dela.\n if (e.currentTarget.contains(e.relatedTarget as Node | null)) return\n // Ficheiro não permitido: a mensagem fica uns segundos depois de largar/sair.\n if (drag === 'rejected') setRejected(true)\n setDrag('none')\n }}\n onDrop={(e) => {\n e.preventDefault()\n setDrag('none')\n receive(e.dataTransfer.files)\n }}\n className={`group relative flex overflow-hidden rounded-2xl transition-[border-color,background-color,box-shadow] duration-200 ${frame} ${className}`}\n >\n {!file && (\n <div\n role=\"button\"\n tabIndex={disabled ? -1 : 0}\n aria-disabled={disabled}\n onClick={pick}\n onKeyDown={onKeyDown}\n className=\"flex h-full w-full cursor-pointer flex-col items-center justify-center gap-[18px] px-6 py-7 text-center outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n >\n <div className=\"flex h-[78px] w-[78px] items-center justify-center rounded-[22px] bg-primary/5 text-primary transition-transform duration-200 group-hover:-translate-y-0.5\">\n <UploadIcon />\n </div>\n <div>\n <div className=\"mb-1.5 text-title font-semibold text-foreground\">{labels.title}</div>\n {labels.hint && <div className=\"text-control text-muted-foreground\">{labels.hint}</div>}\n </div>\n </div>\n )}\n\n {file && (\n <div className=\"flex h-full min-h-0 w-full flex-col\">\n <div className=\"flex flex-none items-center gap-[11px] border-b border-border bg-card py-2.5 pr-3 pl-3.5\">\n <span className=\"flex h-[30px] w-[30px] flex-none items-center justify-center rounded-lg bg-primary/5 text-primary\">\n {isPdf(file) ? (\n <FileTypeIcon label=\"PDF\" />\n ) : isSpreadsheet(file) ? (\n <FileTypeIcon label={spreadsheetLabel(file)} />\n ) : file.type.startsWith('image/') ? (\n <ImageIcon />\n ) : (\n <DocumentIcon size={17} />\n )}\n </span>\n <div className=\"min-w-0 flex-1 text-left\">\n <div className=\"truncate text-control font-semibold text-primary\">{file.name}</div>\n <div className=\"mt-px flex items-center gap-1.5 text-caption text-muted-foreground\">\n <span>{sizeMb} MB</span>\n {scanning && (\n <span role=\"status\" className=\"inline-flex items-center gap-1.5\">\n <span aria-hidden className=\"inline-block h-2.5 w-2.5 animate-spin rounded-full border-[1.5px] border-primary/20 border-t-primary\" />\n {labels.scanningQR}\n </span>\n )}\n </div>\n </div>\n <button\n type=\"button\"\n disabled={disabled}\n onClick={pick}\n className=\"inline-flex flex-none cursor-pointer items-center gap-1.5 rounded-lg border border-border bg-card px-[11px] py-1.5 text-caption font-bold tracking-[0.02em] text-primary transition-colors duration-150 hover:border-primary/30 hover:bg-primary/5 focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none\"\n >\n <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.2\" aria-hidden>\n <path d=\"M21 12a9 9 0 1 1-2.64-6.36\" strokeLinecap=\"round\" />\n <path d=\"M21 3v5h-5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n </svg>\n {labels.change}\n </button>\n <button\n type=\"button\"\n title={labels.remove}\n aria-label={labels.remove}\n disabled={disabled}\n onClick={() => onFileChange(null)}\n className=\"inline-flex h-8 w-8 flex-none cursor-pointer items-center justify-center rounded-lg border border-border bg-card p-0 text-muted-foreground transition-colors duration-150 hover:border-destructive/30 hover:bg-destructive/10 hover:text-destructive\"\n >\n <svg width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.9\" aria-hidden>\n <path d=\"M4 7h16M9 7V5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2M6 7l1 13a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1l1-13\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n </svg>\n </button>\n </div>\n <div className=\"flex min-h-0 flex-1 basis-0 items-center justify-center overflow-hidden bg-card\">\n {preview}\n </div>\n </div>\n )}\n\n {(rejected || drag === 'rejected') && (\n <div\n role=\"alert\"\n className=\"absolute inset-x-3 bottom-3 rounded-lg border border-destructive/30 bg-card px-3 py-2 text-center text-xs font-medium text-destructive shadow-popover\"\n >\n {labels.rejected}\n </div>\n )}\n\n <input\n ref={input}\n type=\"file\"\n accept={accept}\n className=\"hidden\"\n onChange={(e) => {\n receive(e.target.files)\n e.target.value = ''\n }}\n />\n </div>\n )\n}\n"],"names":[],"mappings":";;;AAkDA,MAAM,YAAsC;AAAA,EAC1C,QAAQ,CAAC,iBAAiB;AAAA,EAC1B,QAAQ,CAAC,YAAY;AAAA,EACrB,SAAS,CAAC,YAAY;AAAA,EACtB,QAAQ,CAAC,WAAW;AAAA,EACpB,SAAS,CAAC,YAAY;AAAA,EACtB,QAAQ,CAAC,WAAW;AAAA,EACpB,SAAS,CAAC,YAAY;AAAA,EACtB,QAAQ,CAAC,eAAe;AAAA,EACxB,QAAQ,CAAC,oBAAoB;AAAA,EAC7B,SAAS,CAAC,yEAAyE;AAAA,EACnF,QAAQ,CAAC,0BAA0B;AAAA,EACnC,SAAS,CAAC,mEAAmE;AAAA,EAC7E,QAAQ,CAAC,YAAY,0BAA0B;AAAA,EAC/C,QAAQ,CAAC,YAAY;AAAA,EACrB,QAAQ,CAAC,mBAAmB,8BAA8B;AAC5D;AAMO,SAAS,YAAY,MAAc,QAAiC;AACzE,QAAM,SAAS,UAAU,IACtB,MAAM,GAAG,EACT,IAAI,CAAC,MAAM,EAAE,OAAO,YAAA,CAAa,EACjC,OAAO,OAAO;AACjB,MAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,QAAM,IAAI,KAAK,YAAA;AACf,MAAI,CAAC,EAAG,QAAO;AACf,MAAI,UAAU;AACd,aAAW,QAAQ,OAAO;AACxB,QAAI,KAAK,WAAW,GAAG,GAAG;AACxB,YAAM,QAAQ,UAAU,IAAI;AAC5B,UAAI,CAAC,MAAO,WAAU;AAAA,eACb,MAAM,SAAS,CAAC,EAAG,QAAO;AAAA,IACrC,WAAW,KAAK,SAAS,IAAI,GAAG;AAC9B,UAAI,EAAE,WAAW,KAAK,MAAM,GAAG,EAAE,CAAC,EAAG,QAAO;AAAA,IAC9C,WAAW,MAAM,KAAM,QAAO;AAAA,EAChC;AACA,SAAO,UAAU,OAAO;AAC1B;AAEO,SAAS,QAAQ,MAAY,QAA0B;AAC5D,MAAI,CAAC,OAAQ,QAAO;AACpB,QAAM,OAAO,KAAK,KAAK,YAAA;AACvB,QAAM,OAAO,KAAK,KAAK,YAAA;AAGvB,QAAM,QAAQ,OACX,MAAM,GAAG,EACT,IAAI,CAAC,MAAM,EAAE,OAAO,YAAA,CAAa,EACjC,OAAO,OAAO;AACjB,MAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,SAAO,MAAM,KAAK,CAAC,SAAS;AAC1B,QAAI,KAAK,WAAW,GAAG,EAAG,QAAO,KAAK,SAAS,IAAI;AACnD,QAAI,KAAK,SAAS,IAAI,EAAG,QAAO,KAAK,WAAW,KAAK,MAAM,GAAG,EAAE,CAAC;AACjE,WAAO,SAAS;AAAA,EAClB,CAAC;AACH;AAEA,SAAS,aAAa;AACpB,SACE,qBAAC,OAAA,EAAI,OAAM,MAAK,QAAO,MAAK,SAAQ,aAAY,MAAK,QAAO,QAAO,gBAAe,aAAY,OAAM,eAAW,MAC7G,UAAA;AAAA,IAAA,oBAAC,UAAK,GAAE,iCAAgC,eAAc,SAAQ,gBAAe,SAAQ;AAAA,wBACpF,QAAA,EAAK,GAAE,iDAAgD,eAAc,SAAQ,gBAAe,QAAA,CAAQ;AAAA,EAAA,GACvG;AAEJ;AAEA,SAAS,aAAa,EAAE,QAA0B;AAChD,SACE,qBAAC,OAAA,EAAI,OAAO,MAAM,QAAQ,MAAM,SAAQ,aAAY,MAAK,QAAO,QAAO,gBAAe,aAAY,OAAM,eAAW,MACjH,UAAA;AAAA,IAAA,oBAAC,UAAK,GAAE,8DAA6D,eAAc,SAAQ,gBAAe,SAAQ;AAAA,wBACjH,QAAA,EAAK,GAAE,aAAY,eAAc,SAAQ,gBAAe,QAAA,CAAQ;AAAA,EAAA,GACnE;AAEJ;AAEA,SAAS,YAAY;AACnB,SACE,qBAAC,OAAA,EAAI,OAAM,MAAK,QAAO,MAAK,SAAQ,aAAY,MAAK,QAAO,QAAO,gBAAe,aAAY,OAAM,eAAW,MAC7G,UAAA;AAAA,IAAA,oBAAC,QAAA,EAAK,GAAE,KAAI,GAAE,KAAI,OAAM,MAAK,QAAO,MAAK,IAAG,IAAA,CAAI;AAAA,wBAC/C,UAAA,EAAO,IAAG,OAAM,IAAG,OAAM,GAAE,OAAM;AAAA,wBACjC,QAAA,EAAK,GAAE,oBAAmB,eAAc,SAAQ,gBAAe,QAAA,CAAQ;AAAA,EAAA,GAC1E;AAEJ;AAIA,SAAS,aAAa,EAAE,OAAO,OAAO,MAAwC;AAC5E,QAAM,KAAK,MAAM,MAAA;AACjB,SACE,qBAAC,OAAA,EAAI,OAAO,MAAM,QAAQ,MAAM,SAAQ,aAAY,MAAK,QAAO,QAAO,gBAAe,aAAY,OAAM,eAAW,MACjH,UAAA;AAAA,IAAA,qBAAC,UAAK,IAAI,GAAG,EAAE,QAAQ,QAAO,QAC5B,UAAA;AAAA,MAAA,oBAAC,UAAK,OAAM,MAAK,QAAO,MAAK,MAAK,QAAO;AAAA,MACzC,oBAAC,QAAA,EAAK,GAAE,KAAI,GAAE,MAAK,OAAM,MAAK,QAAO,OAAM,IAAG,KAAI,MAAK,OAAA,CAAO;AAAA,IAAA,GAChE;AAAA,yBACC,QAAA,EAAK,IAAI,GAAG,EAAE,UAAU,QAAO,QAC9B,UAAA;AAAA,MAAA,oBAAC,QAAA,EAAK,GAAE,KAAI,GAAE,MAAK,OAAM,MAAK,QAAO,OAAM,IAAG,OAAM,MAAK,QAAO;AAAA,0BAC/D,QAAA,EAAK,GAAE,OAAM,GAAE,QAAO,YAAW,UAAS,YAAW,gCAA+B,YAAW,OAAM,UAAS,KAAI,MAAK,QACrH,UAAA,MAAA,CACH;AAAA,IAAA,GACF;AAAA,IACA,qBAAC,KAAA,EAAE,MAAM,QAAQ,EAAE,SACjB,UAAA;AAAA,MAAA,oBAAC,QAAA,EAAK,GAAE,8DAA6D,gBAAe,SAAQ;AAAA,MAC5F,oBAAC,QAAA,EAAK,GAAE,aAAY,gBAAe,QAAA,CAAQ;AAAA,IAAA,GAC7C;AAAA,wBACC,QAAA,EAAK,GAAE,KAAI,GAAE,MAAK,OAAM,MAAK,QAAO,OAAM,IAAG,OAAM,MAAK,gBAAe,QAAO,QAAO,MAAM,QAAQ,EAAE,UAAA,CAAW;AAAA,EAAA,GACnH;AAEJ;AAEA,MAAM,mBAAmB,CAAC,SAAgB,SAAS,KAAK,KAAK,IAAI,KAAK,KAAK,KAAK,YAAA,MAAkB,aAAa,QAAQ;AAEvH,MAAM,QAAQ,CAAC,SAAe,KAAK,SAAS;AAE5C,MAAM,oBAAoB;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEO,MAAM,gBAAgB,CAAC,SAAe,0BAA0B,KAAK,KAAK,IAAI,KAAK,kBAAkB,SAAS,KAAK,KAAK,aAAa;AAE5I,SAAS,eAAe,EAAE,MAAM,OAAoC;AAClE,MAAI,KAAK,KAAK,WAAW,QAAQ,GAAG;AAClC,WAAO,oBAAC,SAAI,KAAK,KAAK,KAAK,KAAK,MAAM,WAAU,kDAAiD;AAAA,EACnG;AACA,MAAI,MAAM,IAAI,GAAG;AACf,WAAO,oBAAC,YAAA,EAAW,MAAY,OAAO,KAAK,MAAM;AAAA,EACnD;AACA,SACE,oBAAC,SAAI,WAAU,yGACZ,wBAAc,IAAI,wBAAK,cAAA,EAAa,OAAO,iBAAiB,IAAI,GAAG,MAAM,IAAI,wBAAM,cAAA,EAAa,MAAM,IAAI,EAAA,CAC7G;AAEJ;AASA,SAAwB,iBAAiB;AAAA,EACvC;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AACd,GAA0B;AACxB,QAAM,QAAQ,OAAyB,IAAI;AAE3C,QAAM,CAAC,MAAM,OAAO,IAAI,SAAqC,MAAM;AACnE,QAAM,CAAC,KAAK,MAAM,IAAI,SAAS,EAAE;AACjC,QAAM,CAAC,UAAU,WAAW,IAAI,SAAS,KAAK;AAC9C,QAAM,CAAC,UAAU,WAAW,IAAI,SAAS,KAAK;AAG9C,YAAU,MAAM;AACd,QAAI,CAAC,SAAU;AACf,UAAM,IAAI,OAAO,WAAW,MAAM,YAAY,KAAK,GAAG,GAAI;AAC1D,WAAO,MAAM,OAAO,aAAa,CAAC;AAAA,EACpC,GAAG,CAAC,QAAQ,CAAC;AAIb,QAAM,eAAe,OAAO,SAAS;AACrC,QAAM,kBAAkB,OAAO,YAAY;AAC3C,eAAa,UAAU;AACvB,kBAAgB,UAAU;AAC1B,QAAM,SAAS,QAAQ,SAAS;AAEhC,YAAU,MAAM;AACd,QAAI,CAAC,QAAQ,CAAC,QAAQ;AACpB,kBAAY,KAAK;AACjB;AAAA,IACF;AACA,QAAI,QAAQ;AACZ,gBAAY,IAAI;AAChB,WAAO,2DAAwD,EAC5D,KAAK,CAAC,EAAE,kBAAkB,YAAY,MAAM,EAAE,OAAO,CAAC,SAAS,gBAAgB,UAAU,IAAI,KAAK,KAAA,CAAM,CAAC,EACzG,MAAM,MAAM,EAAc,EAC1B,KAAK,CAAC,UAAU;AAEf,UAAI,CAAC,MAAO;AACZ,kBAAY,KAAK;AACjB,mBAAa,UAAU,OAAO,IAAI;AAAA,IACpC,CAAC;AACH,WAAO,MAAM;AACX,cAAQ;AAAA,IACV;AAAA,EACF,GAAG,CAAC,MAAM,MAAM,CAAC;AAEjB,YAAU,MAAM;AACd,QAAI,CAAC,MAAM;AACT,aAAO,EAAE;AACT;AAAA,IACF;AACA,UAAM,UAAU,IAAI,gBAAgB,IAAI;AACxC,WAAO,OAAO;AACd,WAAO,MAAM,IAAI,gBAAgB,OAAO;AAAA,EAC1C,GAAG,CAAC,IAAI,CAAC;AAET,QAAM,OAAO,MAAM;AACjB,QAAI,CAAC,SAAU,OAAM,SAAS,MAAA;AAAA,EAChC;AAEA,QAAM,UAAU,CAAC,SAA0B;AACzC,QAAI,YAAY,CAAC,QAAQ,KAAK,WAAW,EAAG;AAC5C,UAAM,QAAQ,MAAM,KAAK,IAAI;AAC7B,UAAM,QAAQ,MAAM,KAAK,CAAC,MAAM,QAAQ,GAAG,MAAM,CAAC;AAClD,QAAI,OAAO;AACT,kBAAY,KAAK;AACjB,mBAAa,KAAK;AAClB;AAAA,IACF;AACA,gBAAY,IAAI;AAChB,eAAW,KAAK;AAAA,EAClB;AAEA,QAAM,YAAY,CAAC,MAA2B;AAC5C,QAAI,EAAE,QAAQ,WAAW,EAAE,QAAQ,IAAK;AACxC,MAAE,eAAA;AACF,SAAA;AAAA,EACF;AAEA,MAAI,QAAQ;AACZ,MAAI,KAAM,SAAQ;AAClB,MAAI,SAAS,KAAM,SAAQ;AAC3B,MAAI,SAAS,WAAY,SAAQ;AACjC,MAAI,SAAU,UAAS;AAEvB,QAAM,SAAS,QAAQ,KAAK,OAAO,OAAO,MAAM,QAAQ,CAAC,IAAI;AAC7D,QAAM,SAAS,QAAQ,OAAO,gBAAgB,cAAc,MAAM,GAAG,IAAI;AACzE,QAAM,UAAU,QAAQ,MAAM,8BAAW,gBAAA,EAAe,MAAY,KAAU,IAAK;AAEnF,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,YAAY,CAAC,MAAM;AACjB,UAAE,eAAA;AACF,YAAI,UAAU;AACZ,YAAE,aAAa,aAAa;AAC5B;AAAA,QACF;AACA,cAAM,QAAQ,MAAM,KAAK,EAAE,aAAa,SAAS,CAAA,CAAE,EAChD,OAAO,CAAC,SAAS,KAAK,SAAS,MAAM,EACrC,IAAI,CAAC,SAAS,KAAK,IAAI;AAC1B,cAAM,UAAU,MAAM,UAAU,MAAM,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,MAAM,KAAK,IAAI,aAAa;AACpG,UAAE,aAAa,aAAa,YAAY,aAAa,SAAS;AAC9D,gBAAQ,OAAO;AAAA,MACjB;AAAA,MACA,aAAa,CAAC,MAAM;AAElB,YAAI,EAAE,cAAc,SAAS,EAAE,aAA4B,EAAG;AAE9D,YAAI,SAAS,WAAY,aAAY,IAAI;AACzC,gBAAQ,MAAM;AAAA,MAChB;AAAA,MACA,QAAQ,CAAC,MAAM;AACb,UAAE,eAAA;AACF,gBAAQ,MAAM;AACd,gBAAQ,EAAE,aAAa,KAAK;AAAA,MAC9B;AAAA,MACA,WAAW,sHAAsH,KAAK,IAAI,SAAS;AAAA,MAElJ,UAAA;AAAA,QAAA,CAAC,QACA;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,MAAK;AAAA,YACL,UAAU,WAAW,KAAK;AAAA,YAC1B,iBAAe;AAAA,YACf,SAAS;AAAA,YACT;AAAA,YACA,WAAU;AAAA,YAEV,UAAA;AAAA,cAAA,oBAAC,OAAA,EAAI,WAAU,8JACb,UAAA,oBAAC,cAAW,GACd;AAAA,mCACC,OAAA,EACC,UAAA;AAAA,gBAAA,oBAAC,OAAA,EAAI,WAAU,mDAAmD,UAAA,OAAO,OAAM;AAAA,gBAC9E,OAAO,QAAQ,oBAAC,SAAI,WAAU,sCAAsC,iBAAO,KAAA,CAAK;AAAA,cAAA,EAAA,CACnF;AAAA,YAAA;AAAA,UAAA;AAAA,QAAA;AAAA,QAIH,QACC,qBAAC,OAAA,EAAI,WAAU,uCACb,UAAA;AAAA,UAAA,qBAAC,OAAA,EAAI,WAAU,4FACb,UAAA;AAAA,YAAA,oBAAC,QAAA,EAAK,WAAU,qGACb,UAAA,MAAM,IAAI,IACT,oBAAC,cAAA,EAAa,OAAM,MAAA,CAAM,IACxB,cAAc,IAAI,IACpB,oBAAC,cAAA,EAAa,OAAO,iBAAiB,IAAI,EAAA,CAAG,IAC3C,KAAK,KAAK,WAAW,QAAQ,IAC/B,oBAAC,aAAU,IAEX,oBAAC,cAAA,EAAa,MAAM,IAAI,GAE5B;AAAA,YACA,qBAAC,OAAA,EAAI,WAAU,4BACb,UAAA;AAAA,cAAA,oBAAC,OAAA,EAAI,WAAU,oDAAoD,UAAA,KAAK,MAAK;AAAA,cAC7E,qBAAC,OAAA,EAAI,WAAU,sEACb,UAAA;AAAA,gBAAA,qBAAC,QAAA,EAAM,UAAA;AAAA,kBAAA;AAAA,kBAAO;AAAA,gBAAA,GAAG;AAAA,gBAChB,YACC,qBAAC,QAAA,EAAK,MAAK,UAAS,WAAU,oCAC5B,UAAA;AAAA,kBAAA,oBAAC,QAAA,EAAK,eAAW,MAAC,WAAU,wGAAuG;AAAA,kBAClI,OAAO;AAAA,gBAAA,EAAA,CACV;AAAA,cAAA,EAAA,CAEJ;AAAA,YAAA,GACF;AAAA,YACA;AAAA,cAAC;AAAA,cAAA;AAAA,gBACC,MAAK;AAAA,gBACL;AAAA,gBACA,SAAS;AAAA,gBACT,WAAU;AAAA,gBAEV,UAAA;AAAA,kBAAA,qBAAC,OAAA,EAAI,OAAM,MAAK,QAAO,MAAK,SAAQ,aAAY,MAAK,QAAO,QAAO,gBAAe,aAAY,OAAM,eAAW,MAC7G,UAAA;AAAA,oBAAA,oBAAC,QAAA,EAAK,GAAE,8BAA6B,eAAc,SAAQ;AAAA,wCAC1D,QAAA,EAAK,GAAE,cAAa,eAAc,SAAQ,gBAAe,QAAA,CAAQ;AAAA,kBAAA,GACpE;AAAA,kBACC,OAAO;AAAA,gBAAA;AAAA,cAAA;AAAA,YAAA;AAAA,YAEV;AAAA,cAAC;AAAA,cAAA;AAAA,gBACC,MAAK;AAAA,gBACL,OAAO,OAAO;AAAA,gBACd,cAAY,OAAO;AAAA,gBACnB;AAAA,gBACA,SAAS,MAAM,aAAa,IAAI;AAAA,gBAChC,WAAU;AAAA,gBAEV,UAAA,oBAAC,OAAA,EAAI,OAAM,MAAK,QAAO,MAAK,SAAQ,aAAY,MAAK,QAAO,QAAO,gBAAe,aAAY,OAAM,eAAW,MAC7G,UAAA,oBAAC,QAAA,EAAK,GAAE,6FAA4F,eAAc,SAAQ,gBAAe,QAAA,CAAQ,EAAA,CACnJ;AAAA,cAAA;AAAA,YAAA;AAAA,UACF,GACF;AAAA,UACA,oBAAC,OAAA,EAAI,WAAU,mFACZ,UAAA,QAAA,CACH;AAAA,QAAA,GACF;AAAA,SAGA,YAAY,SAAS,eACrB;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,MAAK;AAAA,YACL,WAAU;AAAA,YAET,UAAA,OAAO;AAAA,UAAA;AAAA,QAAA;AAAA,QAIZ;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,KAAK;AAAA,YACL,MAAK;AAAA,YACL;AAAA,YACA,WAAU;AAAA,YACV,UAAU,CAAC,MAAM;AACf,sBAAQ,EAAE,OAAO,KAAK;AACtB,gBAAE,OAAO,QAAQ;AAAA,YACnB;AAAA,UAAA;AAAA,QAAA;AAAA,MACF;AAAA,IAAA;AAAA,EAAA;AAGN;"}
1
+ {"version":3,"file":"DocumentDropzone.js","sources":["../../../../../../src/v1/components/documents/DocumentDropzone/DocumentDropzone.tsx"],"sourcesContent":["import React, { useEffect, useRef, useState } from 'react'\r\nimport PdfPreview from '../../../primitives/documents/PdfPreview/PdfPreview'\r\n\r\nexport interface DocumentDropzoneLabels {\r\n title: string\r\n hint?: string\r\n change: string\r\n remove: string\r\n /** Texto enquanto procura QR codes (só com `onQRCodes`), ex: \"A procurar QR codes…\". */\r\n scanningQR?: string\r\n /**\r\n * Mensagem quando um ficheiro não é permitido pelo `accept`, ex: \"Formato não\r\n * permitido. Aceita PDF, JPG ou PNG.\" Aparece logo ao arrastar e depois de largar.\r\n */\r\n rejected: string\r\n}\r\n\r\nexport interface DocumentDropzoneProps {\r\n file: File | null\r\n onFileChange: (file: File | null) => void\r\n /**\r\n * Tipos permitidos: extensões e/ou tipos MIME, separados por vírgulas, ex:\r\n * \".pdf,.jpg,.jpeg,.png\". Os restantes são recusados (ver `labels.rejected` e\r\n * `onReject`). Sem `accept`, aceita qualquer ficheiro.\r\n *\r\n * - `image/*` inclui SVG (que pode conter código) e HEIC (sem pré-visualização\r\n * na maioria dos browsers). Para documentos, prefira uma lista explícita.\r\n * - Só se verificam o nome e o tipo declarado, não o conteúdo: um ficheiro\r\n * renomeado passa. O servidor deve validar o que recebe.\r\n */\r\n accept?: string\r\n disabled?: boolean\r\n labels: DocumentDropzoneLabels\r\n /** Pré-visualização própria. Se devolver null/undefined, usa a de omissão (imagem, PDF ou ícone). */\r\n renderPreview?: (file: File, url: string) => React.ReactNode\r\n /**\r\n * Procura QR codes em cada ficheiro escolhido (imagem ou 1.ª página do PDF) e\r\n * entrega o texto de todos os encontrados (vazio se nenhum). Sem esta prop não\r\n * se procura nada. Usa a capacidade [QR Scanning](?path=/docs/v1-capabilities-documents-qr-scanning--docs).\r\n */\r\n onQRCodes?: (codes: string[], file: File) => void\r\n /** Chamado com os ficheiros recusados pelo `accept` (quando nenhum dos largados/escolhidos serve). */\r\n onReject?: (files: File[]) => void\r\n /** Continuar a procurar até um QR code satisfazer isto (ex: ser o QR de uma fatura). Por omissão, pára no primeiro. */\r\n qrCodeWanted?: (code: string) => boolean\r\n className?: string\r\n}\r\n\r\n// Tipos MIME das extensões mais comuns. Ao arrastar, o browser só revela o tipo\r\n// do ficheiro (não o nome), por isso as regras \".ext\" são traduzidas por aqui.\r\nconst EXT_TYPES: Record<string, string[]> = {\r\n '.pdf': ['application/pdf'],\r\n '.jpg': ['image/jpeg'],\r\n '.jpeg': ['image/jpeg'],\r\n '.png': ['image/png'],\r\n '.webp': ['image/webp'],\r\n '.gif': ['image/gif'],\r\n '.heic': ['image/heic'],\r\n '.svg': ['image/svg+xml'],\r\n '.doc': ['application/msword'],\r\n '.docx': ['application/vnd.openxmlformats-officedocument.wordprocessingml.document'],\r\n '.xls': ['application/vnd.ms-excel'],\r\n '.xlsx': ['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'],\r\n '.csv': ['text/csv', 'application/vnd.ms-excel'],\r\n '.txt': ['text/plain'],\r\n '.zip': ['application/zip', 'application/x-zip-compressed'],\r\n}\r\n\r\n/**\r\n * Se um ficheiro deste tipo é permitido, sabendo só o tipo (durante o arrastar).\r\n * `null` quando não dá para saber (tipo desconhecido, ou extensão fora da tabela).\r\n */\r\nexport function acceptsType(type: string, accept?: string): boolean | null {\r\n const rules = (accept ?? '')\r\n .split(',')\r\n .map((s) => s.trim().toLowerCase())\r\n .filter(Boolean)\r\n if (rules.length === 0) return true\r\n const t = type.toLowerCase()\r\n if (!t) return null\r\n let unknown = false\r\n for (const rule of rules) {\r\n if (rule.startsWith('.')) {\r\n const types = EXT_TYPES[rule]\r\n if (!types) unknown = true\r\n else if (types.includes(t)) return true\r\n } else if (rule.endsWith('/*')) {\r\n if (t.startsWith(rule.slice(0, -1))) return true\r\n } else if (t === rule) return true\r\n }\r\n return unknown ? null : false\r\n}\r\n\r\nexport function accepts(file: File, accept?: string): boolean {\r\n if (!accept) return true\r\n const name = file.name.toLowerCase()\r\n const type = file.type.toLowerCase()\r\n // Regras vazias (ex: vírgula a mais em \".pdf,\") são ignoradas: senão aceitavam\r\n // qualquer ficheiro cujo tipo o browser não conhece (type === '').\r\n const rules = accept\r\n .split(',')\r\n .map((s) => s.trim().toLowerCase())\r\n .filter(Boolean)\r\n if (rules.length === 0) return true\r\n return rules.some((rule) => {\r\n if (rule.startsWith('.')) return name.endsWith(rule)\r\n if (rule.endsWith('/*')) return type.startsWith(rule.slice(0, -1))\r\n return type === rule\r\n })\r\n}\r\n\r\nfunction UploadIcon() {\r\n return (\r\n <svg width=\"36\" height=\"36\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.9\" aria-hidden>\r\n <path d=\"M12 15V4M7.5 8.5L12 4l4.5 4.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\r\n <path d=\"M4.5 14v3.5a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2V14\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\r\n </svg>\r\n )\r\n}\r\n\r\nfunction DocumentIcon({ size }: { size: number }) {\r\n return (\r\n <svg width={size} height={size} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.8\" aria-hidden>\r\n <path d=\"M14 2H7a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7z\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\r\n <path d=\"M14 2v5h5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\r\n </svg>\r\n )\r\n}\r\n\r\nfunction ImageIcon() {\r\n return (\r\n <svg width=\"17\" height=\"17\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.8\" aria-hidden>\r\n <rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\" />\r\n <circle cx=\"8.5\" cy=\"8.5\" r=\"1.5\" />\r\n <path d=\"M21 15l-5-5L4 20\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\r\n </svg>\r\n )\r\n}\r\n\r\n// Documento com etiqueta do formato (PDF, XLS, CSV). As letras são recortadas\r\n// da etiqueta (máscara), para lerem bem sobre qualquer fundo, claro ou escuro.\r\nfunction FileTypeIcon({ label, size = 20 }: { label: string; size?: number }) {\r\n const id = React.useId()\r\n return (\r\n <svg width={size} height={size} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.7\" aria-hidden>\r\n <mask id={`${id}-doc`} stroke=\"none\">\r\n <rect width=\"24\" height=\"24\" fill=\"#fff\" />\r\n <rect x=\"1\" y=\"11\" width=\"17\" height=\"9.5\" rx=\"2\" fill=\"#000\" />\r\n </mask>\r\n <mask id={`${id}-label`} stroke=\"none\">\r\n <rect x=\"2\" y=\"12\" width=\"15\" height=\"7.5\" rx=\"1.6\" fill=\"#fff\" />\r\n <text x=\"9.5\" y=\"17.9\" textAnchor=\"middle\" fontFamily=\"Arial, Helvetica, sans-serif\" fontWeight=\"700\" fontSize=\"6\" fill=\"#000\">\r\n {label}\r\n </text>\r\n </mask>\r\n <g mask={`url(#${id}-doc)`}>\r\n <path d=\"M14 2H7a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7z\" strokeLinejoin=\"round\" />\r\n <path d=\"M14 2v5h5\" strokeLinejoin=\"round\" />\r\n </g>\r\n <rect x=\"2\" y=\"12\" width=\"15\" height=\"7.5\" rx=\"1.6\" fill=\"currentColor\" stroke=\"none\" mask={`url(#${id}-label)`} />\r\n </svg>\r\n )\r\n}\r\n\r\nconst spreadsheetLabel = (file: File) => (/.csv$/i.test(file.name) || file.type.toLowerCase() === 'text/csv' ? 'CSV' : 'XLS')\r\n\r\nconst isPdf = (file: File) => file.type === 'application/pdf'\r\n\r\nconst SPREADSHEET_TYPES = [\r\n 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',\r\n 'application/vnd.ms-excel.sheet.macroenabled.12',\r\n 'application/vnd.ms-excel',\r\n 'text/csv',\r\n]\r\n/** Folhas de cálculo (Excel, CSV): pela extensão ou pelo tipo, que o browser às vezes não conhece. */\r\nexport const isSpreadsheet = (file: File) => /\\.(xlsx|xlsm|xls|csv)$/i.test(file.name) || SPREADSHEET_TYPES.includes(file.type.toLowerCase())\r\n\r\nfunction DefaultPreview({ file, url }: { file: File; url: string }) {\r\n if (file.type.startsWith('image/')) {\r\n return <img src={url} alt={file.name} className=\"block max-h-full max-w-full object-contain p-3\" />\r\n }\r\n if (isPdf(file)) {\r\n return <PdfPreview file={file} label={file.name} />\r\n }\r\n return (\r\n <div className=\"flex h-24 w-24 items-center justify-center rounded-3xl bg-primary text-primary-foreground shadow-tile\">\r\n {isSpreadsheet(file) ? <FileTypeIcon label={spreadsheetLabel(file)} size={42} /> : <DocumentIcon size={42} />}\r\n </div>\r\n )\r\n}\r\n\r\n/**\r\n * Caixa para anexar um ficheiro a um formulário: arrastar ou clicar para\r\n * escolher. Mostra o nome, o tamanho e uma pré-visualização do ficheiro.\r\n *\r\n * **Usa:** [PdfPreview](?path=/docs/v1-primitives-documents-pdfpreview--docs) para mostrar PDFs; imagens com `<img>`.\r\n * Com `onQRCodes`, a capacidade [QR Scanning](?path=/docs/v1-capabilities-documents-qr-scanning--docs).\r\n */\r\nexport default function DocumentDropzone({\r\n file,\r\n onFileChange,\r\n accept,\r\n disabled = false,\r\n labels,\r\n renderPreview,\r\n onQRCodes,\r\n qrCodeWanted,\r\n onReject,\r\n className = '',\r\n}: DocumentDropzoneProps) {\r\n const input = useRef<HTMLInputElement>(null)\r\n // Durante o arrastar: 'ok' se o ficheiro serve (ou não dá para saber), 'rejected' se não.\r\n const [drag, setDrag] = useState<'none' | 'ok' | 'rejected'>('none')\r\n const [url, setUrl] = useState('')\r\n const [scanning, setScanning] = useState(false)\r\n const [rejected, setRejected] = useState(false)\r\n\r\n // A mensagem de recusa desaparece sozinha passado uns segundos.\r\n useEffect(() => {\r\n if (!rejected) return\r\n const t = window.setTimeout(() => setRejected(false), 5000)\r\n return () => window.clearTimeout(t)\r\n }, [rejected])\r\n\r\n // Sempre a versão mais recente das funções da app, sem recomeçar a leitura\r\n // quando a app passa uma função nova em cada render.\r\n const onQRCodesRef = useRef(onQRCodes)\r\n const qrCodeWantedRef = useRef(qrCodeWanted)\r\n onQRCodesRef.current = onQRCodes\r\n qrCodeWantedRef.current = qrCodeWanted\r\n const scanQR = Boolean(onQRCodes)\r\n\r\n useEffect(() => {\r\n if (!file || !scanQR) {\r\n setScanning(false)\r\n return\r\n }\r\n let alive = true\r\n setScanning(true)\r\n import('../../../capabilities/documents/QRScanning/scanQRCodes')\r\n .then(({ scanQRCodes }) => scanQRCodes(file, { until: (code) => qrCodeWantedRef.current?.(code) ?? true }))\r\n .catch(() => [] as string[])\r\n .then((codes) => {\r\n // Um resultado de um ficheiro que entretanto mudou é descartado.\r\n if (!alive) return\r\n setScanning(false)\r\n onQRCodesRef.current?.(codes, file)\r\n })\r\n return () => {\r\n alive = false\r\n }\r\n }, [file, scanQR])\r\n\r\n useEffect(() => {\r\n if (!file) {\r\n setUrl('')\r\n return\r\n }\r\n const created = URL.createObjectURL(file)\r\n setUrl(created)\r\n return () => URL.revokeObjectURL(created)\r\n }, [file])\r\n\r\n const pick = () => {\r\n if (!disabled) input.current?.click()\r\n }\r\n\r\n const receive = (list: FileList | null) => {\r\n if (disabled || !list || list.length === 0) return\r\n const files = Array.from(list)\r\n const first = files.find((f) => accepts(f, accept))\r\n if (first) {\r\n setRejected(false)\r\n onFileChange(first)\r\n return\r\n }\r\n setRejected(true)\r\n onReject?.(files)\r\n }\r\n\r\n const onKeyDown = (e: React.KeyboardEvent) => {\r\n if (e.key !== 'Enter' && e.key !== ' ') return\r\n e.preventDefault()\r\n pick()\r\n }\r\n\r\n let frame = 'border-2 border-dashed border-primary/30 bg-card shadow-card hover:border-primary hover:bg-muted hover:shadow-card-hover'\r\n if (file) frame = 'border border-border bg-card shadow-card'\r\n if (drag === 'ok') frame = 'border-2 border-dashed border-primary bg-primary/5 shadow-card-active'\r\n if (drag === 'rejected') frame = 'border-2 border-dashed border-destructive bg-destructive/5 shadow-card'\r\n if (disabled) frame += ' cursor-not-allowed opacity-60'\r\n\r\n const sizeMb = file ? (file.size / 1024 / 1024).toFixed(2) : ''\r\n const custom = file && url && renderPreview ? renderPreview(file, url) : null\r\n const preview = file && url ? custom ?? <DefaultPreview file={file} url={url} /> : null\r\n\r\n return (\r\n <div\r\n onDragOver={(e) => {\r\n e.preventDefault()\r\n if (disabled) {\r\n e.dataTransfer.dropEffect = 'none'\r\n return\r\n }\r\n const types = Array.from(e.dataTransfer.items ?? [])\r\n .filter((item) => item.kind === 'file')\r\n .map((item) => item.type)\r\n const verdict = types.length && types.every((t) => acceptsType(t, accept) === false) ? 'rejected' : 'ok'\r\n e.dataTransfer.dropEffect = verdict === 'rejected' ? 'none' : 'copy'\r\n setDrag(verdict)\r\n }}\r\n onDragLeave={(e) => {\r\n // Passar por cima do ícone ou do texto da caixa não é sair dela.\r\n if (e.currentTarget.contains(e.relatedTarget as Node | null)) return\r\n // Ficheiro não permitido: a mensagem fica uns segundos depois de largar/sair.\r\n if (drag === 'rejected') setRejected(true)\r\n setDrag('none')\r\n }}\r\n onDrop={(e) => {\r\n e.preventDefault()\r\n setDrag('none')\r\n receive(e.dataTransfer.files)\r\n }}\r\n className={`group relative flex overflow-hidden rounded-2xl transition-[border-color,background-color,box-shadow] duration-200 ${frame} ${className}`}\r\n >\r\n {!file && (\r\n <div\r\n role=\"button\"\r\n tabIndex={disabled ? -1 : 0}\r\n aria-disabled={disabled}\r\n onClick={pick}\r\n onKeyDown={onKeyDown}\r\n className=\"flex h-full w-full cursor-pointer flex-col items-center justify-center gap-[18px] px-6 py-7 text-center outline-none focus-visible:ring-2 focus-visible:ring-ring\"\r\n >\r\n <div className=\"flex h-[78px] w-[78px] items-center justify-center rounded-[22px] bg-primary/5 text-primary transition-transform duration-200 group-hover:-translate-y-0.5\">\r\n <UploadIcon />\r\n </div>\r\n <div>\r\n <div className=\"mb-1.5 text-title font-semibold text-foreground\">{labels.title}</div>\r\n {labels.hint && <div className=\"text-control text-muted-foreground\">{labels.hint}</div>}\r\n </div>\r\n </div>\r\n )}\r\n\r\n {file && (\r\n <div className=\"flex h-full min-h-0 w-full flex-col\">\r\n <div className=\"flex flex-none items-center gap-[11px] border-b border-border bg-card py-2.5 pr-3 pl-3.5\">\r\n <span className=\"flex h-[30px] w-[30px] flex-none items-center justify-center rounded-lg bg-primary/5 text-primary\">\r\n {isPdf(file) ? (\r\n <FileTypeIcon label=\"PDF\" />\r\n ) : isSpreadsheet(file) ? (\r\n <FileTypeIcon label={spreadsheetLabel(file)} />\r\n ) : file.type.startsWith('image/') ? (\r\n <ImageIcon />\r\n ) : (\r\n <DocumentIcon size={17} />\r\n )}\r\n </span>\r\n <div className=\"min-w-0 flex-1 text-left\">\r\n <div className=\"truncate text-control font-semibold text-primary\">{file.name}</div>\r\n <div className=\"mt-px flex items-center gap-1.5 text-caption text-muted-foreground\">\r\n <span>{sizeMb} MB</span>\r\n {scanning && (\r\n <span role=\"status\" className=\"inline-flex items-center gap-1.5\">\r\n <span aria-hidden className=\"inline-block h-2.5 w-2.5 animate-spin rounded-full border-[1.5px] border-primary/20 border-t-primary\" />\r\n {labels.scanningQR}\r\n </span>\r\n )}\r\n </div>\r\n </div>\r\n <button\r\n type=\"button\"\r\n disabled={disabled}\r\n onClick={pick}\r\n className=\"inline-flex flex-none cursor-pointer items-center gap-1.5 rounded-lg border border-border bg-card px-[11px] py-1.5 text-caption font-bold tracking-[0.02em] text-primary transition-colors duration-150 hover:border-primary/30 hover:bg-primary/5 focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none\"\r\n >\r\n <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.2\" aria-hidden>\r\n <path d=\"M21 12a9 9 0 1 1-2.64-6.36\" strokeLinecap=\"round\" />\r\n <path d=\"M21 3v5h-5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\r\n </svg>\r\n {labels.change}\r\n </button>\r\n <button\r\n type=\"button\"\r\n title={labels.remove}\r\n aria-label={labels.remove}\r\n disabled={disabled}\r\n onClick={() => onFileChange(null)}\r\n className=\"inline-flex h-8 w-8 flex-none cursor-pointer items-center justify-center rounded-lg border border-border bg-card p-0 text-muted-foreground transition-colors duration-150 hover:border-destructive/30 hover:bg-destructive/10 hover:text-destructive\"\r\n >\r\n <svg width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.9\" aria-hidden>\r\n <path d=\"M4 7h16M9 7V5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2M6 7l1 13a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1l1-13\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\r\n </svg>\r\n </button>\r\n </div>\r\n <div className=\"flex min-h-0 flex-1 basis-0 items-center justify-center overflow-hidden bg-card\">\r\n {preview}\r\n </div>\r\n </div>\r\n )}\r\n\r\n {(rejected || drag === 'rejected') && (\r\n <div\r\n role=\"alert\"\r\n className=\"absolute inset-x-3 bottom-3 rounded-lg border border-destructive/30 bg-card px-3 py-2 text-center text-xs font-medium text-destructive shadow-popover\"\r\n >\r\n {labels.rejected}\r\n </div>\r\n )}\r\n\r\n <input\r\n ref={input}\r\n type=\"file\"\r\n accept={accept}\r\n className=\"hidden\"\r\n onChange={(e) => {\r\n receive(e.target.files)\r\n e.target.value = ''\r\n }}\r\n />\r\n </div>\r\n )\r\n}\r\n"],"names":[],"mappings":"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"}
@@ -166,7 +166,7 @@ function ListFinder({
166
166
  )
167
167
  ] })
168
168
  ] }),
169
- marcaveis && visiveis.length > 0 && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 border-b border-slate-100 bg-slate-50 px-3 py-1.5 text-[10px] text-slate-500", children: [
169
+ marcaveis && visiveis.length > 0 && /* @__PURE__ */ jsxs("div", { className: `flex items-center gap-1.5 bg-white px-3 py-2 text-[10px] text-slate-600 ${LINHA_ESBATIDA}`, children: [
170
170
  /* @__PURE__ */ jsx("span", { className: "flex w-7 shrink-0 justify-center", children: /* @__PURE__ */ jsx(
171
171
  Checkbox,
172
172
  {
@@ -1 +1 @@
1
- {"version":3,"file":"ListFinder.js","sources":["../../../../../../src/v1/components/lists/ListFinder/ListFinder.tsx"],"sourcesContent":["import React, { useEffect, useMemo, useState } from 'react'\r\nimport { ChevronLeftIcon, SearchIcon } from '../../../foundations/icons/icons'\r\nimport ListItem, { LINHA_ESBATIDA, type ListItemContent } from '../../../primitives/lists/ListItem/ListItem'\r\nimport Checkbox from '../../../primitives/forms/Checkbox/Checkbox'\r\nimport { OverlayScrollbar } from '../../tables/Table/OverlayScrollbar'\r\nimport { DEFAULT_LIST_FINDER_LABELS, type ListFinderLabels } from './labels'\r\n\r\nexport interface ListFinderProps<T> {\r\n /** As linhas: todas, ou só a página atual quando o servidor pagina (com `total`). */\r\n items: T[]\r\n /** Uma chave única por linha (ex: o id). */\r\n itemKey: (item: T) => string\r\n /** O que cada linha mostra: as partes do [ListItem](?path=/docs/v1-primitives-lists-listitem--docs). */\r\n itemContent: (item: T) => ListItemContent\r\n /** A linha aberta (fica realçada). */\r\n activeKey?: string | null\r\n /** Ao clicar numa linha: normalmente, abri-la ao lado. */\r\n onItemClick?: (item: T) => void\r\n /** As marcadas. Só há caixas de marcar com `onSelectionChange`. */\r\n selectedKeys?: ReadonlySet<string>\r\n onSelectionChange?: (keys: Set<string>) => void\r\n /** Filtros ao lado do voltar, por cima da pesquisa (ex: um ou dois selects, no traço fino da pesquisa). */\r\n filters?: React.ReactNode\r\n /** Com isto aparece um ‹ à esquerda, para fechar ou esconder a lista. */\r\n onBack?: () => void\r\n /** Com a caixa de pesquisa (o normal). Sem ela, a lista mostra tudo o que recebe. */\r\n searchable?: boolean\r\n /** O texto da pesquisa. Sem ele, a pesquisa guarda o seu. */\r\n search?: string\r\n onSearchChange?: (search: string) => void\r\n /**\r\n * O texto onde a pesquisa procura, em cada linha. Com isto, a lista filtra-se\r\n * sozinha (sem acentos nem maiúsculas); sem isto, quem filtra é a app (ex: o\r\n * servidor, com `onSearchChange`). Uma lista colada (uma por linha, ou\r\n * separadas por vírgulas) mostra as que tiverem qualquer uma delas.\r\n */\r\n searchText?: (item: T) => string\r\n /**\r\n * Como se chega ao resto da lista:\r\n * - `'pages'` (o normal): páginas numeradas no rodapé;\r\n * - `'scroll'`: uma lista só, que vai mostrando mais `pageSize` linhas ao\r\n * chegar ao fundo. Com o servidor a paginar, `items` são as que já vieram e\r\n * pede-se o lote seguinte com `onLoadMore`.\r\n */\r\n mode?: 'pages' | 'scroll'\r\n /** Com `mode=\"scroll\"` e o servidor a paginar: buscar o lote seguinte (junta-o a `items`). */\r\n onLoadMore?: () => void\r\n /** O servidor pagina: quantas linhas há lá ao todo. `items` é só a página atual (ou, a rolar, as que já vieram). */\r\n total?: number\r\n /** A página atual (1, 2, ...), quando o servidor pagina. */\r\n page?: number\r\n onPageChange?: (page: number) => void\r\n /** Quantas linhas por página. Com o servidor a paginar, quantas ele manda. */\r\n pageSize?: number\r\n /** A carregar: sem linhas ainda, mostra o esqueleto delas. A rolar, com linhas, é o lote seguinte a chegar. */\r\n loading?: boolean\r\n labels?: Partial<ListFinderLabels>\r\n /** No rodapé, a seguir à contagem: algo pequeno da app (ex: um ícone de ajuda com as regras da lista). */\r\n footerExtra?: React.ReactNode\r\n /** Com a moldura à volta (o normal). Sem ela, para ir dentro de um painel que já tem a sua. */\r\n bordered?: boolean\r\n className?: string\r\n}\r\n\r\nconst semAcentos = (s: string) => s.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase()\r\n\r\n/**\r\n * Parte o texto da pesquisa nos termos a procurar: vírgula, ponto e vírgula,\r\n * tab e mudança de linha. O espaço só separa quando tudo o que lá está são\r\n * números de 6 ou mais dígitos (uma coluna colada de uma folha de cálculo);\r\n * fora disso \"vila real\" é uma coisa só, e não \"vila\" ou \"real\".\r\n */\r\nexport function termosDaPesquisa(texto: string): string[] {\r\n const porEspacos = texto.trim().split(/\\s+/).filter(Boolean)\r\n if (porEspacos.length > 1 && porEspacos.every(t => /^\\d{6,}$/.test(t))) return porEspacos\r\n return texto.split(/[\\n,;\\t]+/).map(t => t.trim()).filter(Boolean)\r\n}\r\n\r\n/** Os números das páginas numa coluna estreita: no máximo cinco lugares. */\r\nfunction numerosPagina(atual: number, total: number): (number | '…')[] {\r\n if (total <= 5) return Array.from({ length: total }, (_, i) => i + 1)\r\n if (atual <= 2) return [1, 2, 3, '…', total]\r\n if (atual >= total - 1) return [1, '…', total - 2, total - 1, total]\r\n return [1, '…', atual, '…', total]\r\n}\r\n\r\n/** Uma linha a carregar: a forma das linhas já diz que vem ali a lista. */\r\nfunction LinhaEsqueleto({ marcavel, largura }: { marcavel: boolean; largura: string }) {\r\n return (\r\n <div className={`flex items-start gap-1.5 px-3 py-3 animate-pulse motion-reduce:animate-none ${LINHA_ESBATIDA}`}>\r\n {marcavel && <span className=\"flex w-7 shrink-0 justify-center\"><span className=\"mt-0.5 h-4 w-4 border border-slate-200 bg-slate-100\" /></span>}\r\n <div className=\"min-w-0 flex-1 space-y-1.5\">\r\n <div className=\"h-3 w-20 bg-slate-200\" />\r\n <div className={`h-2.5 ${largura} bg-slate-200`} />\r\n <div className=\"h-1.5 w-1/3 bg-slate-100\" />\r\n </div>\r\n </div>\r\n )\r\n}\r\n\r\n/**\r\n * Uma lista para encontrar e escolher: filtros e pesquisa em cima, as linhas\r\n * ([ListItem](?path=/docs/v1-primitives-lists-listitem--docs)) a rolar no meio e\r\n * as páginas (ou a contagem, a rolar) em baixo. Pensada para a coluna ao lado do que se abre (a lista à\r\n * esquerda, o detalhe da linha escolhida à direita).\r\n *\r\n * Enche a altura do pai. O topo, a barra de \"selecionar todos\" e o rodapé\r\n * ficam sempre no sítio: só as linhas rolam.\r\n *\r\n * - **Pesquisa**: com `searchText` filtra sozinha; sem ele, a app filtra\r\n * (`search`/`onSearchChange`).\r\n * - **Páginas**: pagina sozinha (`pageSize`), ou é o servidor (`total`, `page`,\r\n * `onPageChange`).\r\n * - **A rolar** (`mode=\"scroll\"`): sem páginas, vai mostrando mais ao descer;\r\n * com o servidor, `items` são as que já vieram e `onLoadMore` pede mais.\r\n * - **Realce**: o que se procurou realça-se no código e no título das linhas.\r\n * - **Marcar vários**: com `onSelectionChange`. O \"selecionar todos\" só marca\r\n * as linhas à vista, nunca as que não se veem.\r\n */\r\nexport default function ListFinder<T>({\r\n items, itemKey, itemContent, activeKey, onItemClick, selectedKeys, onSelectionChange, filters, onBack,\r\n searchable = true, search: pesquisaDeFora, onSearchChange, searchText, mode = 'pages', onLoadMore, total, page, onPageChange, pageSize = 50, loading = false,\r\n labels, footerExtra, bordered = true, className = '',\r\n}: ListFinderProps<T>) {\r\n const L = { ...DEFAULT_LIST_FINDER_LABELS, ...labels }\r\n const [pesquisaPropria, setPesquisaPropria] = useState('')\r\n const pesquisa = searchable ? (pesquisaDeFora ?? pesquisaPropria) : ''\r\n const pesquisar = (s: string) => { setPesquisaPropria(s); onSearchChange?.(s) }\r\n // Uma caixa de uma linha deita fora as quebras de linha: uma coluna colada\r\n // de uma folha de cálculo chegava como um número só, colado. Passam a vírgulas.\r\n const colar = (e: React.ClipboardEvent<HTMLInputElement>) => {\r\n const texto = e.clipboardData.getData('text')\r\n if (!/\\r?\\n/.test(texto.trim())) return\r\n e.preventDefault()\r\n const lista = texto.split(/\\r?\\n/).map(t => t.trim()).filter(Boolean).join(', ')\r\n const el = e.currentTarget\r\n pesquisar(el.value.slice(0, el.selectionStart ?? el.value.length) + lista + el.value.slice(el.selectionEnd ?? el.value.length))\r\n }\r\n\r\n // Os mesmos termos filtram (com `searchText`) e realçam-se nas linhas.\r\n const termos = useMemo(() => termosDaPesquisa(pesquisa).map(semAcentos), [pesquisa])\r\n const filtradas = useMemo(() => {\r\n if (!searchText || !termos.length) return items\r\n return items.filter(it => {\r\n const texto = semAcentos(searchText(it))\r\n return termos.some(t => texto.includes(t))\r\n })\r\n }, [items, termos, searchText])\r\n\r\n // Paginação: do servidor (com `total`), ou feita aqui.\r\n const peloServidor = total !== undefined\r\n const aRolar = mode === 'scroll'\r\n const [paginaPropria, setPaginaPropria] = useState(1)\r\n const totalLinhas = peloServidor ? total : filtradas.length\r\n const totalPaginas = Math.max(1, Math.ceil(totalLinhas / pageSize))\r\n const pagina = Math.min(peloServidor ? (page ?? 1) : paginaPropria, totalPaginas)\r\n // Uma pesquisa nova volta à primeira página: a terceira de antes já não é a mesma lista.\r\n useEffect(() => { if (!peloServidor) setPaginaPropria(1) }, [pesquisa, peloServidor])\r\n const irPara = (p: number) => { if (peloServidor) onPageChange?.(p); else setPaginaPropria(p) }\r\n // A rolar sem servidor: quantos lotes já se mostram.\r\n const [lotes, setLotes] = useState(1)\r\n useEffect(() => { setLotes(1) }, [pesquisa])\r\n const visiveis = peloServidor ? filtradas\r\n : aRolar ? filtradas.slice(0, lotes * pageSize)\r\n : filtradas.slice((pagina - 1) * pageSize, pagina * pageSize)\r\n const haMais = aRolar && (peloServidor ? !!onLoadMore && items.length < totalLinhas : visiveis.length < filtradas.length)\r\n\r\n // Ao mudar de página, ou de pesquisa a rolar, a lista volta ao topo.\r\n const [caixa, setCaixa] = useState<HTMLDivElement | null>(null)\r\n useEffect(() => { caixa?.scrollTo?.({ top: 0 }) }, [pagina, caixa])\r\n useEffect(() => { if (aRolar) caixa?.scrollTo?.({ top: 0 }) }, [pesquisa, aRolar, caixa])\r\n\r\n // A rolar: a linha-sentinela no fundo, ao chegar a 300px da vista, pede o\r\n // lote seguinte. Volta a observar a cada lote que chega: se continuar à\r\n // vista (uma janela alta), pede logo outro.\r\n const [fim, setFim] = useState<HTMLDivElement | null>(null)\r\n useEffect(() => {\r\n if (!fim || !caixa || !haMais || typeof IntersectionObserver === 'undefined') return\r\n const io = new IntersectionObserver(([e]) => {\r\n if (!e?.isIntersecting) return\r\n if (peloServidor) { if (!loading) onLoadMore?.() } else setLotes(n => n + 1)\r\n }, { root: caixa, rootMargin: '300px' })\r\n io.observe(fim)\r\n return () => io.disconnect()\r\n }, [fim, caixa, haMais, peloServidor, loading, onLoadMore, visiveis.length])\r\n\r\n const marcadas = selectedKeys ?? new Set<string>()\r\n const marcaveis = !!onSelectionChange\r\n const nMarcadasVisiveis = visiveis.filter(it => marcadas.has(itemKey(it))).length\r\n const todasMarcadas = visiveis.length > 0 && nMarcadasVisiveis === visiveis.length\r\n const marcar = (key: string, sim: boolean) => {\r\n const n = new Set(marcadas)\r\n if (sim) n.add(key); else n.delete(key)\r\n onSelectionChange?.(n)\r\n }\r\n const marcarVisiveis = () => {\r\n const n = new Set(marcadas)\r\n for (const it of visiveis) { if (todasMarcadas) n.delete(itemKey(it)); else n.add(itemKey(it)) }\r\n onSelectionChange?.(n)\r\n }\r\n\r\n const aCarregar = loading && visiveis.length === 0\r\n const esqueleto = <span className=\"inline-block h-2 w-20 rounded-sm bg-slate-200 animate-pulse motion-reduce:animate-none\" aria-hidden />\r\n\r\n return (\r\n <div className={`v1-reset flex h-full min-h-0 flex-col bg-white text-control text-slate-800 ${bordered ? 'border border-slate-200' : ''} ${className}`}>\r\n {/* Sem pesquisa, filtros nem voltar, não há cabeça: as linhas começam no topo. */}\r\n {(searchable || onBack || filters) && (\r\n <div className=\"space-y-2 border-b border-slate-100 px-3 py-2\">\r\n {(onBack || filters) && (\r\n <div className=\"flex items-center gap-1.5\">\r\n {onBack && (\r\n <button type=\"button\" onClick={onBack} aria-label={L.back} title={L.back}\r\n className=\"-ml-1 flex h-7 w-6 shrink-0 items-center justify-center rounded text-slate-400 transition-colors hover:bg-slate-100 hover:text-slate-700\">\r\n <ChevronLeftIcon size={14} />\r\n </button>\r\n )}\r\n <div className=\"flex min-w-0 flex-1 items-center gap-1.5\">{filters}</div>\r\n </div>\r\n )}\r\n {searchable && (\r\n <div className=\"relative\">\r\n {/* Caixa direita e fina, no tamanho do texto das linhas: a lista é\r\n uma coluna estreita, e a pesquisa não deve pesar mais do que ela. */}\r\n <SearchIcon size={11} className=\"pointer-events-none absolute top-1/2 left-2 -translate-y-1/2 text-slate-400\" />\r\n <input\r\n type=\"search\"\r\n value={pesquisa}\r\n onChange={e => pesquisar(e.target.value)}\r\n onPaste={colar}\r\n placeholder={L.searchPlaceholder}\r\n aria-label={L.search}\r\n className=\"block w-full min-w-0 border border-slate-200 bg-white py-1.5 pr-2 pl-6 text-micro text-slate-700 outline-none transition-colors placeholder:text-slate-400 focus:border-proef-blue\"\r\n />\r\n </div>\r\n )}\r\n </div>\r\n )}\r\n\r\n {/* Fora da área que rola: é a cabeça da lista, e é a descer a lista que\r\n se decide marcá-la toda. */}\r\n {marcaveis && visiveis.length > 0 && (\r\n <div className=\"flex items-center gap-1.5 border-b border-slate-100 bg-slate-50 px-3 py-1.5 text-[10px] text-slate-500\">\r\n {/* Na coluna de 28px das linhas, para as caixas ficarem umas por baixo das outras. */}\r\n <span className=\"flex w-7 shrink-0 justify-center\">\r\n <Checkbox\r\n checked={todasMarcadas}\r\n indeterminate={nMarcadasVisiveis > 0 && !todasMarcadas}\r\n onChange={marcarVisiveis}\r\n aria-label={L.selectVisible(visiveis.length)}\r\n />\r\n </span>\r\n <span aria-hidden>{nMarcadasVisiveis > 0 ? L.selectedCount(nMarcadasVisiveis, visiveis.length) : L.selectVisible(visiveis.length)}</span>\r\n {marcadas.size > 0 && (\r\n <button type=\"button\" onClick={() => onSelectionChange?.(new Set())}\r\n className=\"ml-auto text-proef-blue hover:underline\">\r\n {L.clearSelection}\r\n </button>\r\n )}\r\n </div>\r\n )}\r\n\r\n <div className=\"relative flex min-h-0 flex-1 flex-col\">\r\n {/* `relative`: o texto só para leitores de ecrã (`sr-only`, posição\r\n absoluta) do fundo da lista tem de ficar preso a esta caixa. Sem isso\r\n ficava no fim das linhas, fora do corte do scroll, e esticava a\r\n página — dava para rolar até um branco sem nada. */}\r\n <div ref={setCaixa} className=\"relative min-h-0 flex-1 overflow-y-auto scrollbar-none\">\r\n {aCarregar ? (\r\n // A forma das linhas já diz que vem ali a lista. Mais do que cabem,\r\n // cortadas pela caixa: chega ao fundo em qualquer altura.\r\n <div role=\"status\" aria-busy=\"true\" className=\"h-full overflow-hidden\">\r\n <span className=\"sr-only\">{L.loading}</span>\r\n {['w-11/12', 'w-3/4', 'w-5/6', 'w-2/3', 'w-4/5', 'w-7/12'].flatMap(l => [l, l, l, l]).map((largura, i) => (\r\n <LinhaEsqueleto key={i} marcavel={marcaveis} largura={largura} />\r\n ))}\r\n </div>\r\n ) : visiveis.length === 0 ? (\r\n <p className=\"m-0 px-3 py-4 text-center text-[10px] text-slate-400\">{L.empty}</p>\r\n ) : (\r\n <ul className=\"m-0 list-none p-0\">\r\n {visiveis.map(it => {\r\n const key = itemKey(it)\r\n return (\r\n <ListItem\r\n key={key}\r\n {...itemContent(it)}\r\n active={activeKey === key}\r\n onClick={onItemClick ? () => onItemClick(it) : undefined}\r\n checked={marcadas.has(key)}\r\n onCheckedChange={marcaveis ? sim => marcar(key, sim) : undefined}\r\n checkboxLabel={L.checkItem(key)}\r\n highlight={termos}\r\n />\r\n )\r\n })}\r\n </ul>\r\n )}\r\n {/* A rolar: o lote seguinte a chegar, e a sentinela que o pede. */}\r\n {!aCarregar && haMais && (\r\n <div ref={setFim} role=\"status\" aria-busy=\"true\">\r\n <span className=\"sr-only\">{L.loading}</span>\r\n {['w-3/4', 'w-5/6', 'w-2/3'].map((largura, i) => <LinhaEsqueleto key={i} marcavel={marcaveis} largura={largura} />)}\r\n </div>\r\n )}\r\n </div>\r\n <OverlayScrollbar alvo={caixa} borda={0} />\r\n </div>\r\n\r\n {/* Sempre à vista e com a mesma altura, carregada ou não: aparecer do\r\n nada fazia saltar o fundo do painel. */}\r\n <div className={`flex shrink-0 items-center gap-2 border-t border-slate-100 bg-slate-50 px-3 text-micro text-slate-400 ${aRolar ? 'h-8 justify-center' : 'h-9 justify-between'}`}>\r\n <span className=\"flex items-center gap-1 whitespace-nowrap\">\r\n {aCarregar ? esqueleto : aRolar ? (\r\n // A rolar: quantas já se mostram, de quantas há.\r\n <><span className=\"font-medium text-slate-500 tabular-nums\">{visiveis.length.toLocaleString('pt-PT')}</span> / <span className=\"font-medium text-slate-500 tabular-nums\">{totalLinhas.toLocaleString('pt-PT')}</span> {L.itemsNoun(totalLinhas)}</>\r\n ) : <><span className=\"font-medium text-slate-500 tabular-nums\">{totalLinhas.toLocaleString('pt-PT')}</span> {L.itemsNoun(totalLinhas)}</>}\r\n {footerExtra}\r\n {/* Com linhas e a pedir outra vez (ex: outra página, ou a atualizar): um \"…\" a pulsar. */}\r\n {loading && !aCarregar && <span aria-hidden className=\"animate-pulse motion-reduce:animate-none\">…</span>}\r\n </span>\r\n {!aRolar && !aCarregar && totalPaginas > 1 && (\r\n <nav aria-label={L.pagination} className=\"flex items-center gap-0.5\">\r\n <span className=\"sr-only\">{L.page} {pagina} / {totalPaginas}</span>\r\n {numerosPagina(pagina, totalPaginas).map((p, i) => p === '…' ? (\r\n <span key={`r${i}`} aria-hidden className=\"flex h-6 min-w-3 items-center justify-center text-slate-300\">…</span>\r\n ) : (\r\n <button\r\n key={p}\r\n type=\"button\"\r\n onClick={() => irPara(p)}\r\n aria-current={p === pagina ? 'page' : undefined}\r\n className={`grid h-6 min-w-5 place-items-center px-1 tabular-nums transition-colors ${\r\n p === pagina ? 'cursor-default font-semibold text-slate-800' : 'text-slate-400 hover:text-slate-700'}`}\r\n >\r\n {/* Sem caixa nem fundo: a página atual é só o número mais forte.\r\n A cópia invisível a negrito guarda a largura, para os números\r\n não se mexerem ao trocar de página. */}\r\n <span aria-hidden className=\"invisible col-start-1 row-start-1 font-semibold\">{p.toLocaleString('pt-PT')}</span>\r\n <span className=\"col-start-1 row-start-1\">{p.toLocaleString('pt-PT')}</span>\r\n </button>\r\n ))}\r\n </nav>\r\n )}\r\n </div>\r\n </div>\r\n )\r\n}\r\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ListFinder.js","sources":["../../../../../../src/v1/components/lists/ListFinder/ListFinder.tsx"],"sourcesContent":["import React, { useEffect, useMemo, useState } from 'react'\r\nimport { ChevronLeftIcon, SearchIcon } from '../../../foundations/icons/icons'\r\nimport ListItem, { LINHA_ESBATIDA, type ListItemContent } from '../../../primitives/lists/ListItem/ListItem'\r\nimport Checkbox from '../../../primitives/forms/Checkbox/Checkbox'\r\nimport { OverlayScrollbar } from '../../tables/Table/OverlayScrollbar'\r\nimport { DEFAULT_LIST_FINDER_LABELS, type ListFinderLabels } from './labels'\r\n\r\nexport interface ListFinderProps<T> {\r\n /** As linhas: todas, ou só a página atual quando o servidor pagina (com `total`). */\r\n items: T[]\r\n /** Uma chave única por linha (ex: o id). */\r\n itemKey: (item: T) => string\r\n /** O que cada linha mostra: as partes do [ListItem](?path=/docs/v1-primitives-lists-listitem--docs). */\r\n itemContent: (item: T) => ListItemContent\r\n /** A linha aberta (fica realçada). */\r\n activeKey?: string | null\r\n /** Ao clicar numa linha: normalmente, abri-la ao lado. */\r\n onItemClick?: (item: T) => void\r\n /** As marcadas. Só há caixas de marcar com `onSelectionChange`. */\r\n selectedKeys?: ReadonlySet<string>\r\n onSelectionChange?: (keys: Set<string>) => void\r\n /** Filtros ao lado do voltar, por cima da pesquisa (ex: um ou dois selects, no traço fino da pesquisa). */\r\n filters?: React.ReactNode\r\n /** Com isto aparece um ‹ à esquerda, para fechar ou esconder a lista. */\r\n onBack?: () => void\r\n /** Com a caixa de pesquisa (o normal). Sem ela, a lista mostra tudo o que recebe. */\r\n searchable?: boolean\r\n /** O texto da pesquisa. Sem ele, a pesquisa guarda o seu. */\r\n search?: string\r\n onSearchChange?: (search: string) => void\r\n /**\r\n * O texto onde a pesquisa procura, em cada linha. Com isto, a lista filtra-se\r\n * sozinha (sem acentos nem maiúsculas); sem isto, quem filtra é a app (ex: o\r\n * servidor, com `onSearchChange`). Uma lista colada (uma por linha, ou\r\n * separadas por vírgulas) mostra as que tiverem qualquer uma delas.\r\n */\r\n searchText?: (item: T) => string\r\n /**\r\n * Como se chega ao resto da lista:\r\n * - `'pages'` (o normal): páginas numeradas no rodapé;\r\n * - `'scroll'`: uma lista só, que vai mostrando mais `pageSize` linhas ao\r\n * chegar ao fundo. Com o servidor a paginar, `items` são as que já vieram e\r\n * pede-se o lote seguinte com `onLoadMore`.\r\n */\r\n mode?: 'pages' | 'scroll'\r\n /** Com `mode=\"scroll\"` e o servidor a paginar: buscar o lote seguinte (junta-o a `items`). */\r\n onLoadMore?: () => void\r\n /** O servidor pagina: quantas linhas há lá ao todo. `items` é só a página atual (ou, a rolar, as que já vieram). */\r\n total?: number\r\n /** A página atual (1, 2, ...), quando o servidor pagina. */\r\n page?: number\r\n onPageChange?: (page: number) => void\r\n /** Quantas linhas por página. Com o servidor a paginar, quantas ele manda. */\r\n pageSize?: number\r\n /** A carregar: sem linhas ainda, mostra o esqueleto delas. A rolar, com linhas, é o lote seguinte a chegar. */\r\n loading?: boolean\r\n labels?: Partial<ListFinderLabels>\r\n /** No rodapé, a seguir à contagem: algo pequeno da app (ex: um ícone de ajuda com as regras da lista). */\r\n footerExtra?: React.ReactNode\r\n /** Com a moldura à volta (o normal). Sem ela, para ir dentro de um painel que já tem a sua. */\r\n bordered?: boolean\r\n className?: string\r\n}\r\n\r\nconst semAcentos = (s: string) => s.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase()\r\n\r\n/**\r\n * Parte o texto da pesquisa nos termos a procurar: vírgula, ponto e vírgula,\r\n * tab e mudança de linha. O espaço só separa quando tudo o que lá está são\r\n * números de 6 ou mais dígitos (uma coluna colada de uma folha de cálculo);\r\n * fora disso \"vila real\" é uma coisa só, e não \"vila\" ou \"real\".\r\n */\r\nexport function termosDaPesquisa(texto: string): string[] {\r\n const porEspacos = texto.trim().split(/\\s+/).filter(Boolean)\r\n if (porEspacos.length > 1 && porEspacos.every(t => /^\\d{6,}$/.test(t))) return porEspacos\r\n return texto.split(/[\\n,;\\t]+/).map(t => t.trim()).filter(Boolean)\r\n}\r\n\r\n/** Os números das páginas numa coluna estreita: no máximo cinco lugares. */\r\nfunction numerosPagina(atual: number, total: number): (number | '…')[] {\r\n if (total <= 5) return Array.from({ length: total }, (_, i) => i + 1)\r\n if (atual <= 2) return [1, 2, 3, '…', total]\r\n if (atual >= total - 1) return [1, '…', total - 2, total - 1, total]\r\n return [1, '…', atual, '…', total]\r\n}\r\n\r\n/** Uma linha a carregar: a forma das linhas já diz que vem ali a lista. */\r\nfunction LinhaEsqueleto({ marcavel, largura }: { marcavel: boolean; largura: string }) {\r\n return (\r\n <div className={`flex items-start gap-1.5 px-3 py-3 animate-pulse motion-reduce:animate-none ${LINHA_ESBATIDA}`}>\r\n {marcavel && <span className=\"flex w-7 shrink-0 justify-center\"><span className=\"mt-0.5 h-4 w-4 border border-slate-200 bg-slate-100\" /></span>}\r\n <div className=\"min-w-0 flex-1 space-y-1.5\">\r\n <div className=\"h-3 w-20 bg-slate-200\" />\r\n <div className={`h-2.5 ${largura} bg-slate-200`} />\r\n <div className=\"h-1.5 w-1/3 bg-slate-100\" />\r\n </div>\r\n </div>\r\n )\r\n}\r\n\r\n/**\r\n * Uma lista para encontrar e escolher: filtros e pesquisa em cima, as linhas\r\n * ([ListItem](?path=/docs/v1-primitives-lists-listitem--docs)) a rolar no meio e\r\n * as páginas (ou a contagem, a rolar) em baixo. Pensada para a coluna ao lado do que se abre (a lista à\r\n * esquerda, o detalhe da linha escolhida à direita).\r\n *\r\n * Enche a altura do pai. O topo, a barra de \"selecionar todos\" e o rodapé\r\n * ficam sempre no sítio: só as linhas rolam.\r\n *\r\n * - **Pesquisa**: com `searchText` filtra sozinha; sem ele, a app filtra\r\n * (`search`/`onSearchChange`).\r\n * - **Páginas**: pagina sozinha (`pageSize`), ou é o servidor (`total`, `page`,\r\n * `onPageChange`).\r\n * - **A rolar** (`mode=\"scroll\"`): sem páginas, vai mostrando mais ao descer;\r\n * com o servidor, `items` são as que já vieram e `onLoadMore` pede mais.\r\n * - **Realce**: o que se procurou realça-se no código e no título das linhas.\r\n * - **Marcar vários**: com `onSelectionChange`. O \"selecionar todos\" só marca\r\n * as linhas à vista, nunca as que não se veem.\r\n */\r\nexport default function ListFinder<T>({\r\n items, itemKey, itemContent, activeKey, onItemClick, selectedKeys, onSelectionChange, filters, onBack,\r\n searchable = true, search: pesquisaDeFora, onSearchChange, searchText, mode = 'pages', onLoadMore, total, page, onPageChange, pageSize = 50, loading = false,\r\n labels, footerExtra, bordered = true, className = '',\r\n}: ListFinderProps<T>) {\r\n const L = { ...DEFAULT_LIST_FINDER_LABELS, ...labels }\r\n const [pesquisaPropria, setPesquisaPropria] = useState('')\r\n const pesquisa = searchable ? (pesquisaDeFora ?? pesquisaPropria) : ''\r\n const pesquisar = (s: string) => { setPesquisaPropria(s); onSearchChange?.(s) }\r\n // Uma caixa de uma linha deita fora as quebras de linha: uma coluna colada\r\n // de uma folha de cálculo chegava como um número só, colado. Passam a vírgulas.\r\n const colar = (e: React.ClipboardEvent<HTMLInputElement>) => {\r\n const texto = e.clipboardData.getData('text')\r\n if (!/\\r?\\n/.test(texto.trim())) return\r\n e.preventDefault()\r\n const lista = texto.split(/\\r?\\n/).map(t => t.trim()).filter(Boolean).join(', ')\r\n const el = e.currentTarget\r\n pesquisar(el.value.slice(0, el.selectionStart ?? el.value.length) + lista + el.value.slice(el.selectionEnd ?? el.value.length))\r\n }\r\n\r\n // Os mesmos termos filtram (com `searchText`) e realçam-se nas linhas.\r\n const termos = useMemo(() => termosDaPesquisa(pesquisa).map(semAcentos), [pesquisa])\r\n const filtradas = useMemo(() => {\r\n if (!searchText || !termos.length) return items\r\n return items.filter(it => {\r\n const texto = semAcentos(searchText(it))\r\n return termos.some(t => texto.includes(t))\r\n })\r\n }, [items, termos, searchText])\r\n\r\n // Paginação: do servidor (com `total`), ou feita aqui.\r\n const peloServidor = total !== undefined\r\n const aRolar = mode === 'scroll'\r\n const [paginaPropria, setPaginaPropria] = useState(1)\r\n const totalLinhas = peloServidor ? total : filtradas.length\r\n const totalPaginas = Math.max(1, Math.ceil(totalLinhas / pageSize))\r\n const pagina = Math.min(peloServidor ? (page ?? 1) : paginaPropria, totalPaginas)\r\n // Uma pesquisa nova volta à primeira página: a terceira de antes já não é a mesma lista.\r\n useEffect(() => { if (!peloServidor) setPaginaPropria(1) }, [pesquisa, peloServidor])\r\n const irPara = (p: number) => { if (peloServidor) onPageChange?.(p); else setPaginaPropria(p) }\r\n // A rolar sem servidor: quantos lotes já se mostram.\r\n const [lotes, setLotes] = useState(1)\r\n useEffect(() => { setLotes(1) }, [pesquisa])\r\n const visiveis = peloServidor ? filtradas\r\n : aRolar ? filtradas.slice(0, lotes * pageSize)\r\n : filtradas.slice((pagina - 1) * pageSize, pagina * pageSize)\r\n const haMais = aRolar && (peloServidor ? !!onLoadMore && items.length < totalLinhas : visiveis.length < filtradas.length)\r\n\r\n // Ao mudar de página, ou de pesquisa a rolar, a lista volta ao topo.\r\n const [caixa, setCaixa] = useState<HTMLDivElement | null>(null)\r\n useEffect(() => { caixa?.scrollTo?.({ top: 0 }) }, [pagina, caixa])\r\n useEffect(() => { if (aRolar) caixa?.scrollTo?.({ top: 0 }) }, [pesquisa, aRolar, caixa])\r\n\r\n // A rolar: a linha-sentinela no fundo, ao chegar a 300px da vista, pede o\r\n // lote seguinte. Volta a observar a cada lote que chega: se continuar à\r\n // vista (uma janela alta), pede logo outro.\r\n const [fim, setFim] = useState<HTMLDivElement | null>(null)\r\n useEffect(() => {\r\n if (!fim || !caixa || !haMais || typeof IntersectionObserver === 'undefined') return\r\n const io = new IntersectionObserver(([e]) => {\r\n if (!e?.isIntersecting) return\r\n if (peloServidor) { if (!loading) onLoadMore?.() } else setLotes(n => n + 1)\r\n }, { root: caixa, rootMargin: '300px' })\r\n io.observe(fim)\r\n return () => io.disconnect()\r\n }, [fim, caixa, haMais, peloServidor, loading, onLoadMore, visiveis.length])\r\n\r\n const marcadas = selectedKeys ?? new Set<string>()\r\n const marcaveis = !!onSelectionChange\r\n const nMarcadasVisiveis = visiveis.filter(it => marcadas.has(itemKey(it))).length\r\n const todasMarcadas = visiveis.length > 0 && nMarcadasVisiveis === visiveis.length\r\n const marcar = (key: string, sim: boolean) => {\r\n const n = new Set(marcadas)\r\n if (sim) n.add(key); else n.delete(key)\r\n onSelectionChange?.(n)\r\n }\r\n const marcarVisiveis = () => {\r\n const n = new Set(marcadas)\r\n for (const it of visiveis) { if (todasMarcadas) n.delete(itemKey(it)); else n.add(itemKey(it)) }\r\n onSelectionChange?.(n)\r\n }\r\n\r\n const aCarregar = loading && visiveis.length === 0\r\n const esqueleto = <span className=\"inline-block h-2 w-20 rounded-sm bg-slate-200 animate-pulse motion-reduce:animate-none\" aria-hidden />\r\n\r\n return (\r\n <div className={`v1-reset flex h-full min-h-0 flex-col bg-white text-control text-slate-800 ${bordered ? 'border border-slate-200' : ''} ${className}`}>\r\n {/* Sem pesquisa, filtros nem voltar, não há cabeça: as linhas começam no topo. */}\r\n {(searchable || onBack || filters) && (\r\n <div className=\"space-y-2 border-b border-slate-100 px-3 py-2\">\r\n {(onBack || filters) && (\r\n <div className=\"flex items-center gap-1.5\">\r\n {onBack && (\r\n <button type=\"button\" onClick={onBack} aria-label={L.back} title={L.back}\r\n className=\"-ml-1 flex h-7 w-6 shrink-0 items-center justify-center rounded text-slate-400 transition-colors hover:bg-slate-100 hover:text-slate-700\">\r\n <ChevronLeftIcon size={14} />\r\n </button>\r\n )}\r\n <div className=\"flex min-w-0 flex-1 items-center gap-1.5\">{filters}</div>\r\n </div>\r\n )}\r\n {searchable && (\r\n <div className=\"relative\">\r\n {/* Caixa direita e fina, no tamanho do texto das linhas: a lista é\r\n uma coluna estreita, e a pesquisa não deve pesar mais do que ela. */}\r\n <SearchIcon size={11} className=\"pointer-events-none absolute top-1/2 left-2 -translate-y-1/2 text-slate-400\" />\r\n <input\r\n type=\"search\"\r\n value={pesquisa}\r\n onChange={e => pesquisar(e.target.value)}\r\n onPaste={colar}\r\n placeholder={L.searchPlaceholder}\r\n aria-label={L.search}\r\n className=\"block w-full min-w-0 border border-slate-200 bg-white py-1.5 pr-2 pl-6 text-micro text-slate-700 outline-none transition-colors placeholder:text-slate-400 focus:border-proef-blue\"\r\n />\r\n </div>\r\n )}\r\n </div>\r\n )}\r\n\r\n {/* Fora da área que rola: é a cabeça da lista, e é a descer a lista que\r\n se decide marcá-la toda. Branca, com a linha esbatida das linhas por\r\n baixo: uma faixa cinzenta entre o branco da pesquisa e o das linhas\r\n era um tom quase igual a meio, e lia-se como sujidade. */}\r\n {marcaveis && visiveis.length > 0 && (\r\n <div className={`flex items-center gap-1.5 bg-white px-3 py-2 text-[10px] text-slate-600 ${LINHA_ESBATIDA}`}>\r\n {/* Na coluna de 28px das linhas, para as caixas ficarem umas por baixo das outras. */}\r\n <span className=\"flex w-7 shrink-0 justify-center\">\r\n <Checkbox\r\n checked={todasMarcadas}\r\n indeterminate={nMarcadasVisiveis > 0 && !todasMarcadas}\r\n onChange={marcarVisiveis}\r\n aria-label={L.selectVisible(visiveis.length)}\r\n />\r\n </span>\r\n <span aria-hidden>{nMarcadasVisiveis > 0 ? L.selectedCount(nMarcadasVisiveis, visiveis.length) : L.selectVisible(visiveis.length)}</span>\r\n {marcadas.size > 0 && (\r\n <button type=\"button\" onClick={() => onSelectionChange?.(new Set())}\r\n className=\"ml-auto text-proef-blue hover:underline\">\r\n {L.clearSelection}\r\n </button>\r\n )}\r\n </div>\r\n )}\r\n\r\n <div className=\"relative flex min-h-0 flex-1 flex-col\">\r\n {/* `relative`: o texto só para leitores de ecrã (`sr-only`, posição\r\n absoluta) do fundo da lista tem de ficar preso a esta caixa. Sem isso\r\n ficava no fim das linhas, fora do corte do scroll, e esticava a\r\n página — dava para rolar até um branco sem nada. */}\r\n <div ref={setCaixa} className=\"relative min-h-0 flex-1 overflow-y-auto scrollbar-none\">\r\n {aCarregar ? (\r\n // A forma das linhas já diz que vem ali a lista. Mais do que cabem,\r\n // cortadas pela caixa: chega ao fundo em qualquer altura.\r\n <div role=\"status\" aria-busy=\"true\" className=\"h-full overflow-hidden\">\r\n <span className=\"sr-only\">{L.loading}</span>\r\n {['w-11/12', 'w-3/4', 'w-5/6', 'w-2/3', 'w-4/5', 'w-7/12'].flatMap(l => [l, l, l, l]).map((largura, i) => (\r\n <LinhaEsqueleto key={i} marcavel={marcaveis} largura={largura} />\r\n ))}\r\n </div>\r\n ) : visiveis.length === 0 ? (\r\n <p className=\"m-0 px-3 py-4 text-center text-[10px] text-slate-400\">{L.empty}</p>\r\n ) : (\r\n <ul className=\"m-0 list-none p-0\">\r\n {visiveis.map(it => {\r\n const key = itemKey(it)\r\n return (\r\n <ListItem\r\n key={key}\r\n {...itemContent(it)}\r\n active={activeKey === key}\r\n onClick={onItemClick ? () => onItemClick(it) : undefined}\r\n checked={marcadas.has(key)}\r\n onCheckedChange={marcaveis ? sim => marcar(key, sim) : undefined}\r\n checkboxLabel={L.checkItem(key)}\r\n highlight={termos}\r\n />\r\n )\r\n })}\r\n </ul>\r\n )}\r\n {/* A rolar: o lote seguinte a chegar, e a sentinela que o pede. */}\r\n {!aCarregar && haMais && (\r\n <div ref={setFim} role=\"status\" aria-busy=\"true\">\r\n <span className=\"sr-only\">{L.loading}</span>\r\n {['w-3/4', 'w-5/6', 'w-2/3'].map((largura, i) => <LinhaEsqueleto key={i} marcavel={marcaveis} largura={largura} />)}\r\n </div>\r\n )}\r\n </div>\r\n <OverlayScrollbar alvo={caixa} borda={0} />\r\n </div>\r\n\r\n {/* Sempre à vista e com a mesma altura, carregada ou não: aparecer do\r\n nada fazia saltar o fundo do painel. */}\r\n <div className={`flex shrink-0 items-center gap-2 border-t border-slate-100 bg-slate-50 px-3 text-micro text-slate-400 ${aRolar ? 'h-8 justify-center' : 'h-9 justify-between'}`}>\r\n <span className=\"flex items-center gap-1 whitespace-nowrap\">\r\n {aCarregar ? esqueleto : aRolar ? (\r\n // A rolar: quantas já se mostram, de quantas há.\r\n <><span className=\"font-medium text-slate-500 tabular-nums\">{visiveis.length.toLocaleString('pt-PT')}</span> / <span className=\"font-medium text-slate-500 tabular-nums\">{totalLinhas.toLocaleString('pt-PT')}</span> {L.itemsNoun(totalLinhas)}</>\r\n ) : <><span className=\"font-medium text-slate-500 tabular-nums\">{totalLinhas.toLocaleString('pt-PT')}</span> {L.itemsNoun(totalLinhas)}</>}\r\n {footerExtra}\r\n {/* Com linhas e a pedir outra vez (ex: outra página, ou a atualizar): um \"…\" a pulsar. */}\r\n {loading && !aCarregar && <span aria-hidden className=\"animate-pulse motion-reduce:animate-none\">…</span>}\r\n </span>\r\n {!aRolar && !aCarregar && totalPaginas > 1 && (\r\n <nav aria-label={L.pagination} className=\"flex items-center gap-0.5\">\r\n <span className=\"sr-only\">{L.page} {pagina} / {totalPaginas}</span>\r\n {numerosPagina(pagina, totalPaginas).map((p, i) => p === '…' ? (\r\n <span key={`r${i}`} aria-hidden className=\"flex h-6 min-w-3 items-center justify-center text-slate-300\">…</span>\r\n ) : (\r\n <button\r\n key={p}\r\n type=\"button\"\r\n onClick={() => irPara(p)}\r\n aria-current={p === pagina ? 'page' : undefined}\r\n className={`grid h-6 min-w-5 place-items-center px-1 tabular-nums transition-colors ${\r\n p === pagina ? 'cursor-default font-semibold text-slate-800' : 'text-slate-400 hover:text-slate-700'}`}\r\n >\r\n {/* Sem caixa nem fundo: a página atual é só o número mais forte.\r\n A cópia invisível a negrito guarda a largura, para os números\r\n não se mexerem ao trocar de página. */}\r\n <span aria-hidden className=\"invisible col-start-1 row-start-1 font-semibold\">{p.toLocaleString('pt-PT')}</span>\r\n <span className=\"col-start-1 row-start-1\">{p.toLocaleString('pt-PT')}</span>\r\n </button>\r\n ))}\r\n </nav>\r\n )}\r\n </div>\r\n </div>\r\n )\r\n}\r\n"],"names":[],"mappings":"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"}
@@ -16,6 +16,7 @@ function PaginatedTable({
16
16
  horizontalScrollbarBelow = true,
17
17
  skeleton = true,
18
18
  fadeRight = true,
19
+ resizableColumns = true,
19
20
  ...resto
20
21
  }) {
21
22
  const [tamanhoProprio, setTamanhoProprio] = useState(pageSize ?? pageSizeOptions?.[0] ?? 50);
@@ -34,6 +35,7 @@ function PaginatedTable({
34
35
  horizontalScrollbarBelow,
35
36
  skeleton,
36
37
  fadeRight,
38
+ resizableColumns,
37
39
  paginacaoNoTopo: {
38
40
  total,
39
41
  page,
@@ -1 +1 @@
1
- {"version":3,"file":"PaginatedTable.js","sources":["../../../../../../src/v1/components/tables/PaginatedTable/PaginatedTable.tsx"],"sourcesContent":["import React, { useState } from 'react'\r\nimport { TableBase, type TableProps } from '../Table/Table'\r\n\r\nexport interface PaginatedTableProps<T> extends TableProps<T> {\r\n /**\r\n * Total de linhas no servidor, quando é ele que pagina (`page` e\r\n * `onPageChange`). É o número da contagem e dá o número de páginas.\r\n */\r\n total?: number\r\n /**\r\n * Página atual (a contar de 1), quando é o servidor que pagina: a app pede a\r\n * página e dá-a em `data`. Sem isto, a tabela pagina-se a si mesma com todas\r\n * as linhas de `data`.\r\n */\r\n page?: number\r\n onPageChange?: (page: number) => void\r\n /** Tamanhos de página à escolha (ex: `[60, 120, 250, 500]`). Sem isto não há escolha. */\r\n pageSizeOptions?: number[]\r\n /**\r\n * A pessoa escolheu outro tamanho de página. Quando é o servidor que pagina,\r\n * a app volta à página 1 (ficar na 7.ª com 500 por página caía fora da lista).\r\n * Sem isto, a tabela guarda a escolha ela própria.\r\n */\r\n onPageSizeChange?: (pageSize: number) => void\r\n}\r\n\r\n/**\r\n * A [Table](?path=/docs/v1-components-tables-table--docs) para listas de página inteira: a paginação fica EM CIMA, numa\r\n * barra com a contagem de linhas, o tamanho de página e os números de página,\r\n * em vez de no fundo.\r\n *\r\n * Funciona de duas maneiras:\r\n * - **O servidor pagina** (`page`, `onPageChange`, `total`): `data` é a\r\n * página atual. Com filtros, ordenação ou agrupamento no cabeçalho, a tabela\r\n * passa a paginar o conjunto completo (`allData`) e a barra mostra as\r\n * páginas dela.\r\n * - **Todas as linhas de uma vez** (só `data`): a tabela pagina-se a si mesma.\r\n *\r\n * Por omissão ocupa a altura do pai (`fillHeight`), com as barras de scroll\r\n * por cima das linhas, linhas-esqueleto a carregar e a aresta direita\r\n * esbatida. Qualquer uma pode ser desligada.\r\n */\r\nexport default function PaginatedTable<T>({\r\n total,\r\n page,\r\n onPageChange,\r\n pageSizeOptions,\r\n onPageSizeChange,\r\n pageSize,\r\n data,\r\n allData,\r\n alwaysClient,\r\n fillHeight = true,\r\n overlayScrollbars = true,\r\n horizontalScrollbarBelow = true,\r\n skeleton = true,\r\n fadeRight = true,\r\n ...resto\r\n}: PaginatedTableProps<T>) {\r\n const [tamanhoProprio, setTamanhoProprio] = useState(pageSize ?? pageSizeOptions?.[0] ?? 50)\r\n const tamanho = onPageSizeChange ? (pageSize ?? pageSizeOptions?.[0] ?? 50) : tamanhoProprio\r\n // Sem paginação do servidor, todas as linhas são o conjunto completo e a tabela pagina-as.\r\n const peloServidor = !!onPageChange && page !== undefined\r\n return (\r\n <TableBase\r\n {...resto}\r\n data={data}\r\n allData={peloServidor ? allData : (allData ?? data)}\r\n alwaysClient={peloServidor ? alwaysClient : true}\r\n pageSize={tamanho}\r\n fillHeight={fillHeight}\r\n overlayScrollbars={overlayScrollbars}\r\n horizontalScrollbarBelow={horizontalScrollbarBelow}\r\n skeleton={skeleton}\r\n fadeRight={fadeRight}\r\n paginacaoNoTopo={{\r\n total,\r\n page,\r\n onPageChange,\r\n pageSizeOptions,\r\n onPageSizeChange: onPageSizeChange ?? (pageSizeOptions ? setTamanhoProprio : undefined),\r\n }}\r\n />\r\n )\r\n}\r\n"],"names":[],"mappings":";;;AA0CA,SAAwB,eAAkB;AAAA,EACxC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,aAAa;AAAA,EACb,oBAAoB;AAAA,EACpB,2BAA2B;AAAA,EAC3B,WAAW;AAAA,EACX,YAAY;AAAA,EACZ,GAAG;AACL,GAA2B;AACzB,QAAM,CAAC,gBAAgB,iBAAiB,IAAI,SAAS,YAAY,kBAAkB,CAAC,KAAK,EAAE;AAC3F,QAAM,UAAU,mBAAoB,YAAY,kBAAkB,CAAC,KAAK,KAAM;AAE9E,QAAM,eAAe,CAAC,CAAC,gBAAgB,SAAS;AAChD,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACE,GAAG;AAAA,MACJ;AAAA,MACA,SAAS,eAAe,UAAW,WAAW;AAAA,MAC9C,cAAc,eAAe,eAAe;AAAA,MAC5C,UAAU;AAAA,MACV;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,iBAAiB;AAAA,QACf;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,kBAAkB,qBAAqB,kBAAkB,oBAAoB;AAAA,MAAA;AAAA,IAC/E;AAAA,EAAA;AAGN;"}
1
+ {"version":3,"file":"PaginatedTable.js","sources":["../../../../../../src/v1/components/tables/PaginatedTable/PaginatedTable.tsx"],"sourcesContent":["import React, { useState } from 'react'\r\nimport { TableBase, type TableProps } from '../Table/Table'\r\n\r\nexport interface PaginatedTableProps<T> extends TableProps<T> {\r\n /**\r\n * Total de linhas no servidor, quando é ele que pagina (`page` e\r\n * `onPageChange`). É o número da contagem e dá o número de páginas.\r\n */\r\n total?: number\r\n /**\r\n * Página atual (a contar de 1), quando é o servidor que pagina: a app pede a\r\n * página e dá-a em `data`. Sem isto, a tabela pagina-se a si mesma com todas\r\n * as linhas de `data`.\r\n */\r\n page?: number\r\n onPageChange?: (page: number) => void\r\n /** Tamanhos de página à escolha (ex: `[60, 120, 250, 500]`). Sem isto não há escolha. */\r\n pageSizeOptions?: number[]\r\n /**\r\n * A pessoa escolheu outro tamanho de página. Quando é o servidor que pagina,\r\n * a app volta à página 1 (ficar na 7.ª com 500 por página caía fora da lista).\r\n * Sem isto, a tabela guarda a escolha ela própria.\r\n */\r\n onPageSizeChange?: (pageSize: number) => void\r\n}\r\n\r\n/**\r\n * A [Table](?path=/docs/v1-components-tables-table--docs) para listas de página inteira: a paginação fica EM CIMA, numa\r\n * barra com a contagem de linhas, o tamanho de página e os números de página,\r\n * em vez de no fundo.\r\n *\r\n * Funciona de duas maneiras:\r\n * - **O servidor pagina** (`page`, `onPageChange`, `total`): `data` é a\r\n * página atual. Com filtros, ordenação ou agrupamento no cabeçalho, a tabela\r\n * passa a paginar o conjunto completo (`allData`) e a barra mostra as\r\n * páginas dela.\r\n * - **Todas as linhas de uma vez** (só `data`): a tabela pagina-se a si mesma.\r\n *\r\n * Por omissão ocupa a altura do pai (`fillHeight`), com as barras de scroll\r\n * por cima das linhas, linhas-esqueleto a carregar e a aresta direita\r\n * esbatida, e as colunas redimensionáveis. Qualquer uma pode ser desligada.\r\n */\r\nexport default function PaginatedTable<T>({\r\n total,\r\n page,\r\n onPageChange,\r\n pageSizeOptions,\r\n onPageSizeChange,\r\n pageSize,\r\n data,\r\n allData,\r\n alwaysClient,\r\n fillHeight = true,\r\n overlayScrollbars = true,\r\n horizontalScrollbarBelow = true,\r\n skeleton = true,\r\n fadeRight = true,\r\n resizableColumns = true,\r\n ...resto\r\n}: PaginatedTableProps<T>) {\r\n const [tamanhoProprio, setTamanhoProprio] = useState(pageSize ?? pageSizeOptions?.[0] ?? 50)\r\n const tamanho = onPageSizeChange ? (pageSize ?? pageSizeOptions?.[0] ?? 50) : tamanhoProprio\r\n // Sem paginação do servidor, todas as linhas são o conjunto completo e a tabela pagina-as.\r\n const peloServidor = !!onPageChange && page !== undefined\r\n return (\r\n <TableBase\r\n {...resto}\r\n data={data}\r\n allData={peloServidor ? allData : (allData ?? data)}\r\n alwaysClient={peloServidor ? alwaysClient : true}\r\n pageSize={tamanho}\r\n fillHeight={fillHeight}\r\n overlayScrollbars={overlayScrollbars}\r\n horizontalScrollbarBelow={horizontalScrollbarBelow}\r\n skeleton={skeleton}\r\n fadeRight={fadeRight}\r\n resizableColumns={resizableColumns}\r\n paginacaoNoTopo={{\r\n total,\r\n page,\r\n onPageChange,\r\n pageSizeOptions,\r\n onPageSizeChange: onPageSizeChange ?? (pageSizeOptions ? setTamanhoProprio : undefined),\r\n }}\r\n />\r\n )\r\n}\r\n"],"names":[],"mappings":";;;AA0CA,SAAwB,eAAkB;AAAA,EACxC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,aAAa;AAAA,EACb,oBAAoB;AAAA,EACpB,2BAA2B;AAAA,EAC3B,WAAW;AAAA,EACX,YAAY;AAAA,EACZ,mBAAmB;AAAA,EACnB,GAAG;AACL,GAA2B;AACzB,QAAM,CAAC,gBAAgB,iBAAiB,IAAI,SAAS,YAAY,kBAAkB,CAAC,KAAK,EAAE;AAC3F,QAAM,UAAU,mBAAoB,YAAY,kBAAkB,CAAC,KAAK,KAAM;AAE9E,QAAM,eAAe,CAAC,CAAC,gBAAgB,SAAS;AAChD,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACE,GAAG;AAAA,MACJ;AAAA,MACA,SAAS,eAAe,UAAW,WAAW;AAAA,MAC9C,cAAc,eAAe,eAAe;AAAA,MAC5C,UAAU;AAAA,MACV;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,iBAAiB;AAAA,QACf;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,kBAAkB,qBAAqB,kBAAkB,oBAAoB;AAAA,MAAA;AAAA,IAC/E;AAAA,EAAA;AAGN;"}
@@ -111,7 +111,7 @@ function ColumnFilter({
111
111
  width: LARGURA,
112
112
  maxHeight: ALTURA
113
113
  },
114
- className: "v1-reset z-[250] flex flex-col bg-white border border-slate-200 shadow-lg",
114
+ className: "v1-reset z-[250] flex flex-col overflow-hidden rounded-lg bg-white border border-slate-200 shadow-lg",
115
115
  children: [
116
116
  label && /* @__PURE__ */ jsx("div", { className: "px-3 pt-2.5 pb-2 border-b border-slate-100 flex-shrink-0", children: /* @__PURE__ */ jsx("span", { className: "text-[9px] font-black uppercase tracking-widest text-proef-blue", children: label }) }),
117
117
  aCarregar && /* @__PURE__ */ jsxs("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", children: [
@@ -123,7 +123,7 @@ function ColumnFilter({
123
123
  "button",
124
124
  {
125
125
  onClick: () => onOrdenar("asc"),
126
- className: `w-full flex items-center gap-2 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"}`,
126
+ 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"}`,
127
127
  children: [
128
128
  /* @__PURE__ */ jsx(ChevronUpIcon, { size: 11 }),
129
129
  " ",
@@ -135,7 +135,7 @@ function ColumnFilter({
135
135
  "button",
136
136
  {
137
137
  onClick: () => onOrdenar("desc"),
138
- className: `w-full flex items-center gap-2 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"}`,
138
+ 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"}`,
139
139
  children: [
140
140
  /* @__PURE__ */ jsx(ChevronDownIcon, { size: 11 }),
141
141
  " ",
@@ -156,7 +156,7 @@ function ColumnFilter({
156
156
  onKeyDown: (e) => {
157
157
  if (e.key === "Enter") onAplicarDatas?.(de, ate);
158
158
  },
159
- className: "w-full mt-0.5 bg-slate-50 border border-slate-100 px-2.5 py-1.5 text-xs outline-none focus:border-proef-blue"
159
+ 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"
160
160
  }
161
161
  )
162
162
  ] }),
@@ -171,7 +171,7 @@ function ColumnFilter({
171
171
  onKeyDown: (e) => {
172
172
  if (e.key === "Enter") onAplicarDatas?.(de, ate);
173
173
  },
174
- className: "w-full mt-0.5 bg-slate-50 border border-slate-100 px-2.5 py-1.5 text-xs outline-none focus:border-proef-blue"
174
+ 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"
175
175
  }
176
176
  )
177
177
  ] })
@@ -195,7 +195,7 @@ function ColumnFilter({
195
195
  onKeyDown: (e) => {
196
196
  if (e.key === "Enter") onAplicarNumeros?.(min, max);
197
197
  },
198
- className: "w-full mt-0.5 bg-slate-50 border border-slate-100 px-2.5 py-1.5 text-xs outline-none focus:border-proef-blue"
198
+ 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"
199
199
  }
200
200
  )
201
201
  ] }),
@@ -210,7 +210,7 @@ function ColumnFilter({
210
210
  onKeyDown: (e) => {
211
211
  if (e.key === "Enter") onAplicarNumeros?.(min, max);
212
212
  },
213
- className: "w-full mt-0.5 bg-slate-50 border border-slate-100 px-2.5 py-1.5 text-xs outline-none focus:border-proef-blue"
213
+ 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"
214
214
  }
215
215
  )
216
216
  ] })
@@ -225,7 +225,7 @@ function ColumnFilter({
225
225
  value: busca,
226
226
  onChange: (e) => setBusca(e.target.value),
227
227
  placeholder: L.search,
228
- className: "flex-1 min-w-0 bg-slate-50 border border-slate-100 px-2.5 py-1.5 text-xs outline-none focus:border-proef-blue"
228
+ 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"
229
229
  }
230
230
  ),
231
231
  botaoModo && /* @__PURE__ */ jsx("span", { className: "pr-1.5", children: botaoModo })
@@ -236,15 +236,15 @@ function ColumnFilter({
236
236
  onClick: alternarTodos,
237
237
  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",
238
238
  children: [
239
- /* @__PURE__ */ jsx("div", { className: `w-3.5 h-3.5 flex items-center justify-center border flex-shrink-0 transition-colors ${todosVisiveis ? "bg-primary border-primary" : algunsVisiveis ? "bg-primary/30 border-primary" : "border-slate-300"}`, children: todosVisiveis && /* @__PURE__ */ jsx(CheckIcon, { size: 9, strokeWidth: 3, className: "text-white" }) }),
239
+ /* @__PURE__ */ jsx("div", { className: `w-3.5 h-3.5 flex items-center justify-center rounded-sm border flex-shrink-0 transition-colors ${todosVisiveis ? "bg-primary border-primary" : algunsVisiveis ? "bg-primary/30 border-primary" : "border-slate-300"}`, children: todosVisiveis && /* @__PURE__ */ jsx(CheckIcon, { size: 9, strokeWidth: 3, className: "text-white" }) }),
240
240
  /* @__PURE__ */ jsx("span", { className: "text-caption font-semibold text-slate-600", children: busca ? L.selectAllFound(visiveis.length) : L.selectAll })
241
241
  ]
242
242
  }
243
243
  ),
244
244
  /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto p-1.5 space-y-0.5 min-h-0", children: visiveis.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-caption text-slate-400 text-center py-3", children: L.noResults }) : visiveis.map((v) => {
245
245
  const escolhido = rascunho.has(v);
246
- return /* @__PURE__ */ jsxs("button", { onClick: () => alternar(v), className: "w-full flex items-center gap-2 px-2 py-1.5 hover:bg-slate-50 text-left", children: [
247
- /* @__PURE__ */ jsx("div", { className: `w-3.5 h-3.5 flex items-center justify-center border flex-shrink-0 transition-colors ${escolhido ? "bg-primary border-primary" : "border-slate-300"}`, children: escolhido && /* @__PURE__ */ jsx(CheckIcon, { size: 9, strokeWidth: 3, className: "text-white" }) }),
246
+ return /* @__PURE__ */ jsxs("button", { onClick: () => alternar(v), className: "w-full flex items-center gap-2 rounded-md px-2 py-1.5 hover:bg-slate-50 text-left", children: [
247
+ /* @__PURE__ */ jsx("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"}`, children: escolhido && /* @__PURE__ */ jsx(CheckIcon, { size: 9, strokeWidth: 3, className: "text-white" }) }),
248
248
  /* @__PURE__ */ jsx("span", { className: "text-xs text-slate-700 whitespace-normal break-words leading-snug", children: v })
249
249
  ] }, v);
250
250
  }) })
@@ -259,7 +259,7 @@ function ColumnFilter({
259
259
  else onAplicar(rascunho);
260
260
  },
261
261
  disabled: !sujo,
262
- className: "flex-1 text-micro font-black uppercase tracking-wider text-white bg-primary hover:bg-primary/90 py-1.5 disabled:opacity-30",
262
+ 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",
263
263
  children: L.apply
264
264
  }
265
265
  ),
@@ -267,7 +267,7 @@ function ColumnFilter({
267
267
  "button",
268
268
  {
269
269
  onClick: onFechar,
270
- className: "text-micro font-black uppercase tracking-wider text-slate-500 hover:text-slate-700 px-2 py-1.5",
270
+ className: "text-micro font-black uppercase tracking-wider text-slate-500 hover:text-slate-700 rounded-md px-2 py-1.5",
271
271
  children: L.cancel
272
272
  }
273
273
  ),
@@ -275,7 +275,7 @@ function ColumnFilter({
275
275
  "button",
276
276
  {
277
277
  onClick: onLimpar,
278
- className: "text-micro font-black uppercase tracking-wider text-red-400 hover:text-red-500 px-2 py-1.5",
278
+ className: "text-micro font-black uppercase tracking-wider text-red-400 hover:text-red-500 rounded-md px-2 py-1.5",
279
279
  children: L.clearFilter
280
280
  }
281
281
  )