@bluemarble/bi 0.6.5 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","names":[],"sources":["../src/cli/scaffold.ts","../src/cli/index.ts"],"sourcesContent":["import { literal, toModelCode } from '../codegen'\nimport type { ChartsLayout } from '../layout'\n\n/**\n * Arquivos de um dashboard novo: o modelo vazio (`models/<nome>.ts`), o registro dele no\n * `models/index.ts` e o layout vazio (`layouts/<id>.json`). Tudo como texto, sem tocar no disco —\n * quem grava é o CLI (`bm-bi new`).\n */\n\nexport class ScaffoldError extends Error {}\n\nconst IDENTIFIER = /^[A-Za-z_$][\\w$]*$/\nconst LAYOUT_ID = /^[\\w-]+$/\n\nexport function checkModelName(name: string) {\n if (!IDENTIFIER.test(name))\n throw new ScaffoldError(\n `Nome de modelo inválido: \"${name}\" (use um identificador JS, ex.: analiseCusto)`\n )\n}\n\nexport function checkLayoutId(id: string) {\n if (!LAYOUT_ID.test(id))\n throw new ScaffoldError(`Id de layout inválido: \"${id}\" (só letras, números, _ e -)`)\n}\n\n/** Modelo vazio, pronto para montar pelo diagrama da aba Modelo do devtools. */\nexport function modelFileCode(name: string) {\n checkModelName(name)\n return toModelCode({ name, tables: {}, dimensions: {}, measures: {} }).replace(\n 'export const',\n '/** Monte tabelas, relacionamentos, dimensões e medidas pela aba Modelo do devtools. */\\nexport const'\n )\n}\n\n/** Layout vazio, pronto para montar pela aba Layout do devtools. */\nexport function layoutFileCode(id: string) {\n checkLayoutId(id)\n const layout: ChartsLayout = { id, filters: [], charts: [] }\n return `${JSON.stringify(layout, null, 2)}\\n`\n}\n\n/** `models/index.ts` novo, com só este modelo. */\nexport function modelIndexCode(name: string) {\n checkModelName(name)\n return [\n `import { ${name} } from './${name}'`,\n '',\n '/** Modelos expostos pelo endpoint do dashboard (`runQuery(dashboardModels, ...)`). */',\n 'export const dashboardModels = {',\n ` ${name}`,\n '}',\n ''\n ].join('\\n')\n}\n\n/**\n * Acrescenta o modelo ao `models/index.ts` existente: o import depois do último import e o nome no\n * primeiro `export const x = { ... }`. Devolve `null` se o modelo já está registrado.\n */\nexport function registerModel(source: string, name: string): string | null {\n checkModelName(name)\n const importPath = new RegExp(`from\\\\s+['\"]\\\\./${name}(\\\\.ts)?['\"]`)\n if (importPath.test(source)) return null\n\n const object = /export\\s+const\\s+\\w+(?:\\s*:[^=]+)?\\s*=\\s*\\{/.exec(source)\n if (!object)\n throw new ScaffoldError(\n 'Não encontrei `export const <nome> = { ... }` no models/index.ts — registre o modelo à mão.'\n )\n const open = object.index + object[0].length - 1\n let depth = 0\n let close = -1\n for (let i = open; i < source.length; i++) {\n if (source[i] === '{') depth++\n else if (source[i] === '}' && --depth === 0) {\n close = i\n break\n }\n }\n if (close === -1) throw new ScaffoldError('Objeto de modelos do models/index.ts sem `}`.')\n\n const body = source.slice(open + 1, close)\n const items = body.trimEnd()\n let newBody: string\n if (!items.trim()) newBody = `\\n ${name}\\n`\n else if (!body.includes('\\n')) newBody = ` ${items.trim().replace(/,$/, '')}, ${name} `\n else newBody = `${items.endsWith(',') ? items : `${items},`}\\n ${name}\\n`\n let result = `${source.slice(0, open + 1)}${newBody}${source.slice(close)}`\n\n // Import logo depois do último import do arquivo (ou no topo, se não houver)\n const line = `import { ${name} } from './${name}'\\n`\n const imports = [...result.matchAll(/^import[\\s\\S]*?from\\s+['\"][^'\"]+['\"];?[^\\n]*\\n/gm)]\n const last = imports.at(-1)\n const at = last ? last.index + last[0].length : 0\n result = `${result.slice(0, at)}${line}${last ? '' : '\\n'}${result.slice(at)}`\n return result\n}\n\n// --- página ---\n\n/** `analiseCusto` → `analise-custo` (rota da página). */\nexport const kebabCase = (text: string) =>\n text\n .replace(/([a-z0-9])([A-Z])/g, '$1-$2')\n .replace(/[_\\s]+/g, '-')\n .toLowerCase()\n\n/** `analiseCusto` → `Analise Custo` (título padrão da página). */\nexport const titleCase = (text: string) =>\n kebabCase(text)\n .split('-')\n .filter(Boolean)\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1))\n .join(' ')\n\n/** `analiseCusto` → `AnaliseCustoPage` (nome do componente). */\nexport const pageComponentName = (text: string) => `${titleCase(text).replaceAll(' ', '')}Page`\n\nexport type PageOptions = {\n model: string\n /** Import do JSON do layout, relativo à página (ex.: `../../dashboard/layouts/vendas.json`). */\n layoutImport: string\n title: string\n /** Endpoint do `runQuery` (padrão `/api/dashboard/query`). */\n endpoint: string\n /** App Router do Next: a página vira client component (`'use client'`). */\n clientDirective?: boolean\n /** Import do registro de tipos de gráfico do app (`export const charts`), relativo à página. */\n chartsImport?: string\n}\n\n/**\n * Valores trocados num template de página do app (`page.template`): o template é a página de um\n * dashboard com esses marcadores no lugar do que muda de um para outro.\n */\nexport const pageTemplateValues = (options: PageOptions): Record<string, string> => ({\n __MODEL__: options.model,\n __LAYOUT__: options.layoutImport,\n __TITLE__: options.title,\n __COMPONENT__: pageComponentName(options.model),\n __ENDPOINT__: options.endpoint\n})\n\nexport function fillPageTemplate(template: string, options: PageOptions) {\n let content = template\n for (const [marker, value] of Object.entries(pageTemplateValues(options)))\n content = content.replaceAll(marker, value)\n return content\n}\n\n/** Página padrão: provider, filtros e gráficos do layout, consultando o endpoint com `fetch`. */\nexport function pageFileCode(options: PageOptions) {\n const { model, layoutImport, title, endpoint, clientDirective, chartsImport } = options\n if (endpoint.includes(\"'\")) throw new ScaffoldError(`Endpoint inválido: ${endpoint}`)\n checkModelName(model)\n return [\n ...(clientDirective ? [\"'use client'\", ''] : []),\n 'import {',\n ' DashboardCanvas,',\n ' DashboardCharts,',\n ' type DashboardFetcher,',\n ' DashboardFilters,',\n ' DashboardProvider',\n \"} from '@bluemarble/bi/client'\",\n \"import type { ChartsLayout, DashboardResult } from '@bluemarble/bi/types'\",\n \"import { Box, Typography } from '@mui/material'\",\n ...(chartsImport ? [`import { charts } from '${chartsImport}'`] : []),\n `import layout from '${layoutImport}'`,\n '',\n '/**',\n ` * Dashboard do modelo \\`${model}\\`. Filtros, gráficos e o tamanho da tela vêm do layout JSON,`,\n ' * editável pela aba Layout do devtools (◆ BI, em desenvolvimento).',\n ' */',\n '',\n `const TITULO = ${literal(title)}`,\n 'const chartsLayout = layout as ChartsLayout',\n '',\n '// Fora do componente: referência estável, senão toda consulta refaria a cada render',\n 'const fetcher: DashboardFetcher = async (query) => {',\n ` const response = await fetch('${endpoint}', {`,\n \" method: 'POST',\",\n \" headers: { 'Content-Type': 'application/json' },\",\n ' body: JSON.stringify(query)',\n ' })',\n ' if (!response.ok) throw new Error(await response.text())',\n ' return (await response.json()) as DashboardResult',\n '}',\n '',\n `export default function ${pageComponentName(model)}() {`,\n ' return (',\n ` <DashboardProvider model=\"${model}\" fetcher={fetcher}${chartsImport ? ' charts={charts}' : ''}>`,\n ' <Box sx={{ p: 2 }}>',\n ' {/* Com `canvas` no JSON, tela de tamanho fixo que escala; sem, responsivo */}',\n ' <DashboardCanvas layout={chartsLayout}>',\n \" <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>\",\n ' <Typography component=\"h1\" variant=\"h5\">',\n ' {TITULO}',\n ' </Typography>',\n ' <DashboardFilters layout={chartsLayout} />',\n ' <DashboardCharts layout={chartsLayout} />',\n ' </Box>',\n ' </DashboardCanvas>',\n ' </Box>',\n ' </DashboardProvider>',\n ' )',\n '}',\n ''\n ].join('\\n')\n}\n\n/**\n * Extensão das páginas do Pages Router pelo `pageExtensions` do `next.config` (ex.: `page.tsx` no\n * `custo`). Sem a opção (ou sem config), `tsx`.\n */\nexport function pageExtension(nextConfig: string | undefined) {\n const list = /pageExtensions\\s*:\\s*\\[([^\\]]*)\\]/.exec(nextConfig ?? '')?.[1]\n const extensions = [...(list ?? '').matchAll(/['\"]([^'\"]+)['\"]/g)].map((match) => match[1])\n return extensions.find((extension) => extension?.endsWith('tsx')) ?? 'tsx'\n}\n","#!/usr/bin/env node\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'\nimport { basename, dirname, join, relative, sep } from 'node:path'\nimport { parseArgs } from 'node:util'\nimport {\n checkLayoutId,\n checkModelName,\n fillPageTemplate,\n kebabCase,\n layoutFileCode,\n modelFileCode,\n modelIndexCode,\n type PageOptions,\n pageExtension,\n pageFileCode,\n registerModel,\n ScaffoldError,\n titleCase\n} from './scaffold'\n\nconst HELP = `Uso: bm-bi new <nome> [opções]\n\nCria um dashboard vazio: o modelo (models/<nome>.ts, registrado em models/index.ts), o layout\n(layouts/<id>.json) e a página. Depois é montar tudo pelas abas Modelo e Layout do devtools.\n\nA página vai para o router do Next que existir: src/pages (ou pages) →\ndashboard/<id-em-kebab>/index.<ext>, com a extensão do pageExtensions do next.config; src/app (ou\napp) → dashboard/<id-em-kebab>/page.tsx, com 'use client'. Com <dir>/page.template, a página sai\ndele, trocando __MODEL__, __LAYOUT__ (import do JSON), __TITLE__, __COMPONENT__ e __ENDPOINT__.\n\nOpções:\n --id <id> id do layout (padrão: o nome do modelo)\n --title <texto> título da página (padrão: o id em palavras)\n --dir <pasta> pasta dos dashboards (padrão: src/dashboard)\n --models <pasta> pasta dos modelos (padrão: <dir>/models)\n --layouts <pasta> pasta dos layouts (padrão: <dir>/layouts)\n --page <arquivo> arquivo da página (padrão: pelo router do Next, ver acima)\n --skip-page não cria a página\n --template <arq.> template da página (padrão: <dir>/page.template, se existir)\n --endpoint <url> endpoint do runQuery na página padrão (padrão: /api/dashboard/query)\n -h, --help esta ajuda\n`\n\nconst exists = (path: string) => existsSync(path)\n\n/** Caminho de import de `to` a partir do arquivo `from` (sempre com `./` ou `../` e `/`). */\nfunction importPath(from: string, to: string) {\n const path = relative(dirname(from), to).split(sep).join('/')\n return path.startsWith('.') ? path : `./${path}`\n}\n\n/** Página pelo router do Next que existir no app (Pages primeiro, como o `custo`). */\nfunction defaultPagePath(cwd: string, slug: string) {\n for (const pages of ['src/pages', 'pages']) {\n if (!exists(join(cwd, pages))) continue\n const config = ['next.config.js', 'next.config.mjs', 'next.config.ts', 'next.config.cjs']\n .map((file) => join(cwd, file))\n .find(exists)\n const extension = pageExtension(config && readFileSync(config, 'utf8'))\n return join(cwd, pages, 'dashboard', slug, `index.${extension}`)\n }\n for (const app of ['src/app', 'app'])\n if (exists(join(cwd, app))) return join(cwd, app, 'dashboard', slug, 'page.tsx')\n return undefined\n}\n\n/** `export const charts` em <dir>/charts: os tipos de gráfico do app entram no provider. */\nfunction chartsIndex(dir: string) {\n return ['index.ts', 'index.tsx']\n .map((file) => join(dir, 'charts', file))\n .find((path) => exists(path) && /export\\s+const\\s+charts\\b/.test(readFileSync(path, 'utf8')))\n}\n\nfunction main(argv: string[]) {\n const { values, positionals } = parseArgs({\n args: argv,\n allowPositionals: true,\n options: {\n id: { type: 'string' },\n dir: { type: 'string', default: 'src/dashboard' },\n models: { type: 'string' },\n layouts: { type: 'string' },\n title: { type: 'string' },\n page: { type: 'string' },\n 'skip-page': { type: 'boolean' },\n template: { type: 'string' },\n endpoint: { type: 'string', default: '/api/dashboard/query' },\n help: { type: 'boolean', short: 'h' }\n }\n })\n const [command, name, ...rest] = positionals\n if (values.help || !command) {\n process.stdout.write(HELP)\n return\n }\n if (command !== 'new') throw new ScaffoldError(`Comando desconhecido: ${command}\\n\\n${HELP}`)\n if (!name) throw new ScaffoldError(`Falta o nome do modelo.\\n\\n${HELP}`)\n if (rest.length) throw new ScaffoldError(`Argumentos a mais: ${rest.join(' ')}`)\n\n const id = values.id ?? name\n checkModelName(name)\n checkLayoutId(id)\n\n const cwd = process.cwd()\n const modelsDir = join(cwd, values.models ?? join(values.dir, 'models'))\n const layoutsDir = join(cwd, values.layouts ?? join(values.dir, 'layouts'))\n const modelPath = join(modelsDir, `${name}.ts`)\n const indexPath = join(modelsDir, 'index.ts')\n const layoutPath = join(layoutsDir, `${id}.json`)\n const dashboardDir = join(cwd, values.dir)\n const pagePath = values['skip-page']\n ? undefined\n : values.page\n ? join(cwd, values.page)\n : defaultPagePath(cwd, kebabCase(id))\n const templatePath = values.template\n ? join(cwd, values.template)\n : [join(dashboardDir, 'page.template')].find(exists)\n if (templatePath && !exists(templatePath))\n throw new ScaffoldError(`Template não encontrado: ${relative(cwd, templatePath)}`)\n\n // Confere tudo antes de escrever: ou cria o dashboard inteiro, ou nada\n for (const path of [modelPath, layoutPath, pagePath])\n if (path && existsSync(path)) throw new ScaffoldError(`Já existe: ${relative(cwd, path)}`)\n const index = existsSync(indexPath)\n ? registerModel(readFileSync(indexPath, 'utf8'), name)\n : modelIndexCode(name)\n let page: string | undefined\n if (pagePath) {\n const charts = chartsIndex(dashboardDir)\n const options: PageOptions = {\n model: name,\n layoutImport: importPath(pagePath, layoutPath),\n title: values.title ?? titleCase(id),\n endpoint: values.endpoint,\n // App Router: a página usa hooks, então é client component\n clientDirective:\n basename(pagePath).startsWith('page.') &&\n relative(cwd, pagePath).split(sep).includes('app'),\n chartsImport: charts && importPath(pagePath, dirname(charts))\n }\n page = templatePath\n ? fillPageTemplate(readFileSync(templatePath, 'utf8'), options)\n : pageFileCode(options)\n }\n\n const written: string[] = []\n const write = (path: string, content: string) => {\n mkdirSync(dirname(path), { recursive: true })\n writeFileSync(path, content)\n written.push(relative(cwd, path))\n }\n write(modelPath, modelFileCode(name))\n if (index !== null) write(indexPath, index)\n write(layoutPath, layoutFileCode(id))\n if (pagePath && page !== undefined) write(pagePath, page)\n\n process.stdout.write(\n [\n ...written.map((path) => `✓ ${path}`),\n ...(pagePath\n ? templatePath\n ? [` (página do template ${relative(cwd, templatePath)})`]\n : []\n : values['skip-page']\n ? []\n : [\n '',\n 'Página não criada: não achei src/pages nem src/app. Use --page <arquivo>, com o layout:',\n ` <DashboardProvider model=\"${name}\" fetcher={fetcher}>`,\n ' <DashboardFilters layout={layout} />',\n ' <DashboardCharts layout={layout} />',\n ' </DashboardProvider>'\n ]),\n ''\n ].join('\\n')\n )\n}\n\ntry {\n main(process.argv.slice(2))\n} catch (error) {\n if (!(error instanceof ScaffoldError) && !(error instanceof TypeError)) throw error\n process.stderr.write(`${error.message}\\n`)\n process.exitCode = 1\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"cli.js","names":[],"sources":["../src/cli/scaffold.ts","../src/cli/index.ts"],"sourcesContent":["import { literal, toModelCode } from '../codegen'\nimport type { ChartsLayout } from '../layout'\n\n/**\n * Arquivos de um dashboard novo: o modelo vazio (`models/<nome>.ts`), o registro dele no\n * `models/index.ts` e o layout vazio (`layouts/<id>.json`). Tudo como texto, sem tocar no disco —\n * quem grava é o CLI (`bm-bi new`).\n */\n\nexport class ScaffoldError extends Error {}\n\nconst IDENTIFIER = /^[A-Za-z_$][\\w$]*$/\nconst LAYOUT_ID = /^[\\w-]+$/\n\nexport function checkModelName(name: string) {\n if (!IDENTIFIER.test(name))\n throw new ScaffoldError(\n `Nome de modelo inválido: \"${name}\" (use um identificador JS, ex.: analiseCusto)`\n )\n}\n\nexport function checkLayoutId(id: string) {\n if (!LAYOUT_ID.test(id))\n throw new ScaffoldError(`Id de layout inválido: \"${id}\" (só letras, números, _ e -)`)\n}\n\n/** Modelo vazio, pronto para montar pelo diagrama da aba Modelo do devtools. */\nexport function modelFileCode(name: string) {\n checkModelName(name)\n return toModelCode({ name, tables: {}, dimensions: {}, measures: {} }).replace(\n 'export const',\n '/** Monte tabelas, relacionamentos, dimensões e medidas pela aba Modelo do devtools. */\\nexport const'\n )\n}\n\n/** Layout vazio, pronto para montar pela aba Layout do devtools. */\nexport function layoutFileCode(id: string) {\n checkLayoutId(id)\n const layout: ChartsLayout = { id, filters: [], charts: [] }\n return `${JSON.stringify(layout, null, 2)}\\n`\n}\n\n/** `models/index.ts` novo, com só este modelo. */\nexport function modelIndexCode(name: string) {\n checkModelName(name)\n return [\n `import { ${name} } from './${name}'`,\n '',\n '/** Modelos expostos pelo endpoint do dashboard (`runQuery(dashboardModels, ...)`). */',\n 'export const dashboardModels = {',\n ` ${name}`,\n '}',\n ''\n ].join('\\n')\n}\n\n/**\n * Acrescenta o modelo ao `models/index.ts` existente: o import depois do último import e o nome no\n * primeiro `export const x = { ... }`. Devolve `null` se o modelo já está registrado.\n */\nexport function registerModel(source: string, name: string): string | null {\n checkModelName(name)\n const importPath = new RegExp(`from\\\\s+['\"]\\\\./${name}(\\\\.ts)?['\"]`)\n if (importPath.test(source)) return null\n\n const object = /export\\s+const\\s+\\w+(?:\\s*:[^=]+)?\\s*=\\s*\\{/.exec(source)\n if (!object)\n throw new ScaffoldError(\n 'Não encontrei `export const <nome> = { ... }` no models/index.ts — registre o modelo à mão.'\n )\n const open = object.index + object[0].length - 1\n let depth = 0\n let close = -1\n for (let i = open; i < source.length; i++) {\n if (source[i] === '{') depth++\n else if (source[i] === '}' && --depth === 0) {\n close = i\n break\n }\n }\n if (close === -1) throw new ScaffoldError('Objeto de modelos do models/index.ts sem `}`.')\n\n const body = source.slice(open + 1, close)\n const items = body.trimEnd()\n let newBody: string\n if (!items.trim()) newBody = `\\n ${name}\\n`\n else if (!body.includes('\\n')) newBody = ` ${items.trim().replace(/,$/, '')}, ${name} `\n else newBody = `${items.endsWith(',') ? items : `${items},`}\\n ${name}\\n`\n let result = `${source.slice(0, open + 1)}${newBody}${source.slice(close)}`\n\n // Import logo depois do último import do arquivo (ou no topo, se não houver)\n const line = `import { ${name} } from './${name}'\\n`\n const imports = [...result.matchAll(/^import[\\s\\S]*?from\\s+['\"][^'\"]+['\"];?[^\\n]*\\n/gm)]\n const last = imports.at(-1)\n const at = last ? last.index + last[0].length : 0\n result = `${result.slice(0, at)}${line}${last ? '' : '\\n'}${result.slice(at)}`\n return result\n}\n\n// --- página ---\n\n/** `analiseCusto` → `analise-custo` (rota da página). */\nexport const kebabCase = (text: string) =>\n text\n .replace(/([a-z0-9])([A-Z])/g, '$1-$2')\n .replace(/[_\\s]+/g, '-')\n .toLowerCase()\n\n/** `analiseCusto` → `Analise Custo` (título padrão da página). */\nexport const titleCase = (text: string) =>\n kebabCase(text)\n .split('-')\n .filter(Boolean)\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1))\n .join(' ')\n\n/** `analiseCusto` → `AnaliseCustoPage` (nome do componente). */\nexport const pageComponentName = (text: string) => `${titleCase(text).replaceAll(' ', '')}Page`\n\nexport type PageOptions = {\n model: string\n /** Import do JSON do layout, relativo à página (ex.: `../../dashboard/layouts/vendas.json`). */\n layoutImport: string\n title: string\n /** Endpoint do `runQuery` (padrão `/api/dashboard/query`). */\n endpoint: string\n /** App Router do Next: a página vira client component (`'use client'`). */\n clientDirective?: boolean\n /** Import do registro de tipos de gráfico do app (`export const charts`), relativo à página. */\n chartsImport?: string\n}\n\n/**\n * Valores trocados num template de página do app (`page.template`): o template é a página de um\n * dashboard com esses marcadores no lugar do que muda de um para outro.\n */\nexport const pageTemplateValues = (options: PageOptions): Record<string, string> => ({\n __MODEL__: options.model,\n __LAYOUT__: options.layoutImport,\n __TITLE__: options.title,\n __COMPONENT__: pageComponentName(options.model),\n __ENDPOINT__: options.endpoint\n})\n\nexport function fillPageTemplate(template: string, options: PageOptions) {\n let content = template\n for (const [marker, value] of Object.entries(pageTemplateValues(options)))\n content = content.replaceAll(marker, value)\n return content\n}\n\n/** Página padrão: provider, filtros e gráficos do layout, consultando o endpoint com `fetch`. */\nexport function pageFileCode(options: PageOptions) {\n const { model, layoutImport, title, endpoint, clientDirective, chartsImport } = options\n if (endpoint.includes(\"'\")) throw new ScaffoldError(`Endpoint inválido: ${endpoint}`)\n checkModelName(model)\n return [\n ...(clientDirective ? [\"'use client'\", ''] : []),\n 'import {',\n ' DashboardCanvas,',\n ' DashboardCharts,',\n ' type DashboardFetcher,',\n ' DashboardFilters,',\n ' DashboardProvider',\n \"} from '@bluemarble/bi/client'\",\n \"import type { ChartsLayout, DashboardResult } from '@bluemarble/bi/types'\",\n \"import { Box, Typography } from '@mui/material'\",\n ...(chartsImport ? [`import { charts } from '${chartsImport}'`] : []),\n `import layout from '${layoutImport}'`,\n '',\n '/**',\n ` * Dashboard do modelo \\`${model}\\`. Filtros, gráficos e o tamanho da tela vêm do layout JSON,`,\n ' * editável pela aba Layout do devtools (◆ BI, em desenvolvimento).',\n ' */',\n '',\n `const TITULO = ${literal(title)}`,\n 'const chartsLayout = layout as ChartsLayout',\n '',\n '// Fora do componente: referência estável, senão toda consulta refaria a cada render',\n 'const fetcher: DashboardFetcher = async (query) => {',\n ` const response = await fetch('${endpoint}', {`,\n \" method: 'POST',\",\n \" headers: { 'Content-Type': 'application/json' },\",\n ' body: JSON.stringify(query)',\n ' })',\n ' if (!response.ok) throw new Error(await response.text())',\n ' return (await response.json()) as DashboardResult',\n '}',\n '',\n `export default function ${pageComponentName(model)}() {`,\n ' return (',\n ` <DashboardProvider model=\"${model}\" fetcher={fetcher}${chartsImport ? ' charts={charts}' : ''}>`,\n ' <Box sx={{ p: 2 }}>',\n ' {/* Com `canvas` no JSON, tela de tamanho fixo que escala; sem, responsivo */}',\n ' <DashboardCanvas layout={chartsLayout}>',\n \" <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>\",\n ' <Typography component=\"h1\" variant=\"h5\">',\n ' {TITULO}',\n ' </Typography>',\n ' <DashboardFilters layout={chartsLayout} />',\n ' <DashboardCharts layout={chartsLayout} />',\n ' </Box>',\n ' </DashboardCanvas>',\n ' </Box>',\n ' </DashboardProvider>',\n ' )',\n '}',\n ''\n ].join('\\n')\n}\n\n/**\n * Extensão das páginas do Pages Router pelo `pageExtensions` do `next.config` (ex.: `page.tsx` no\n * `custo`). Sem a opção (ou sem config), `tsx`.\n */\nexport function pageExtension(nextConfig: string | undefined) {\n return pageExtensions(nextConfig).find((extension) => extension.endsWith('tsx')) ?? 'tsx'\n}\n\n/** Itens do `pageExtensions` do `next.config` (lido como texto). */\nfunction pageExtensions(nextConfig: string | undefined) {\n const list = /pageExtensions\\s*:\\s*\\[([^\\]]*)\\]/.exec(nextConfig ?? '')?.[1]\n return [...(list ?? '').matchAll(/['\"]([^'\"]+)['\"]/g)].map((match) => match[1] as string)\n}\n\n// --- endpoint ---\n\nexport type ApiOptions = {\n /** Import do `models/index.ts` (o `dashboardModels`), relativo ao endpoint. */\n modelsImport: string\n /** Pastas do devtools, relativas à raiz do app (`process.cwd()` no servidor). */\n modelsDir: string\n layoutsDir: string\n snapshotsDir: string\n /** Import do registro de tipos de gráfico do servidor (`export const chartTypes`). */\n chartTypesImport?: string\n /** App Router do Next: `route.ts` com `POST(request)` em vez do handler do Pages Router. */\n appRouter?: boolean\n}\n\n/** Valores trocados num template de endpoint do app (`api.template`). */\nexport const apiTemplateValues = (options: ApiOptions): Record<string, string> => ({\n __MODELS__: options.modelsImport,\n __MODELS_DIR__: options.modelsDir,\n __LAYOUTS_DIR__: options.layoutsDir,\n __SNAPSHOTS_DIR__: options.snapshotsDir\n})\n\nexport function fillApiTemplate(template: string, options: ApiOptions) {\n let content = template\n for (const [marker, value] of Object.entries(apiTemplateValues(options)))\n content = content.replaceAll(marker, value)\n return content\n}\n\n/** `src/dashboard/models` → `'src', 'dashboard', 'models'` (argumentos do `path.join`). */\nconst pathArgs = (dir: string) =>\n dir\n .split(/[\\\\/]/)\n .filter(Boolean)\n .map((part) => literal(part))\n .join(', ')\n\n/**\n * Endpoint padrão do `runQuery` com Prisma: um só para todos os dashboards (só aceita os modelos do\n * `models/index.ts`), com o devtools ligado só em desenvolvimento.\n */\nexport function apiFileCode(options: ApiOptions) {\n const { modelsImport, chartTypesImport, appRouter } = options\n const dir = (path: string) => `path.join(process.cwd(), ${pathArgs(path)})`\n const run = (body: string, indent: string) =>\n [\n 'const result = await runQuery(',\n ` dashboardModels,`,\n ` ${body} as DashboardQuery,`,\n ' (sql, params) => prisma.$queryRawUnsafe<unknown[]>(sql, ...params),',\n ' { cacheTtlMs: CACHE_TTL_MS, devtools: DEVTOOLS }',\n ')'\n ].map((line) => `${indent}${line}`)\n return [\n \"import path from 'node:path'\",\n \"import { DashboardQueryError, runQuery } from '@bluemarble/bi/server'\",\n \"import type { DashboardQuery } from '@bluemarble/bi/types'\",\n \"import { PrismaClient } from '@prisma/client'\",\n ...(appRouter ? [] : [\"import type { NextApiRequest, NextApiResponse } from 'next'\"]),\n ...(chartTypesImport ? [`import { chartTypes } from '${chartTypesImport}'`] : []),\n `import { dashboardModels } from '${modelsImport}'`,\n '',\n '// Client próprio: rotas que chamam `prisma.$disconnect()` no client global derrubariam',\n '// consultas longas em andamento (\"Engine is not yet connected\").',\n 'const globalForPrisma = globalThis as unknown as { dashboardPrisma?: PrismaClient }',\n 'const prisma = globalForPrisma.dashboardPrisma ?? new PrismaClient()',\n \"if (process.env.NODE_ENV === 'development') globalForPrisma.dashboardPrisma = prisma\",\n '',\n 'const CACHE_TTL_MS = 5 * 60 * 1000',\n '',\n '// Devtools (debug, rascunho de modelo, salvar modelo/layout) aceita SQL vindo do browser: só em dev',\n \"const DEVTOOLS = process.env.NODE_ENV === 'development' && {\",\n ` modelsDir: ${dir(options.modelsDir)},`,\n ` snapshotsDir: ${dir(options.snapshotsDir)},`,\n ` layoutsDir: ${dir(options.layoutsDir)}${chartTypesImport ? ',' : ''}`,\n ...(chartTypesImport ? [' chartTypes'] : []),\n '}',\n '',\n '/**',\n ' * Endpoint genérico do `@bluemarble/bi`: recebe `{ model, dimensions, measures, filters }` e',\n ' * devolve as linhas agregadas. Só aceita nomes definidos nos modelos registrados.',\n ' */',\n ...(appRouter\n ? [\n 'export async function POST(request: Request) {',\n ' try {',\n ...run('(await request.json())', ' '),\n ' return Response.json(result)',\n ' } catch (error) {',\n ' if (error instanceof DashboardQueryError)',\n ' return Response.json({ error: error.message }, { status: 400 })',\n ' throw error',\n ' }',\n '}'\n ]\n : [\n 'export default async function handler(req: NextApiRequest, res: NextApiResponse) {',\n \" if (req.method !== 'POST') return res.status(405).setHeader('Allow', 'POST').end()\",\n ' try {',\n ...run('req.body', ' '),\n ' return res.json(result)',\n ' } catch (error) {',\n ' if (error instanceof DashboardQueryError)',\n ' return res.status(400).json({ error: error.message })',\n ' throw error',\n ' }',\n '}'\n ]),\n ''\n ].join('\\n')\n}\n\n/**\n * Extensão das rotas de API do Pages Router pelo `pageExtensions` do `next.config` (ex.: `api.ts`\n * no `custo`). Sem a opção (ou sem config), `ts`.\n */\nexport function apiExtension(nextConfig: string | undefined) {\n return pageExtensions(nextConfig).find((extension) => extension.endsWith('ts')) ?? 'ts'\n}\n","#!/usr/bin/env node\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'\nimport { basename, dirname, join, relative, sep } from 'node:path'\nimport { parseArgs } from 'node:util'\nimport {\n type ApiOptions,\n apiExtension,\n apiFileCode,\n checkLayoutId,\n checkModelName,\n fillApiTemplate,\n fillPageTemplate,\n kebabCase,\n layoutFileCode,\n modelFileCode,\n modelIndexCode,\n type PageOptions,\n pageExtension,\n pageFileCode,\n registerModel,\n ScaffoldError,\n titleCase\n} from './scaffold'\n\nconst HELP = `Uso: bm-bi new <nome> [opções]\n\nCria um dashboard vazio: o modelo (models/<nome>.ts, registrado em models/index.ts), o layout\n(layouts/<id>.json), a página e, se ainda não existir, o endpoint do runQuery (um só para todos os\ndashboards). Depois é montar tudo pelas abas Modelo e Layout do devtools.\n\nA página vai para o router do Next que existir: src/pages (ou pages) →\ndashboard/<id-em-kebab>/index.<ext>, com a extensão do pageExtensions do next.config; src/app (ou\napp) → dashboard/<id-em-kebab>/page.tsx, com 'use client'. Com <dir>/page.template, a página sai\ndele, trocando __MODEL__, __LAYOUT__ (import do JSON), __TITLE__, __COMPONENT__ e __ENDPOINT__.\n\nO endpoint vai para a rota do --endpoint no mesmo router: src/pages → api/dashboard/query.<ext> (ext\nde API do pageExtensions); src/app → api/dashboard/query/route.ts. O padrão usa Prisma\n($queryRawUnsafe); com <dir>/api.template, sai dele, trocando __MODELS__ (import do models/index),\n__MODELS_DIR__, __LAYOUTS_DIR__ e __SNAPSHOTS_DIR__ (pastas relativas à raiz do app).\n\nOpções:\n --id <id> id do layout (padrão: o nome do modelo)\n --title <texto> título da página (padrão: o id em palavras)\n --dir <pasta> pasta dos dashboards (padrão: src/dashboard)\n --models <pasta> pasta dos modelos (padrão: <dir>/models)\n --layouts <pasta> pasta dos layouts (padrão: <dir>/layouts)\n --page <arquivo> arquivo da página (padrão: pelo router do Next, ver acima)\n --skip-page não cria a página\n --template <arq.> template da página (padrão: <dir>/page.template, se existir)\n --endpoint <url> endpoint do runQuery (padrão: /api/dashboard/query)\n --api <arquivo> arquivo do endpoint (padrão: pela rota do --endpoint, ver acima)\n --skip-api não cria o endpoint\n --api-template <a.> template do endpoint (padrão: <dir>/api.template, se existir)\n --snapshots <pasta> pasta dos snapshots no endpoint (padrão: tests/dashboard/snapshots)\n -h, --help esta ajuda\n`\n\nconst exists = (path: string) => existsSync(path)\n\n/** Caminho de import de `to` a partir do arquivo `from` (sempre com `./` ou `../` e `/`). */\nfunction importPath(from: string, to: string) {\n const path = relative(dirname(from), to).split(sep).join('/')\n return path.startsWith('.') ? path : `./${path}`\n}\n\n/** Router do Next que existir no app (Pages primeiro, como o `custo`). */\nfunction nextRouter(cwd: string) {\n for (const pages of ['src/pages', 'pages']) {\n if (!exists(join(cwd, pages))) continue\n const config = ['next.config.js', 'next.config.mjs', 'next.config.ts', 'next.config.cjs']\n .map((file) => join(cwd, file))\n .find(exists)\n return { app: false, dir: join(cwd, pages), config: config && readFileSync(config, 'utf8') }\n }\n for (const app of ['src/app', 'app'])\n if (exists(join(cwd, app))) return { app: true, dir: join(cwd, app), config: undefined }\n return undefined\n}\n\ntype Router = ReturnType<typeof nextRouter>\n\nfunction defaultPagePath(router: Router, slug: string) {\n if (!router) return undefined\n return router.app\n ? join(router.dir, 'dashboard', slug, 'page.tsx')\n : join(router.dir, 'dashboard', slug, `index.${pageExtension(router.config)}`)\n}\n\n/** Endpoint na rota do `--endpoint` (ex.: `/api/dashboard/query`). */\nfunction defaultApiPath(router: Router, endpoint: string) {\n if (!router) return undefined\n if (!/^(\\/[\\w-]+)+$/.test(endpoint))\n throw new ScaffoldError(\n `Endpoint inválido para criar a rota: ${endpoint} (use --api ou --skip-api)`\n )\n const route = endpoint.slice(1).split('/')\n return router.app\n ? join(router.dir, ...route, 'route.ts')\n : join(router.dir, ...route.slice(0, -1), `${route.at(-1)}.${apiExtension(router.config)}`)\n}\n\n/** `export const charts` em <dir>/charts: os tipos de gráfico do app entram no provider. */\nfunction chartsIndex(dir: string) {\n return ['index.ts', 'index.tsx']\n .map((file) => join(dir, 'charts', file))\n .find((path) => exists(path) && /export\\s+const\\s+charts\\b/.test(readFileSync(path, 'utf8')))\n}\n\n/** `export const chartTypes` em <dir>/charts/types.ts: o devtools do endpoint valida o layout. */\nfunction chartTypesFile(dir: string) {\n return ['types.ts', 'types.tsx']\n .map((file) => join(dir, 'charts', file))\n .find(\n (path) => exists(path) && /export\\s+const\\s+chartTypes\\b/.test(readFileSync(path, 'utf8'))\n )\n}\n\n/** Caminho relativo à raiz do app, sempre com `/`. */\nconst fromRoot = (cwd: string, path: string) => relative(cwd, path).split(sep).join('/')\n\nfunction main(argv: string[]) {\n const { values, positionals } = parseArgs({\n args: argv,\n allowPositionals: true,\n options: {\n id: { type: 'string' },\n dir: { type: 'string', default: 'src/dashboard' },\n models: { type: 'string' },\n layouts: { type: 'string' },\n title: { type: 'string' },\n page: { type: 'string' },\n 'skip-page': { type: 'boolean' },\n template: { type: 'string' },\n endpoint: { type: 'string', default: '/api/dashboard/query' },\n api: { type: 'string' },\n 'skip-api': { type: 'boolean' },\n 'api-template': { type: 'string' },\n snapshots: { type: 'string', default: 'tests/dashboard/snapshots' },\n help: { type: 'boolean', short: 'h' }\n }\n })\n const [command, name, ...rest] = positionals\n if (values.help || !command) {\n process.stdout.write(HELP)\n return\n }\n if (command !== 'new') throw new ScaffoldError(`Comando desconhecido: ${command}\\n\\n${HELP}`)\n if (!name) throw new ScaffoldError(`Falta o nome do modelo.\\n\\n${HELP}`)\n if (rest.length) throw new ScaffoldError(`Argumentos a mais: ${rest.join(' ')}`)\n\n const id = values.id ?? name\n checkModelName(name)\n checkLayoutId(id)\n\n const cwd = process.cwd()\n const modelsDir = join(cwd, values.models ?? join(values.dir, 'models'))\n const layoutsDir = join(cwd, values.layouts ?? join(values.dir, 'layouts'))\n const modelPath = join(modelsDir, `${name}.ts`)\n const indexPath = join(modelsDir, 'index.ts')\n const layoutPath = join(layoutsDir, `${id}.json`)\n const dashboardDir = join(cwd, values.dir)\n const router = nextRouter(cwd)\n const pagePath = values['skip-page']\n ? undefined\n : values.page\n ? join(cwd, values.page)\n : defaultPagePath(router, kebabCase(id))\n const apiPath = values['skip-api']\n ? undefined\n : values.api\n ? join(cwd, values.api)\n : defaultApiPath(router, values.endpoint)\n // O endpoint é um só para todos os dashboards: se já existe, fica como está\n const apiExists = !!apiPath && exists(apiPath)\n const templatePath = values.template\n ? join(cwd, values.template)\n : [join(dashboardDir, 'page.template')].find(exists)\n const apiTemplatePath = values['api-template']\n ? join(cwd, values['api-template'])\n : [join(dashboardDir, 'api.template')].find(exists)\n for (const path of [templatePath, apiTemplatePath])\n if (path && !exists(path))\n throw new ScaffoldError(`Template não encontrado: ${relative(cwd, path)}`)\n\n // Confere tudo antes de escrever: ou cria o dashboard inteiro, ou nada\n for (const path of [modelPath, layoutPath, pagePath])\n if (path && existsSync(path)) throw new ScaffoldError(`Já existe: ${relative(cwd, path)}`)\n const index = existsSync(indexPath)\n ? registerModel(readFileSync(indexPath, 'utf8'), name)\n : modelIndexCode(name)\n let page: string | undefined\n if (pagePath) {\n const charts = chartsIndex(dashboardDir)\n const options: PageOptions = {\n model: name,\n layoutImport: importPath(pagePath, layoutPath),\n title: values.title ?? titleCase(id),\n endpoint: values.endpoint,\n // App Router: a página usa hooks, então é client component\n clientDirective:\n basename(pagePath).startsWith('page.') &&\n relative(cwd, pagePath).split(sep).includes('app'),\n chartsImport: charts && importPath(pagePath, dirname(charts))\n }\n page = templatePath\n ? fillPageTemplate(readFileSync(templatePath, 'utf8'), options)\n : pageFileCode(options)\n }\n let api: string | undefined\n if (apiPath && !apiExists) {\n const chartTypes = chartTypesFile(dashboardDir)\n const options: ApiOptions = {\n modelsImport: importPath(apiPath, modelsDir),\n modelsDir: fromRoot(cwd, modelsDir),\n layoutsDir: fromRoot(cwd, layoutsDir),\n snapshotsDir: fromRoot(cwd, join(cwd, values.snapshots)),\n chartTypesImport: chartTypes && importPath(apiPath, chartTypes.replace(/\\.tsx?$/, '')),\n appRouter: basename(apiPath).startsWith('route.')\n }\n api = apiTemplatePath\n ? fillApiTemplate(readFileSync(apiTemplatePath, 'utf8'), options)\n : apiFileCode(options)\n }\n\n const written: string[] = []\n const write = (path: string, content: string) => {\n mkdirSync(dirname(path), { recursive: true })\n writeFileSync(path, content)\n written.push(relative(cwd, path))\n }\n write(modelPath, modelFileCode(name))\n if (index !== null) write(indexPath, index)\n write(layoutPath, layoutFileCode(id))\n if (pagePath && page !== undefined) write(pagePath, page)\n if (apiPath && api !== undefined) write(apiPath, api)\n\n process.stdout.write(\n [\n ...written.map((path) => `✓ ${path}`),\n ...(pagePath\n ? templatePath\n ? [` (página do template ${relative(cwd, templatePath)})`]\n : []\n : values['skip-page']\n ? []\n : [\n '',\n 'Página não criada: não achei src/pages nem src/app. Use --page <arquivo>, com o layout:',\n ` <DashboardProvider model=\"${name}\" fetcher={fetcher}>`,\n ' <DashboardFilters layout={layout} />',\n ' <DashboardCharts layout={layout} />',\n ' </DashboardProvider>'\n ]),\n ...(apiPath\n ? apiExists\n ? [`· ${relative(cwd, apiPath)} (endpoint já existe, mantido)`]\n : apiTemplatePath\n ? [` (endpoint do template ${relative(cwd, apiTemplatePath)})`]\n : []\n : values['skip-api']\n ? []\n : ['', 'Endpoint não criado: não achei src/pages nem src/app. Use --api <arquivo>.']),\n ''\n ].join('\\n')\n )\n}\n\ntry {\n main(process.argv.slice(2))\n} catch (error) {\n if (!(error instanceof ScaffoldError) && !(error instanceof TypeError)) throw error\n process.stderr.write(`${error.message}\\n`)\n process.exitCode = 1\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { A as DashboardQuery, C as ColumnAlign, I as FilterValue, M as DashboardResult, N as DashboardRow, T as FormatRule, X as ModelMeta, Y as ModelDef, a as CardStyle, c as ChartNumberFormat, d as ChartTypes, f as ChartsLayout, j as DashboardQueryDebug, k as DashboardFilters$1, n as BarValuePosition, s as ChartDef, u as ChartTypeSpec, w as ColumnVerticalAlign } from "../layout-DsG-zRIO.js";
1
+ import { $ as ModelDef, C as BarValuePosition, E as CardStyle, L as DonutLegendPosition, M as ChartTypes, N as ChartsLayout, O as ChartDef, R as DonutValueContent, a as ValueRule, d as DashboardQueryDebug, et as ModelMeta, f as DashboardResult, g as FilterValue, i as FormatRule, j as ChartTypeSpec, k as ChartNumberFormat, l as DashboardFilters$1, n as ColumnAlign, p as DashboardRow, r as ColumnVerticalAlign, t as ColorScale, u as DashboardQuery } from "../formatRules-DxA9vMvz.js";
2
2
  import { ComponentType, MouseEvent, ReactNode } from "react";
3
3
  //#region src/client/BarChart.d.ts
4
4
  type BarChartProps = {
@@ -44,6 +44,13 @@ type BarChartProps = {
44
44
  valueSize?: number;
45
45
  /** Empilhado: total da pilha na ponta da barra. */
46
46
  showTotal?: boolean;
47
+ /** Só mostra o valor (e o total) que atende à regra, ex.: `{ operator: '!=', value: 0 }`. */
48
+ valueRule?: ValueRule;
49
+ /**
50
+ * Com `clickToFilter`: o filtro da dimensão (e da série) esconde o resto em vez de só apagá-lo
51
+ * (a consulta passa a respeitar o próprio filtro). Padrão: `false` (apaga).
52
+ */
53
+ hideUnselected?: boolean;
47
54
  /**
48
55
  * Cor por série: `{ medida: '#rrggbb' }` ou, com `series`, `{ valor: '#rrggbb' }` (também
49
56
  * "(em branco)" e "Outros"). O que faltar segue a paleta.
@@ -64,7 +71,7 @@ type BarChartProps = {
64
71
  * Com `series`, uma medida quebrada pelos valores de outra dimensão:
65
72
  * `<BarChart dimension="mes" series="tipo_descarga" measures={['volume']} stacked />`.
66
73
  */
67
- declare function BarChart({ dimension, measures: measureList, series, formatSeries, maxSeries, orientation, stacked, sort, limit, formatDimension, hideEmpty, showEmpty, showValues, valuePosition, valueColor, valueSize, showTotal, colors, clickToFilter, height, labels, formats: formatsByMeasure, queryLabel }: BarChartProps): import("react").JSX.Element;
74
+ declare function BarChart({ dimension, measures: measureList, series, formatSeries, maxSeries, orientation, stacked, sort, limit, formatDimension, hideEmpty, showEmpty, showValues, valuePosition, valueColor, valueSize, showTotal, valueRule, hideUnselected, colors, clickToFilter, height, labels, formats: formatsByMeasure, queryLabel }: BarChartProps): import("react").JSX.Element;
68
75
  //#endregion
69
76
  //#region src/client/ComboChart.d.ts
70
77
  type ComboChartProps = {
@@ -199,9 +206,30 @@ type DashboardChartsProps = {
199
206
  formatters?: Record<string, (value: FilterValue) => string>;
200
207
  /** Moldura de cada gráfico (título + conteúdo). */
201
208
  panel?: ComponentType<ChartPanelProps>;
202
- /** Espaço entre os cards (unidades do `theme.spacing`); o `cards.gap` do layout (px) tem prioridade. */
209
+ /** Espaço entre os cards (unidades do `theme.spacing`); `cards.rowGap`/`columnGap` do layout (px) têm prioridade. */
203
210
  gap?: number;
204
211
  };
212
+ /**
213
+ * `sx` da moldura de um card com o estilo do layout (`card` do `panel`). Só traz o que o layout
214
+ * definiu, para um `panel` do app espalhar por cima do visual dele:
215
+ * `<Paper sx={{ p: 2, borderRadius: 0, ...chartCardSx(card) }}>`.
216
+ */
217
+ declare function chartCardSx(card: CardStyle): {
218
+ boxSizing: 'border-box';
219
+ p?: string | undefined;
220
+ bgcolor?: string | undefined;
221
+ borderWidth?: string | undefined;
222
+ borderStyle?: string | undefined;
223
+ borderColor?: string | undefined;
224
+ borderRadius?: string | undefined;
225
+ boxShadow?: number | undefined;
226
+ };
227
+ /** `sx` do título de um card com o estilo do layout (idem `chartCardSx`). */
228
+ declare function chartTitleSx(card: CardStyle): Partial<{
229
+ color: string | undefined;
230
+ fontSize: string | undefined;
231
+ textAlign: "center" | "left" | "right" | undefined;
232
+ }>;
205
233
  /**
206
234
  * Renderiza os gráficos de um layout declarativo num grid de 12 colunas (no celular, um por linha;
207
235
  * dentro de um `<DashboardCanvas>` de tamanho fixo, sempre as 12). Em desenvolvimento, a aba
@@ -437,6 +465,11 @@ type DataTableProps = {
437
465
  * uma dimensão/medida (da tabela, de fora dela ou `%:medida`).
438
466
  */
439
467
  formatRules?: FormatRule[];
468
+ /**
469
+ * Escalas de cor (gradiente): o fundo ou o texto de uma coluna numérica vai de `minColor` a
470
+ * `color` conforme o valor (ex.: 0 a 100). As regras de `formatRules` ficam por cima.
471
+ */
472
+ colorScales?: ColorScale[];
440
473
  /** Bordas entre as células (grade). Padrão: só a linha embaixo de cada linha. */
441
474
  cellBorders?: boolean;
442
475
  /** Cor das bordas da grade. Padrão: `#e0e0e0`. */
@@ -455,7 +488,7 @@ type DataTableProps = {
455
488
  * Tabela de medidas por dimensões, com total e ordenação pelo cabeçalho. Ex.:
456
489
  * `<DataTable dimensions={['navio']} measures={['volTotal', 'custoTotal', 'custoTon']} />`.
457
490
  */
458
- declare function DataTable({ dimensions, measures, percentOf, formatDimensions, showTotal, sortBy, sortDirection, clickToFilter, height, labels, formats: formatsByMeasure, percentDecimals, columnOrder, formatRules, cellBorders, borderColor, align, verticalAlign, queryLabel }: DataTableProps): import("react").JSX.Element;
491
+ declare function DataTable({ dimensions, measures, percentOf, formatDimensions, showTotal, sortBy, sortDirection, clickToFilter, height, labels, formats: formatsByMeasure, percentDecimals, columnOrder, formatRules, colorScales, cellBorders, borderColor, align, verticalAlign, queryLabel }: DataTableProps): import("react").JSX.Element;
459
492
  //#endregion
460
493
  //#region src/client/DonutChart.d.ts
461
494
  type DonutChartProps = {
@@ -474,14 +507,23 @@ type DonutChartProps = {
474
507
  formats?: Record<string, ChartNumberFormat>;
475
508
  /** Casas decimais do percentual. Padrão: 1. */
476
509
  percentDecimals?: number;
510
+ /** Lado da legenda. Padrão: `right` (nome, % e valor; em cima/embaixo, só o nome, em linha). */
511
+ legendPosition?: DonutLegendPosition;
512
+ /** Valor de cada fatia do lado de fora da rosca. */
513
+ showValues?: boolean;
514
+ /** O que o valor de fora mostra. Padrão: `both` (`44.981 (51%)`). */
515
+ valueContent?: DonutValueContent;
516
+ /** Total no furo da rosca. Padrão: `true`. */
517
+ showTotal?: boolean;
477
518
  /** Nome da consulta no devtools. */
478
519
  queryLabel?: string;
479
520
  };
480
521
  /**
481
- * Rosca de uma medida por uma dimensão, com o total no centro e a legenda ao lado (nome, % e valor).
522
+ * Rosca de uma medida por uma dimensão, com o total no centro e a legenda ao lado (nome, % e valor)
523
+ * ou em cima/embaixo; `showValues` põe o valor de cada fatia do lado de fora.
482
524
  * Ex.: `<DonutChart dimension="operacao_status" measure="custoTotal" />`.
483
525
  */
484
- declare function DonutChart({ dimension, measure, maxSlices, formatDimension, clickToFilter, height, labels, formats, percentDecimals, queryLabel }: DonutChartProps): import("react").JSX.Element;
526
+ declare function DonutChart({ dimension, measure, maxSlices, formatDimension, clickToFilter, height, labels, formats, percentDecimals, legendPosition, showValues, valueContent, showTotal, queryLabel }: DonutChartProps): import("react").JSX.Element;
485
527
  //#endregion
486
528
  //#region src/client/FilterSelect.d.ts
487
529
  type FilterSelectProps = {
@@ -575,5 +617,5 @@ declare function useModelQuery({ dimensions, measures, ignoreFilters, label, ski
575
617
  };
576
618
  };
577
619
  //#endregion
578
- export { BarChart, type ChartComponentProps, type ChartComponentSpec, type ChartPanelProps, ComboChart, DashboardCanvas, DashboardCharts, type DashboardFetcher, DashboardFilters, DashboardProvider, DataTable, DonutChart, FilterSelect, INSPECT_ATTRIBUTE, KpiCard, Legend, ModelDictionary, OTHER_COLOR, SERIES_COLORS, TooltipBox, chartPlaceholder, fieldLabel, formatCurrency, formatMeasure, formatNumber, measureFormat, measureFormatter, numberOf, seriesColor, useClickFilter, useDashboard, useModelQuery };
620
+ export { BarChart, type ChartComponentProps, type ChartComponentSpec, type ChartPanelProps, ComboChart, DashboardCanvas, DashboardCharts, type DashboardFetcher, DashboardFilters, DashboardProvider, DataTable, DonutChart, FilterSelect, INSPECT_ATTRIBUTE, KpiCard, Legend, ModelDictionary, OTHER_COLOR, SERIES_COLORS, TooltipBox, chartCardSx, chartPlaceholder, chartTitleSx, fieldLabel, formatCurrency, formatMeasure, formatNumber, measureFormat, measureFormatter, numberOf, seriesColor, useClickFilter, useDashboard, useModelQuery };
579
621
  //# sourceMappingURL=index.d.ts.map
@@ -1,5 +1,5 @@
1
- import { T as rowRuleStyles, _ as tableColumns, f as isBuiltinChart, h as percentColumn, o as chartCardStyle, u as customChartProps } from "../layout-CBzYDoqE.js";
2
- import { S as layoutItemKey, _ as useClickFilter, a as formatCurrency, c as measureFormat, d as OTHER_COLOR, f as SERIES_COLORS, g as seriesColor, h as numberOf, i as fieldLabel, l as measureFormatter, m as chartPlaceholder, n as INSPECT_ATTRIBUTE, o as formatMeasure, p as TooltipBox, r as useModelQuery, s as formatNumber, t as ModelDictionary, u as Legend, v as DashboardProvider, x as LAYOUT_ITEM_ATTRIBUTE, y as useDashboard } from "../ModelDictionary-CyeYU6ZJ.js";
1
+ import { A as contrastText, M as rowRuleStyles, d as customChartProps, g as percentColumn, j as matchesValueRule, k as colorScaleStyles, o as cardsGap, p as isBuiltinChart, s as chartCardStyle, v as tableColumns } from "../layout-BS3nO6OT.js";
2
+ import { S as layoutItemKey, _ as useClickFilter, a as formatCurrency, c as measureFormat, d as OTHER_COLOR, f as SERIES_COLORS, g as seriesColor, h as numberOf, i as fieldLabel, l as measureFormatter, m as chartPlaceholder, n as INSPECT_ATTRIBUTE, o as formatMeasure, p as TooltipBox, r as useModelQuery, s as formatNumber, t as ModelDictionary, u as Legend, v as DashboardProvider, x as LAYOUT_ITEM_ATTRIBUTE, y as useDashboard } from "../ModelDictionary-Cw4sa8tW.js";
3
3
  import { Box, Checkbox, CircularProgress, FormControl, InputLabel, ListItemText, MenuItem, OutlinedInput, Paper, Select, Table, TableBody, TableCell, TableHead, TableRow, TableSortLabel, Typography } from "@mui/material";
4
4
  import { Bar, BarChart as BarChart$1, CartesianGrid, Cell, ComposedChart, LabelList, Line, Pie, PieChart, ResponsiveContainer, Tooltip as Tooltip$1, XAxis, YAxis } from "recharts";
5
5
  import { createContext, useContext, useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from "react";
@@ -134,13 +134,14 @@ const ROW_HEIGHT = 28;
134
134
  * Com `series`, uma medida quebrada pelos valores de outra dimensão:
135
135
  * `<BarChart dimension="mes" series="tipo_descarga" measures={['volume']} stacked />`.
136
136
  */
137
- function BarChart({ dimension, measures: measureList, series, formatSeries = String, maxSeries = 6, orientation = "vertical", stacked = false, sort = "dimension", limit, formatDimension = String, hideEmpty = true, showEmpty = false, showValues = true, valuePosition = "outside", valueColor, valueSize = 12, showTotal = false, colors, clickToFilter = false, height = 360, labels, formats: formatsByMeasure, queryLabel }) {
137
+ function BarChart({ dimension, measures: measureList, series, formatSeries = String, maxSeries = 6, orientation = "vertical", stacked = false, sort = "dimension", limit, formatDimension = String, hideEmpty = true, showEmpty = false, showValues = true, valuePosition = "outside", valueColor, valueSize = 12, showTotal = false, valueRule, hideUnselected = false, colors, clickToFilter = false, height = 360, labels, formats: formatsByMeasure, queryLabel }) {
138
138
  const { meta, filters, setFilter } = useDashboard();
139
+ const dimming = clickToFilter && !hideUnselected;
139
140
  const measures = series ? measureList.slice(0, 1) : measureList;
140
141
  const { rows, isLoading, error, inspectProps } = useModelQuery({
141
142
  dimensions: series ? [dimension, series] : [dimension],
142
143
  measures,
143
- ignoreFilters: clickToFilter ? series ? [dimension, series] : [dimension] : void 0,
144
+ ignoreFilters: dimming ? series ? [dimension, series] : [dimension] : void 0,
144
145
  label: queryLabel ?? `BarChart · ${dimension}`
145
146
  });
146
147
  const domain = useModelQuery({
@@ -153,7 +154,7 @@ function BarChart({ dimension, measures: measureList, series, formatSeries = Str
153
154
  const table = meta?.dimensions[dimension]?.table;
154
155
  const categories = useModelQuery({
155
156
  dimensions: [dimension],
156
- ignoreFilters: Object.keys(filters).filter((name) => clickToFilter && name === dimension || !table || meta?.dimensions[name]?.table !== table),
157
+ ignoreFilters: Object.keys(filters).filter((name) => dimming && name === dimension || !table || meta?.dimensions[name]?.table !== table),
157
158
  label: `${queryLabel ?? `BarChart · ${dimension}`} · categorias`,
158
159
  skip: !showEmpty || !meta
159
160
  });
@@ -420,13 +421,13 @@ function BarChart({ dimension, measures: measureList, series, formatSeries = Str
420
421
  segmentLabels && /* @__PURE__ */ jsx(LabelList, {
421
422
  dataKey: bar.dataKey,
422
423
  position: position[valuePosition],
423
- formatter: (value) => stacked && !Number(value) ? "" : bar.format(value),
424
+ formatter: (value) => stacked && !Number(value) || !matchesValueRule(valueRule, Number(value)) ? "" : bar.format(value),
424
425
  style: labelStyle(valuePosition === "outside")
425
426
  }),
426
427
  totalLabel && index === bars.length - 1 && /* @__PURE__ */ jsx(LabelList, {
427
428
  dataKey: TOTAL_KEY,
428
429
  position: position.outside,
429
- formatter: (value) => (bars[0]?.format ?? String)(value),
430
+ formatter: (value) => matchesValueRule(valueRule, Number(value)) ? (bars[0]?.format ?? String)(value) : "",
430
431
  style: {
431
432
  ...labelStyle(true),
432
433
  ...valuePosition !== "outside" && { fill: "#333" },
@@ -818,6 +819,7 @@ function DashboardCanvas({ layout, children }) {
818
819
  //#region src/client/DataTable.tsx
819
820
  const EMPTY = [];
820
821
  const NO_RULES = [];
822
+ const NO_SCALES = [];
821
823
  function compare(a, b) {
822
824
  if (typeof a === "number" && typeof b === "number") return a - b;
823
825
  return String(a).localeCompare(String(b), "pt-BR", { numeric: true });
@@ -826,7 +828,7 @@ function compare(a, b) {
826
828
  * Tabela de medidas por dimensões, com total e ordenação pelo cabeçalho. Ex.:
827
829
  * `<DataTable dimensions={['navio']} measures={['volTotal', 'custoTotal', 'custoTon']} />`.
828
830
  */
829
- function DataTable({ dimensions = EMPTY, measures, percentOf = EMPTY, formatDimensions = {}, showTotal = true, sortBy, sortDirection = "desc", clickToFilter = false, height = 400, labels, formats: formatsByMeasure, percentDecimals = 1, columnOrder, formatRules = NO_RULES, cellBorders = false, borderColor = "#e0e0e0", align, verticalAlign, queryLabel }) {
831
+ function DataTable({ dimensions = EMPTY, measures, percentOf = EMPTY, formatDimensions = {}, showTotal = true, sortBy, sortDirection = "desc", clickToFilter = false, height = 400, labels, formats: formatsByMeasure, percentDecimals = 1, columnOrder, formatRules = NO_RULES, colorScales = NO_SCALES, cellBorders = false, borderColor = "#e0e0e0", align, verticalAlign, queryLabel }) {
830
832
  const { meta } = useDashboard();
831
833
  const name = queryLabel ?? `DataTable · ${dimensions.join(", ") || "total"}`;
832
834
  const firstDimension = dimensions[0];
@@ -888,11 +890,31 @@ function DataTable({ dimensions = EMPTY, measures, percentOf = EMPTY, formatDime
888
890
  return value === null || value === void 0 || !total ? null : Number(value) / total;
889
891
  };
890
892
  const columns = tableColumns([...dimensions, ...measures], percentOf.filter((measure) => !dimensions.includes(measure)), columnOrder);
891
- const ruleStyles = (row) => {
892
- if (!formatRules.length) return void 0;
893
+ const evaluate = (row) => {
894
+ if (!percentOf.length) return row;
893
895
  const evaluated = { ...row };
894
896
  for (const measure of percentOf) evaluated[percentColumn(measure)] = percentOfRow(row, measure);
895
- return rowRuleStyles(formatRules, evaluated, columns);
897
+ return evaluated;
898
+ };
899
+ const scaleStyles = colorScales.length ? colorScaleStyles(colorScales, rows.map(evaluate)) : null;
900
+ const ruleStyles = (row) => {
901
+ if (!formatRules.length && !scaleStyles) return void 0;
902
+ const evaluated = evaluate(row);
903
+ const rules = formatRules.length ? rowRuleStyles(formatRules, evaluated, columns) : {};
904
+ if (!scaleStyles) return rules;
905
+ const scaled = scaleStyles(evaluated);
906
+ return Object.fromEntries(columns.map((column) => {
907
+ const [scale, rule] = [scaled[column], rules[column]];
908
+ const style = {
909
+ ...scale,
910
+ ...rule
911
+ };
912
+ if (scale?.background && !rule?.background && !style.color) {
913
+ const text = contrastText(scale.background);
914
+ if (text) style.color = text;
915
+ }
916
+ return [column, style];
917
+ }));
896
918
  };
897
919
  const ruleSx = (style) => style && {
898
920
  ...style.color && { color: style.color },
@@ -1021,11 +1043,22 @@ function DataTable({ dimensions = EMPTY, measures, percentOf = EMPTY, formatDime
1021
1043
  //#endregion
1022
1044
  //#region src/client/DonutChart.tsx
1023
1045
  const OTHERS = "Outros";
1046
+ /** Fatia abaixo dessa fração fica sem valor de fora (os rótulos se atropelariam); tem o tooltip. */
1047
+ const MIN_VALUE_SHARE = .02;
1048
+ const RADIAN = Math.PI / 180;
1049
+ /** Direção da caixa (rosca + legenda) conforme o lado da legenda. */
1050
+ const LEGEND_DIRECTION = {
1051
+ right: "row",
1052
+ left: "row-reverse",
1053
+ top: "column-reverse",
1054
+ bottom: "column"
1055
+ };
1024
1056
  /**
1025
- * Rosca de uma medida por uma dimensão, com o total no centro e a legenda ao lado (nome, % e valor).
1057
+ * Rosca de uma medida por uma dimensão, com o total no centro e a legenda ao lado (nome, % e valor)
1058
+ * ou em cima/embaixo; `showValues` põe o valor de cada fatia do lado de fora.
1026
1059
  * Ex.: `<DonutChart dimension="operacao_status" measure="custoTotal" />`.
1027
1060
  */
1028
- function DonutChart({ dimension, measure, maxSlices = 6, formatDimension = String, clickToFilter = false, height = 300, labels, formats, percentDecimals = 1, queryLabel }) {
1061
+ function DonutChart({ dimension, measure, maxSlices = 6, formatDimension = String, clickToFilter = false, height = 300, labels, formats, percentDecimals = 1, legendPosition = "right", showValues = false, valueContent = "both", showTotal = true, queryLabel }) {
1029
1062
  const { meta } = useDashboard();
1030
1063
  const { rows, isLoading, error, inspectProps } = useModelQuery({
1031
1064
  dimensions: [dimension],
@@ -1068,6 +1101,26 @@ function DonutChart({ dimension, measure, maxSlices = 6, formatDimension = Strin
1068
1101
  if (slice?.value !== null && slice?.value !== void 0) filter.toggle(slice.value, event);
1069
1102
  };
1070
1103
  const dimmed = (slice) => filter.selected.length > 0 && (slice.value === null || filter.isDimmed(slice.value));
1104
+ const valueText = (amount) => valueContent === "value" ? format(amount) : valueContent === "percent" ? percent(amount) : `${format(amount)} (${percent(amount)})`;
1105
+ const SliceValue = ({ cx, cy, midAngle, outerRadius, payload }) => {
1106
+ const slice = payload;
1107
+ if (slice.amount / total < MIN_VALUE_SHARE) return null;
1108
+ const radius = outerRadius + 10;
1109
+ const cos = Math.cos(-midAngle * RADIAN);
1110
+ const x = cx + radius * cos;
1111
+ const y = cy + radius * Math.sin(-midAngle * RADIAN);
1112
+ return /* @__PURE__ */ jsx("text", {
1113
+ x,
1114
+ y,
1115
+ textAnchor: Math.abs(cos) < .15 ? "middle" : cos > 0 ? "start" : "end",
1116
+ dominantBaseline: "central",
1117
+ fontSize: 12,
1118
+ fill: "#333",
1119
+ opacity: dimmed(slice) ? .45 : 1,
1120
+ children: valueText(slice.amount)
1121
+ });
1122
+ };
1123
+ const side = legendPosition === "left" || legendPosition === "right";
1071
1124
  const SliceTooltip = ({ active, payload }) => {
1072
1125
  if (!active || !payload?.length) return null;
1073
1126
  const slice = payload[0].payload;
@@ -1093,17 +1146,21 @@ function DonutChart({ dimension, measure, maxSlices = 6, formatDimension = Strin
1093
1146
  sx: {
1094
1147
  height,
1095
1148
  display: "flex",
1096
- alignItems: "center",
1097
- gap: 2,
1149
+ flexDirection: LEGEND_DIRECTION[legendPosition],
1150
+ alignItems: side ? "center" : "stretch",
1151
+ gap: side ? 2 : 1,
1098
1152
  opacity: isLoading ? .5 : 1,
1099
1153
  transition: "opacity .2s"
1100
1154
  },
1101
1155
  children: [/* @__PURE__ */ jsxs(Box, {
1102
1156
  sx: {
1103
1157
  position: "relative",
1104
- height: "100%",
1105
- flex: "0 0 50%",
1106
- minWidth: 140
1158
+ minWidth: 140,
1159
+ minHeight: 0,
1160
+ ...side ? {
1161
+ height: "100%",
1162
+ flex: showValues ? "1 1 0" : "0 0 50%"
1163
+ } : { flex: "1 1 0" }
1107
1164
  },
1108
1165
  children: [/* @__PURE__ */ jsx(ResponsiveContainer, {
1109
1166
  width: "100%",
@@ -1112,13 +1169,15 @@ function DonutChart({ dimension, measure, maxSlices = 6, formatDimension = Strin
1112
1169
  data: slices,
1113
1170
  dataKey: "amount",
1114
1171
  nameKey: "label",
1115
- innerRadius: "62%",
1116
- outerRadius: "92%",
1172
+ innerRadius: showValues ? "46%" : "62%",
1173
+ outerRadius: showValues ? "68%" : "92%",
1117
1174
  startAngle: 90,
1118
1175
  endAngle: -270,
1119
1176
  stroke: "#fff",
1120
1177
  strokeWidth: 2,
1121
1178
  isAnimationActive: false,
1179
+ label: showValues ? SliceValue : false,
1180
+ labelLine: false,
1122
1181
  onClick: (_, index, event) => select(slices[index], event),
1123
1182
  cursor: clickToFilter ? "pointer" : void 0,
1124
1183
  children: slices.map((slice) => /* @__PURE__ */ jsx(Cell, {
@@ -1126,7 +1185,7 @@ function DonutChart({ dimension, measure, maxSlices = 6, formatDimension = Strin
1126
1185
  fillOpacity: dimmed(slice) ? .35 : 1
1127
1186
  }, slice.key))
1128
1187
  })] })
1129
- }), /* @__PURE__ */ jsxs(Box, {
1188
+ }), showTotal && /* @__PURE__ */ jsxs(Box, {
1130
1189
  sx: {
1131
1190
  position: "absolute",
1132
1191
  inset: 0,
@@ -1152,14 +1211,15 @@ function DonutChart({ dimension, measure, maxSlices = 6, formatDimension = Strin
1152
1211
  children: format(total)
1153
1212
  })]
1154
1213
  })]
1155
- }), /* @__PURE__ */ jsx(Box, {
1214
+ }), side ? /* @__PURE__ */ jsx(Box, {
1156
1215
  component: "ul",
1157
1216
  sx: {
1158
1217
  listStyle: "none",
1159
1218
  m: 0,
1160
1219
  p: 0,
1161
1220
  minWidth: 0,
1162
- flex: 1,
1221
+ flex: showValues ? "0 1 auto" : 1,
1222
+ maxWidth: showValues ? "45%" : void 0,
1163
1223
  overflow: "auto",
1164
1224
  maxHeight: "100%"
1165
1225
  },
@@ -1210,6 +1270,43 @@ function DonutChart({ dimension, measure, maxSlices = 6, formatDimension = Strin
1210
1270
  })
1211
1271
  ]
1212
1272
  }, slice.key))
1273
+ }) : /* @__PURE__ */ jsx(Box, {
1274
+ component: "ul",
1275
+ sx: {
1276
+ listStyle: "none",
1277
+ m: 0,
1278
+ p: 0,
1279
+ display: "flex",
1280
+ flexWrap: "wrap",
1281
+ justifyContent: "center",
1282
+ columnGap: 1.5,
1283
+ rowGap: .25,
1284
+ flex: "0 0 auto",
1285
+ maxHeight: "40%",
1286
+ overflow: "auto"
1287
+ },
1288
+ children: slices.map((slice) => /* @__PURE__ */ jsxs(Box, {
1289
+ component: "li",
1290
+ title: `${format(slice.amount)} · ${percent(slice.amount)}`,
1291
+ onClick: (event) => select(slice, event),
1292
+ sx: {
1293
+ display: "flex",
1294
+ alignItems: "center",
1295
+ gap: .5,
1296
+ fontSize: 13,
1297
+ color: "#333",
1298
+ whiteSpace: "nowrap",
1299
+ opacity: dimmed(slice) ? .45 : 1,
1300
+ cursor: clickToFilter && slice.value !== null ? "pointer" : void 0
1301
+ },
1302
+ children: [/* @__PURE__ */ jsx(Box, { sx: {
1303
+ width: 10,
1304
+ height: 10,
1305
+ borderRadius: "50%",
1306
+ bgcolor: slice.color,
1307
+ flex: "0 0 auto"
1308
+ } }), slice.label]
1309
+ }, slice.key))
1213
1310
  })]
1214
1311
  })
1215
1312
  });
@@ -1295,26 +1392,52 @@ function KpiCard({ measure, label, secondary, secondaryLabel, labels, formats, q
1295
1392
  }
1296
1393
  //#endregion
1297
1394
  //#region src/client/DashboardCharts.tsx
1395
+ const px = (value) => value === void 0 ? void 0 : `${value}px`;
1396
+ /** Tira as chaves `undefined`, para o estilo do layout só sobrepor o que foi definido. */
1397
+ const defined = (object) => Object.fromEntries(Object.entries(object).filter(([, value]) => value !== void 0));
1398
+ /**
1399
+ * `sx` da moldura de um card com o estilo do layout (`card` do `panel`). Só traz o que o layout
1400
+ * definiu, para um `panel` do app espalhar por cima do visual dele:
1401
+ * `<Paper sx={{ p: 2, borderRadius: 0, ...chartCardSx(card) }}>`.
1402
+ */
1403
+ function chartCardSx(card) {
1404
+ return {
1405
+ boxSizing: "border-box",
1406
+ ...defined({
1407
+ p: px(card.padding),
1408
+ bgcolor: card.background,
1409
+ borderWidth: px(card.borderWidth),
1410
+ borderStyle: card.borderWidth === void 0 ? void 0 : "solid",
1411
+ borderColor: card.borderColor,
1412
+ borderRadius: px(card.borderRadius),
1413
+ boxShadow: card.shadow
1414
+ })
1415
+ };
1416
+ }
1417
+ /** `sx` do título de um card com o estilo do layout (idem `chartCardSx`). */
1418
+ function chartTitleSx(card) {
1419
+ return defined({
1420
+ color: card.titleColor,
1421
+ fontSize: px(card.titleSize),
1422
+ textAlign: card.titleAlign
1423
+ });
1424
+ }
1298
1425
  function DefaultPanel({ title, card, children }) {
1299
- const px = (value) => value === void 0 ? void 0 : `${value}px`;
1300
1426
  return /* @__PURE__ */ jsxs(Paper, {
1301
- elevation: card.shadow ?? 0,
1427
+ elevation: 0,
1302
1428
  sx: {
1303
- p: px(card.padding) ?? 1.5,
1429
+ p: 1.5,
1304
1430
  minWidth: 0,
1305
1431
  height: "100%",
1306
- bgcolor: card.background,
1307
- border: `${card.borderWidth ?? 1}px solid`,
1308
- borderColor: card.borderColor ?? "divider",
1309
- borderRadius: px(card.borderRadius)
1432
+ border: "1px solid",
1433
+ borderColor: "divider",
1434
+ ...chartCardSx(card)
1310
1435
  },
1311
1436
  children: [title && /* @__PURE__ */ jsx(Typography, {
1312
1437
  sx: {
1313
1438
  fontWeight: 700,
1314
1439
  mb: 1,
1315
- color: card.titleColor,
1316
- fontSize: px(card.titleSize),
1317
- textAlign: card.titleAlign
1440
+ ...chartTitleSx(card)
1318
1441
  },
1319
1442
  children: title
1320
1443
  }), children]
@@ -1335,10 +1458,12 @@ function DashboardCharts({ layout, formatters = {}, panel: Panel = DefaultPanel,
1335
1458
  const { charts } = current;
1336
1459
  const fixed = useFixedCanvas();
1337
1460
  const columns = `repeat(12, minmax(0, 1fr))`;
1461
+ const space = cardsGap(current.cards);
1338
1462
  return /* @__PURE__ */ jsx(Box, {
1339
1463
  sx: {
1340
1464
  display: "grid",
1341
- gap: current.cards?.gap === void 0 ? gap : `${current.cards.gap}px`,
1465
+ rowGap: px(space.row) ?? gap,
1466
+ columnGap: px(space.column) ?? gap,
1342
1467
  gridTemplateColumns: fixed ? columns : {
1343
1468
  xs: "1fr",
1344
1469
  lg: columns
@@ -1520,6 +1645,6 @@ function DashboardFilters({ layout, formatters = {}, gap = 2 }) {
1520
1645
  });
1521
1646
  }
1522
1647
  //#endregion
1523
- export { BarChart, ComboChart, DashboardCanvas, DashboardCharts, DashboardFilters, DashboardProvider, DataTable, DonutChart, FilterSelect, INSPECT_ATTRIBUTE, KpiCard, Legend, ModelDictionary, OTHER_COLOR, SERIES_COLORS, TooltipBox, chartPlaceholder, fieldLabel, formatCurrency, formatMeasure, formatNumber, measureFormat, measureFormatter, numberOf, seriesColor, useClickFilter, useDashboard, useModelQuery };
1648
+ export { BarChart, ComboChart, DashboardCanvas, DashboardCharts, DashboardFilters, DashboardProvider, DataTable, DonutChart, FilterSelect, INSPECT_ATTRIBUTE, KpiCard, Legend, ModelDictionary, OTHER_COLOR, SERIES_COLORS, TooltipBox, chartCardSx, chartPlaceholder, chartTitleSx, fieldLabel, formatCurrency, formatMeasure, formatNumber, measureFormat, measureFormatter, numberOf, seriesColor, useClickFilter, useDashboard, useModelQuery };
1524
1649
 
1525
1650
  //# sourceMappingURL=index.js.map