@proef/common-ui 2.2.0 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/index.js +26 -3
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/legacy/components/molecules/DataTable/DataTable.js +1 -1
- package/dist/esm/legacy/components/molecules/DataTable/DataTable.js.map +1 -1
- package/dist/esm/v1/capabilities/documents/QRScanning/scanQRCodes.js.map +1 -1
- package/dist/esm/v1/components/documents/DocumentDropzone/DocumentDropzone.js.map +1 -1
- package/dist/esm/v1/components/lists/ListFinder/ListFinder.js +111 -54
- package/dist/esm/v1/components/lists/ListFinder/ListFinder.js.map +1 -1
- package/dist/esm/v1/foundations/icons/icons.js +49 -0
- package/dist/esm/v1/foundations/icons/icons.js.map +1 -1
- package/dist/esm/v1/primitives/display/RichText/RichText.js +12 -0
- package/dist/esm/v1/primitives/display/RichText/RichText.js.map +1 -0
- package/dist/esm/v1/primitives/display/RichText/utils.js +56 -0
- package/dist/esm/v1/primitives/display/RichText/utils.js.map +1 -0
- package/dist/esm/v1/primitives/documents/PdfPreview/PdfPreview.js.map +1 -1
- package/dist/esm/v1/primitives/documents/PdfPreview/pdfjs.js.map +1 -1
- package/dist/esm/v1/primitives/forms/RichTextEditor/RichTextEditor.js +253 -0
- package/dist/esm/v1/primitives/forms/RichTextEditor/RichTextEditor.js.map +1 -0
- package/dist/esm/v1/primitives/forms/TextInput/TextInput.js.map +1 -1
- package/dist/esm/v1/primitives/lists/ListItem/ListItem.js +45 -11
- package/dist/esm/v1/primitives/lists/ListItem/ListItem.js.map +1 -1
- package/dist/style.components.css +1 -1
- package/dist/style.css +1 -1
- package/dist/style.layered.css +1 -1
- package/dist/types/v1/components/lists/ListFinder/ListFinder.d.ts +31 -5
- package/dist/types/v1/foundations/icons/icons.d.ts +20 -0
- package/dist/types/v1/index.d.ts +8 -3
- package/dist/types/v1/primitives/display/RichText/RichText.d.ts +17 -0
- package/dist/types/v1/primitives/display/RichText/utils.d.ts +22 -0
- package/dist/types/v1/primitives/forms/RichTextEditor/RichTextEditor.d.ts +64 -0
- package/dist/types/v1/primitives/lists/ListItem/ListItem.d.ts +39 -8
- package/package.json +6 -1
|
@@ -1 +1 @@
|
|
|
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
|
+
{"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 +1 @@
|
|
|
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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"}
|
|
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":"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"}
|
|
@@ -1,17 +1,32 @@
|
|
|
1
1
|
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useState, useMemo, useEffect } from "react";
|
|
3
3
|
import { ChevronLeftIcon, SearchIcon } from "../../../foundations/icons/icons.js";
|
|
4
|
-
import ListItem from "../../../primitives/lists/ListItem/ListItem.js";
|
|
4
|
+
import ListItem, { LINHA_ESBATIDA } from "../../../primitives/lists/ListItem/ListItem.js";
|
|
5
5
|
import Checkbox from "../../../primitives/forms/Checkbox/Checkbox.js";
|
|
6
6
|
import { OverlayScrollbar } from "../../tables/Table/OverlayScrollbar.js";
|
|
7
7
|
import { DEFAULT_LIST_FINDER_LABELS } from "./labels.js";
|
|
8
8
|
const semAcentos = (s) => s.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase();
|
|
9
|
+
function termosDaPesquisa(texto) {
|
|
10
|
+
const porEspacos = texto.trim().split(/\s+/).filter(Boolean);
|
|
11
|
+
if (porEspacos.length > 1 && porEspacos.every((t) => /^\d{6,}$/.test(t))) return porEspacos;
|
|
12
|
+
return texto.split(/[\n,;\t]+/).map((t) => t.trim()).filter(Boolean);
|
|
13
|
+
}
|
|
9
14
|
function numerosPagina(atual, total) {
|
|
10
15
|
if (total <= 5) return Array.from({ length: total }, (_, i) => i + 1);
|
|
11
16
|
if (atual <= 2) return [1, 2, 3, "…", total];
|
|
12
17
|
if (atual >= total - 1) return [1, "…", total - 2, total - 1, total];
|
|
13
18
|
return [1, "…", atual, "…", total];
|
|
14
19
|
}
|
|
20
|
+
function LinhaEsqueleto({ marcavel, largura }) {
|
|
21
|
+
return /* @__PURE__ */ jsxs("div", { className: `flex items-start gap-1.5 px-3 py-3 animate-pulse motion-reduce:animate-none ${LINHA_ESBATIDA}`, children: [
|
|
22
|
+
marcavel && /* @__PURE__ */ jsx("span", { className: "flex w-7 shrink-0 justify-center", children: /* @__PURE__ */ jsx("span", { className: "mt-0.5 h-4 w-4 border border-slate-200 bg-slate-100" }) }),
|
|
23
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1 space-y-1.5", children: [
|
|
24
|
+
/* @__PURE__ */ jsx("div", { className: "h-3 w-20 bg-slate-200" }),
|
|
25
|
+
/* @__PURE__ */ jsx("div", { className: `h-2.5 ${largura} bg-slate-200` }),
|
|
26
|
+
/* @__PURE__ */ jsx("div", { className: "h-1.5 w-1/3 bg-slate-100" })
|
|
27
|
+
] })
|
|
28
|
+
] });
|
|
29
|
+
}
|
|
15
30
|
function ListFinder({
|
|
16
31
|
items,
|
|
17
32
|
itemKey,
|
|
@@ -22,20 +37,25 @@ function ListFinder({
|
|
|
22
37
|
onSelectionChange,
|
|
23
38
|
filters,
|
|
24
39
|
onBack,
|
|
40
|
+
searchable = true,
|
|
25
41
|
search: pesquisaDeFora,
|
|
26
42
|
onSearchChange,
|
|
27
43
|
searchText,
|
|
44
|
+
mode = "pages",
|
|
45
|
+
onLoadMore,
|
|
28
46
|
total,
|
|
29
47
|
page,
|
|
30
48
|
onPageChange,
|
|
31
49
|
pageSize = 50,
|
|
32
50
|
loading = false,
|
|
33
51
|
labels,
|
|
52
|
+
footerExtra,
|
|
53
|
+
bordered = true,
|
|
34
54
|
className = ""
|
|
35
55
|
}) {
|
|
36
56
|
const L = { ...DEFAULT_LIST_FINDER_LABELS, ...labels };
|
|
37
57
|
const [pesquisaPropria, setPesquisaPropria] = useState("");
|
|
38
|
-
const pesquisa = pesquisaDeFora ?? pesquisaPropria;
|
|
58
|
+
const pesquisa = searchable ? pesquisaDeFora ?? pesquisaPropria : "";
|
|
39
59
|
const pesquisar = (s) => {
|
|
40
60
|
setPesquisaPropria(s);
|
|
41
61
|
onSearchChange?.(s);
|
|
@@ -48,15 +68,16 @@ function ListFinder({
|
|
|
48
68
|
const el = e.currentTarget;
|
|
49
69
|
pesquisar(el.value.slice(0, el.selectionStart ?? el.value.length) + lista + el.value.slice(el.selectionEnd ?? el.value.length));
|
|
50
70
|
};
|
|
71
|
+
const termos = useMemo(() => termosDaPesquisa(pesquisa).map(semAcentos), [pesquisa]);
|
|
51
72
|
const filtradas = useMemo(() => {
|
|
52
|
-
if (!searchText || !
|
|
53
|
-
const termos = pesquisa.split(/[\n,;\t]+/).map((t) => semAcentos(t.trim())).filter(Boolean);
|
|
73
|
+
if (!searchText || !termos.length) return items;
|
|
54
74
|
return items.filter((it) => {
|
|
55
75
|
const texto = semAcentos(searchText(it));
|
|
56
76
|
return termos.some((t) => texto.includes(t));
|
|
57
77
|
});
|
|
58
|
-
}, [items,
|
|
78
|
+
}, [items, termos, searchText]);
|
|
59
79
|
const peloServidor = total !== void 0;
|
|
80
|
+
const aRolar = mode === "scroll";
|
|
60
81
|
const [paginaPropria, setPaginaPropria] = useState(1);
|
|
61
82
|
const totalLinhas = peloServidor ? total : filtradas.length;
|
|
62
83
|
const totalPaginas = Math.max(1, Math.ceil(totalLinhas / pageSize));
|
|
@@ -68,11 +89,31 @@ function ListFinder({
|
|
|
68
89
|
if (peloServidor) onPageChange?.(p);
|
|
69
90
|
else setPaginaPropria(p);
|
|
70
91
|
};
|
|
71
|
-
const
|
|
92
|
+
const [lotes, setLotes] = useState(1);
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
setLotes(1);
|
|
95
|
+
}, [pesquisa]);
|
|
96
|
+
const visiveis = peloServidor ? filtradas : aRolar ? filtradas.slice(0, lotes * pageSize) : filtradas.slice((pagina - 1) * pageSize, pagina * pageSize);
|
|
97
|
+
const haMais = aRolar && (peloServidor ? !!onLoadMore && items.length < totalLinhas : visiveis.length < filtradas.length);
|
|
72
98
|
const [caixa, setCaixa] = useState(null);
|
|
73
99
|
useEffect(() => {
|
|
74
100
|
caixa?.scrollTo?.({ top: 0 });
|
|
75
101
|
}, [pagina, caixa]);
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
if (aRolar) caixa?.scrollTo?.({ top: 0 });
|
|
104
|
+
}, [pesquisa, aRolar, caixa]);
|
|
105
|
+
const [fim, setFim] = useState(null);
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
if (!fim || !caixa || !haMais || typeof IntersectionObserver === "undefined") return;
|
|
108
|
+
const io = new IntersectionObserver(([e]) => {
|
|
109
|
+
if (!e?.isIntersecting) return;
|
|
110
|
+
if (peloServidor) {
|
|
111
|
+
if (!loading) onLoadMore?.();
|
|
112
|
+
} else setLotes((n) => n + 1);
|
|
113
|
+
}, { root: caixa, rootMargin: "300px" });
|
|
114
|
+
io.observe(fim);
|
|
115
|
+
return () => io.disconnect();
|
|
116
|
+
}, [fim, caixa, haMais, peloServidor, loading, onLoadMore, visiveis.length]);
|
|
76
117
|
const marcadas = selectedKeys ?? /* @__PURE__ */ new Set();
|
|
77
118
|
const marcaveis = !!onSelectionChange;
|
|
78
119
|
const nMarcadasVisiveis = visiveis.filter((it) => marcadas.has(itemKey(it))).length;
|
|
@@ -93,8 +134,8 @@ function ListFinder({
|
|
|
93
134
|
};
|
|
94
135
|
const aCarregar = loading && visiveis.length === 0;
|
|
95
136
|
const esqueleto = /* @__PURE__ */ jsx("span", { className: "inline-block h-2 w-20 rounded-sm bg-slate-200 animate-pulse motion-reduce:animate-none", "aria-hidden": true });
|
|
96
|
-
return /* @__PURE__ */ jsxs("div", { className: `v1-reset flex h-full min-h-0 flex-col
|
|
97
|
-
/* @__PURE__ */ jsxs("div", { className: "space-y-2 border-b border-slate-100 px-3 py-2
|
|
137
|
+
return /* @__PURE__ */ jsxs("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}`, children: [
|
|
138
|
+
(searchable || onBack || filters) && /* @__PURE__ */ jsxs("div", { className: "space-y-2 border-b border-slate-100 px-3 py-2", children: [
|
|
98
139
|
(onBack || filters) && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
99
140
|
onBack && /* @__PURE__ */ jsx(
|
|
100
141
|
"button",
|
|
@@ -109,8 +150,8 @@ function ListFinder({
|
|
|
109
150
|
),
|
|
110
151
|
/* @__PURE__ */ jsx("div", { className: "flex min-w-0 flex-1 items-center gap-1.5", children: filters })
|
|
111
152
|
] }),
|
|
112
|
-
/* @__PURE__ */ jsxs("div", { className: "relative", children: [
|
|
113
|
-
/* @__PURE__ */ jsx(SearchIcon, { size:
|
|
153
|
+
searchable && /* @__PURE__ */ jsxs("div", { className: "relative", children: [
|
|
154
|
+
/* @__PURE__ */ jsx(SearchIcon, { size: 11, className: "pointer-events-none absolute top-1/2 left-2 -translate-y-1/2 text-slate-400" }),
|
|
114
155
|
/* @__PURE__ */ jsx(
|
|
115
156
|
"input",
|
|
116
157
|
{
|
|
@@ -120,13 +161,13 @@ function ListFinder({
|
|
|
120
161
|
onPaste: colar,
|
|
121
162
|
placeholder: L.searchPlaceholder,
|
|
122
163
|
"aria-label": L.search,
|
|
123
|
-
className: "block
|
|
164
|
+
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"
|
|
124
165
|
}
|
|
125
166
|
)
|
|
126
167
|
] })
|
|
127
168
|
] }),
|
|
128
|
-
marcaveis && visiveis.length > 0 && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-
|
|
129
|
-
/* @__PURE__ */ jsx(
|
|
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: [
|
|
170
|
+
/* @__PURE__ */ jsx("span", { className: "flex w-7 shrink-0 justify-center", children: /* @__PURE__ */ jsx(
|
|
130
171
|
Checkbox,
|
|
131
172
|
{
|
|
132
173
|
checked: todasMarcadas,
|
|
@@ -134,58 +175,70 @@ function ListFinder({
|
|
|
134
175
|
onChange: marcarVisiveis,
|
|
135
176
|
"aria-label": L.selectVisible(visiveis.length)
|
|
136
177
|
}
|
|
137
|
-
),
|
|
178
|
+
) }),
|
|
138
179
|
/* @__PURE__ */ jsx("span", { "aria-hidden": true, children: nMarcadasVisiveis > 0 ? L.selectedCount(nMarcadasVisiveis, visiveis.length) : L.selectVisible(visiveis.length) }),
|
|
139
180
|
marcadas.size > 0 && /* @__PURE__ */ jsx(
|
|
140
181
|
"button",
|
|
141
182
|
{
|
|
142
183
|
type: "button",
|
|
143
184
|
onClick: () => onSelectionChange?.(/* @__PURE__ */ new Set()),
|
|
144
|
-
className: "ml-auto
|
|
185
|
+
className: "ml-auto text-proef-blue hover:underline",
|
|
145
186
|
children: L.clearSelection
|
|
146
187
|
}
|
|
147
188
|
)
|
|
148
189
|
] }),
|
|
149
190
|
/* @__PURE__ */ jsxs("div", { className: "relative flex min-h-0 flex-1 flex-col", children: [
|
|
150
|
-
/* @__PURE__ */
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
191
|
+
/* @__PURE__ */ jsxs("div", { ref: setCaixa, className: "relative min-h-0 flex-1 overflow-y-auto scrollbar-none", children: [
|
|
192
|
+
aCarregar ? (
|
|
193
|
+
// A forma das linhas já diz que vem ali a lista. Mais do que cabem,
|
|
194
|
+
// cortadas pela caixa: chega ao fundo em qualquer altura.
|
|
195
|
+
/* @__PURE__ */ jsxs("div", { role: "status", "aria-busy": "true", className: "h-full overflow-hidden", children: [
|
|
196
|
+
/* @__PURE__ */ jsx("span", { className: "sr-only", children: L.loading }),
|
|
197
|
+
["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) => /* @__PURE__ */ jsx(LinhaEsqueleto, { marcavel: marcaveis, largura }, i))
|
|
198
|
+
] })
|
|
199
|
+
) : visiveis.length === 0 ? /* @__PURE__ */ jsx("p", { className: "m-0 px-3 py-4 text-center text-[10px] text-slate-400", children: L.empty }) : /* @__PURE__ */ jsx("ul", { className: "m-0 list-none p-0", children: visiveis.map((it) => {
|
|
200
|
+
const key = itemKey(it);
|
|
201
|
+
return /* @__PURE__ */ jsx(
|
|
202
|
+
ListItem,
|
|
203
|
+
{
|
|
204
|
+
...itemContent(it),
|
|
205
|
+
active: activeKey === key,
|
|
206
|
+
onClick: onItemClick ? () => onItemClick(it) : void 0,
|
|
207
|
+
checked: marcadas.has(key),
|
|
208
|
+
onCheckedChange: marcaveis ? (sim) => marcar(key, sim) : void 0,
|
|
209
|
+
checkboxLabel: L.checkItem(key),
|
|
210
|
+
highlight: termos
|
|
211
|
+
},
|
|
212
|
+
key
|
|
213
|
+
);
|
|
214
|
+
}) }),
|
|
215
|
+
!aCarregar && haMais && /* @__PURE__ */ jsxs("div", { ref: setFim, role: "status", "aria-busy": "true", children: [
|
|
154
216
|
/* @__PURE__ */ jsx("span", { className: "sr-only", children: L.loading }),
|
|
155
|
-
["w-
|
|
156
|
-
marcaveis && /* @__PURE__ */ jsx("div", { className: "mt-0.5 h-4 w-4 shrink-0 border border-slate-200 bg-slate-100" }),
|
|
157
|
-
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1 space-y-1.5", children: [
|
|
158
|
-
/* @__PURE__ */ jsx("div", { className: "h-3 w-12 bg-slate-200" }),
|
|
159
|
-
/* @__PURE__ */ jsx("div", { className: "h-1.5 w-16 bg-slate-100" }),
|
|
160
|
-
/* @__PURE__ */ jsx("div", { className: `h-2.5 ${largura} bg-slate-200` }),
|
|
161
|
-
/* @__PURE__ */ jsx("div", { className: "h-1.5 w-1/3 bg-slate-100" })
|
|
162
|
-
] })
|
|
163
|
-
] }, i))
|
|
217
|
+
["w-3/4", "w-5/6", "w-2/3"].map((largura, i) => /* @__PURE__ */ jsx(LinhaEsqueleto, { marcavel: marcaveis, largura }, i))
|
|
164
218
|
] })
|
|
165
|
-
|
|
166
|
-
const key = itemKey(it);
|
|
167
|
-
return /* @__PURE__ */ jsx(
|
|
168
|
-
ListItem,
|
|
169
|
-
{
|
|
170
|
-
...itemContent(it),
|
|
171
|
-
active: activeKey === key,
|
|
172
|
-
onClick: onItemClick ? () => onItemClick(it) : void 0,
|
|
173
|
-
checked: marcadas.has(key),
|
|
174
|
-
onCheckedChange: marcaveis ? (sim) => marcar(key, sim) : void 0,
|
|
175
|
-
checkboxLabel: L.checkItem(key)
|
|
176
|
-
},
|
|
177
|
-
key
|
|
178
|
-
);
|
|
179
|
-
}) }) }),
|
|
219
|
+
] }),
|
|
180
220
|
/* @__PURE__ */ jsx(OverlayScrollbar, { alvo: caixa, borda: 0 })
|
|
181
221
|
] }),
|
|
182
|
-
/* @__PURE__ */ jsxs("div", { className:
|
|
183
|
-
/* @__PURE__ */
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
222
|
+
/* @__PURE__ */ jsxs("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"}`, children: [
|
|
223
|
+
/* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1 whitespace-nowrap", children: [
|
|
224
|
+
aCarregar ? esqueleto : aRolar ? (
|
|
225
|
+
// A rolar: quantas já se mostram, de quantas há.
|
|
226
|
+
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
227
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium text-slate-500 tabular-nums", children: visiveis.length.toLocaleString("pt-PT") }),
|
|
228
|
+
" / ",
|
|
229
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium text-slate-500 tabular-nums", children: totalLinhas.toLocaleString("pt-PT") }),
|
|
230
|
+
" ",
|
|
231
|
+
L.itemsNoun(totalLinhas)
|
|
232
|
+
] })
|
|
233
|
+
) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
234
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium text-slate-500 tabular-nums", children: totalLinhas.toLocaleString("pt-PT") }),
|
|
235
|
+
" ",
|
|
236
|
+
L.itemsNoun(totalLinhas)
|
|
237
|
+
] }),
|
|
238
|
+
footerExtra,
|
|
239
|
+
loading && !aCarregar && /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "animate-pulse motion-reduce:animate-none", children: "…" })
|
|
240
|
+
] }),
|
|
241
|
+
!aRolar && !aCarregar && totalPaginas > 1 && /* @__PURE__ */ jsxs("nav", { "aria-label": L.pagination, className: "flex items-center gap-0.5", children: [
|
|
189
242
|
/* @__PURE__ */ jsxs("span", { className: "sr-only", children: [
|
|
190
243
|
L.page,
|
|
191
244
|
" ",
|
|
@@ -193,14 +246,17 @@ function ListFinder({
|
|
|
193
246
|
" / ",
|
|
194
247
|
totalPaginas
|
|
195
248
|
] }),
|
|
196
|
-
numerosPagina(pagina, totalPaginas).map((p, i) => p === "…" ? /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "flex h-
|
|
249
|
+
numerosPagina(pagina, totalPaginas).map((p, i) => p === "…" ? /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "flex h-6 min-w-3 items-center justify-center text-slate-300", children: "…" }, `r${i}`) : /* @__PURE__ */ jsxs(
|
|
197
250
|
"button",
|
|
198
251
|
{
|
|
199
252
|
type: "button",
|
|
200
253
|
onClick: () => irPara(p),
|
|
201
254
|
"aria-current": p === pagina ? "page" : void 0,
|
|
202
|
-
className: `h-
|
|
203
|
-
children:
|
|
255
|
+
className: `grid h-6 min-w-5 place-items-center px-1 tabular-nums transition-colors ${p === pagina ? "cursor-default font-semibold text-slate-800" : "text-slate-400 hover:text-slate-700"}`,
|
|
256
|
+
children: [
|
|
257
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "invisible col-start-1 row-start-1 font-semibold", children: p.toLocaleString("pt-PT") }),
|
|
258
|
+
/* @__PURE__ */ jsx("span", { className: "col-start-1 row-start-1", children: p.toLocaleString("pt-PT") })
|
|
259
|
+
]
|
|
204
260
|
},
|
|
205
261
|
p
|
|
206
262
|
))
|
|
@@ -209,6 +265,7 @@ function ListFinder({
|
|
|
209
265
|
] });
|
|
210
266
|
}
|
|
211
267
|
export {
|
|
212
|
-
ListFinder as default
|
|
268
|
+
ListFinder as default,
|
|
269
|
+
termosDaPesquisa
|
|
213
270
|
};
|
|
214
271
|
//# sourceMappingURL=ListFinder.js.map
|