feat: add dense report composition
This commit is contained in:
parent
8edad1e9e4
commit
444990a3af
@ -36,6 +36,7 @@ my-pack.zip
|
|||||||
"pageMarginBottom": 12,
|
"pageMarginBottom": 12,
|
||||||
"pageMarginLeft": 12,
|
"pageMarginLeft": 12,
|
||||||
"pagePadding": 18,
|
"pagePadding": 18,
|
||||||
|
"reportDensity": "auto",
|
||||||
"pageBackgroundImage": "asset://paper-bg",
|
"pageBackgroundImage": "asset://paper-bg",
|
||||||
"pageBackgroundMode": "none",
|
"pageBackgroundMode": "none",
|
||||||
"pageBackgroundOpacity": 0.08,
|
"pageBackgroundOpacity": 0.08,
|
||||||
@ -121,6 +122,19 @@ Report themes can set A4 print margins independently in millimetres with `pageMa
|
|||||||
|
|
||||||
`pagePadding` is separate: it controls the internal spacing inside the rendered document/frame in pixels and defaults to 18 px.
|
`pagePadding` is separate: it controls the internal spacing inside the rendered document/frame in pixels and defaults to 18 px.
|
||||||
|
|
||||||
|
### Report density
|
||||||
|
|
||||||
|
Set `reportDensity` to `"auto"`, `"comfortable"`, or `"compact"`. Auto is the default. Reports with at least 20 blocks or 4 table blocks automatically use compact density.
|
||||||
|
|
||||||
|
Compact density:
|
||||||
|
- groups consecutive KPI blocks into a three-column grid;
|
||||||
|
- tightens heading, block, KPI and table spacing;
|
||||||
|
- treats normal `pageBreakBefore` hints as soft so sections can flow into unused page space;
|
||||||
|
- still honours `forcePageBreak: true`;
|
||||||
|
- keeps headings with the following content where possible.
|
||||||
|
|
||||||
|
For long tables set `style.allowPageBreak: true`. The renderer then permits rows to continue onto the next page and repeats the table header. Use `style.compact: true` to request tight table spacing even in comfortable mode.
|
||||||
|
|
||||||
### Page artwork
|
### Page artwork
|
||||||
|
|
||||||
Reports default to a clean page even if the ZIP includes background or border assets. Set `pageBackgroundMode` to `"watermark"` or `"full"` to opt in to the background image. `pageBackgroundOpacity` controls artwork strength from 0 to 0.35.
|
Reports default to a clean page even if the ZIP includes background or border assets. Set `pageBackgroundMode` to `"watermark"` or `"full"` to opt in to the background image. `pageBackgroundOpacity` controls artwork strength from 0 to 0.35.
|
||||||
|
|||||||
@ -51,8 +51,20 @@ function safeBackgroundMode(value) {
|
|||||||
return ['none', 'watermark', 'full'].includes(value) ? value : 'none'
|
return ['none', 'watermark', 'full'].includes(value) ? value : 'none'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function safeDensityMode(value) {
|
||||||
|
return ['auto', 'comfortable', 'compact'].includes(value) ? value : 'auto'
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveDensity(template, mode) {
|
||||||
|
if (mode !== 'auto') return mode
|
||||||
|
const blocks = Array.isArray(template?.blocks) ? template.blocks : []
|
||||||
|
const tableCount = blocks.filter(block => block?.type === 'table').length
|
||||||
|
return blocks.length >= 20 || tableCount >= 4 ? 'compact' : 'comfortable'
|
||||||
|
}
|
||||||
|
|
||||||
function themeOf(template) {
|
function themeOf(template) {
|
||||||
const theme = template?.theme || {}
|
const theme = template?.theme || {}
|
||||||
|
const reportDensityMode = safeDensityMode(theme.reportDensity)
|
||||||
return {
|
return {
|
||||||
backgroundColor: safeHex(theme.backgroundColor, '#f8fafc'),
|
backgroundColor: safeHex(theme.backgroundColor, '#f8fafc'),
|
||||||
surfaceColor: safeHex(theme.surfaceColor, '#ffffff'),
|
surfaceColor: safeHex(theme.surfaceColor, '#ffffff'),
|
||||||
@ -72,7 +84,9 @@ function themeOf(template) {
|
|||||||
pageBackgroundMode: safeBackgroundMode(theme.pageBackgroundMode),
|
pageBackgroundMode: safeBackgroundMode(theme.pageBackgroundMode),
|
||||||
pageBackgroundOpacity: safeNumber(theme.pageBackgroundOpacity, 0.08, 0, 0.35),
|
pageBackgroundOpacity: safeNumber(theme.pageBackgroundOpacity, 0.08, 0, 0.35),
|
||||||
decorativeBorder: theme.decorativeBorder === true,
|
decorativeBorder: theme.decorativeBorder === true,
|
||||||
borderImage: safeUrl(theme.borderImage, {})
|
borderImage: safeUrl(theme.borderImage, {}),
|
||||||
|
reportDensityMode,
|
||||||
|
reportDensity: resolveDensity(template, reportDensityMode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,8 +105,11 @@ function safeVariant(block) {
|
|||||||
|
|
||||||
function reportBlockStyle(block, theme) {
|
function reportBlockStyle(block, theme) {
|
||||||
const style = block?.style || {}
|
const style = block?.style || {}
|
||||||
const breakBefore = style.pageBreakBefore === true ? 'page' : 'auto'
|
const compact = theme.reportDensity === 'compact'
|
||||||
const breakAfter = style.pageBreakAfter === true ? 'page' : (style.keepWithNext === true ? 'avoid-page' : 'auto')
|
const forceBreakBefore = style.forcePageBreak === true
|
||||||
|
const breakBefore = (style.pageBreakBefore === true && (!compact || forceBreakBefore)) ? 'page' : 'auto'
|
||||||
|
const keepWithNext = style.keepWithNext === true || (compact && block?.type === 'heading')
|
||||||
|
const breakAfter = style.pageBreakAfter === true ? 'page' : (keepWithNext ? 'avoid-page' : 'auto')
|
||||||
const breakInside = style.allowPageBreak === true ? 'auto' : 'avoid-page'
|
const breakInside = style.allowPageBreak === true ? 'auto' : 'avoid-page'
|
||||||
const rules = [
|
const rules = [
|
||||||
`background:${safeHex(style.backgroundColor, 'transparent')}`,
|
`background:${safeHex(style.backgroundColor, 'transparent')}`,
|
||||||
@ -142,11 +159,14 @@ function wrapReportBlock(block, theme, html) {
|
|||||||
return `<div class="block${variant}" style="${reportBlockStyle(block, theme)}">${html}</div>`
|
return `<div class="block${variant}" style="${reportBlockStyle(block, theme)}">${html}</div>`
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderTable(block, data) {
|
function renderTable(block, data, theme) {
|
||||||
const rows = getPath(data, block.props?.rowsPath) || []
|
const rows = getPath(data, block.props?.rowsPath) || []
|
||||||
const columns = Array.isArray(block.props?.columns) ? block.props.columns : []
|
const columns = Array.isArray(block.props?.columns) ? block.props.columns : []
|
||||||
if (!Array.isArray(rows)) return ''
|
if (!Array.isArray(rows)) return ''
|
||||||
return `<div class="table-wrap"><table><thead><tr>${columns.map(c => `<th>${escapeHtml(c.label)}</th>`).join('')}</tr></thead><tbody>${rows.map(row => `<tr>${columns.map(c => `<td>${escapeHtml(getPath(row, c.key))}</td>`).join('')}</tr>`).join('')}</tbody></table></div>`
|
const allowBreak = block?.style?.allowPageBreak === true
|
||||||
|
const compact = theme.reportDensity === 'compact' || block?.style?.compact === true
|
||||||
|
const classes = ['table-wrap', allowBreak ? 'allow-break' : '', compact ? 'compact-table' : ''].filter(Boolean).join(' ')
|
||||||
|
return `<div class="${classes}"><table><thead><tr>${columns.map(c => `<th>${escapeHtml(c.label)}</th>`).join('')}</tr></thead><tbody>${rows.map(row => `<tr>${columns.map(c => `<td>${escapeHtml(getPath(row, c.key))}</td>`).join('')}</tr>`).join('')}</tbody></table></div>`
|
||||||
}
|
}
|
||||||
|
|
||||||
function blockHtml(block, data, theme) {
|
function blockHtml(block, data, theme) {
|
||||||
@ -172,7 +192,7 @@ function blockHtml(block, data, theme) {
|
|||||||
html = '<hr />'
|
html = '<hr />'
|
||||||
break
|
break
|
||||||
case 'table':
|
case 'table':
|
||||||
html = renderTable(block, data)
|
html = renderTable(block, data, theme)
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
return ''
|
return ''
|
||||||
@ -180,6 +200,26 @@ function blockHtml(block, data, theme) {
|
|||||||
return wrapReportBlock(block, theme, html)
|
return wrapReportBlock(block, theme, html)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderReportBlocks(blocks, data, theme) {
|
||||||
|
const output = []
|
||||||
|
for (let index = 0; index < blocks.length; index += 1) {
|
||||||
|
const block = blocks[index]
|
||||||
|
if (theme.reportDensity === 'compact' && block?.type === 'kpi') {
|
||||||
|
const group = [block]
|
||||||
|
while (index + 1 < blocks.length && blocks[index + 1]?.type === 'kpi') {
|
||||||
|
group.push(blocks[index + 1])
|
||||||
|
index += 1
|
||||||
|
}
|
||||||
|
if (group.length > 1) {
|
||||||
|
output.push(`<div class="kpi-grid">${group.map(item => blockHtml(item, data, theme)).join('')}</div>`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output.push(blockHtml(block, data, theme))
|
||||||
|
}
|
||||||
|
return output.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
export function renderReportHtml(template, data = {}) {
|
export function renderReportHtml(template, data = {}) {
|
||||||
const blocks = template?.blocks || []
|
const blocks = template?.blocks || []
|
||||||
const theme = themeOf(template)
|
const theme = themeOf(template)
|
||||||
@ -192,8 +232,8 @@ export function renderReportHtml(template, data = {}) {
|
|||||||
return `<!doctype html>
|
return `<!doctype html>
|
||||||
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
<style>
|
<style>
|
||||||
@page{size:A4;margin:${theme.pageMarginTop}mm ${theme.pageMarginRight}mm ${theme.pageMarginBottom}mm ${theme.pageMarginLeft}mm}*{box-sizing:border-box}html,body{min-height:100%}body{font:${15}px/1.45 ${theme.fontFamily},sans-serif;color:${theme.textColor};margin:0;background:${theme.backgroundColor};-webkit-print-color-adjust:exact;print-color-adjust:exact}.document{position:relative;isolation:isolate;overflow:hidden;min-height:calc(297mm - ${theme.pageMarginTop + theme.pageMarginBottom}mm);background:${theme.surfaceColor};padding:${theme.pagePadding}px;border:${borderEnabled ? 10 : 0}px solid transparent;border-image:${borderImage};border-radius:${theme.borderRadius}px}.document::before{content:"";position:absolute;z-index:-1;inset:0;background-image:${backgroundImage};background-size:${backgroundSize};background-position:center top;background-repeat:no-repeat;opacity:${hasBackground ? theme.pageBackgroundOpacity : 0};pointer-events:none}.block{margin:0 0 10px;break-inside:avoid-page;page-break-inside:avoid}.block:last-child{margin-bottom:0}.image-logo{margin-bottom:6px}.image-hero{margin-top:2px;margin-bottom:10px}h2{font-size:23px;line-height:1.2;margin:0;color:inherit}p{margin:0}.kpi{border:1px solid ${theme.borderColor};border-radius:${theme.borderRadius}px;padding:12px 14px;display:flex;justify-content:space-between;gap:16px;background:${theme.surfaceColor}}.kpi span{color:${theme.mutedColor}}.kpi strong{font-size:18px}.button{display:inline-block;background:${theme.accentColor};color:${theme.buttonTextColor};text-decoration:none;padding:10px 16px;border-radius:${theme.borderRadius}px}.image{height:auto;vertical-align:middle}hr{border:0;border-top:1px solid ${theme.borderColor};margin:10px 0}.table-wrap{overflow:hidden;border:1px solid ${theme.borderColor};border-radius:${theme.borderRadius}px;break-inside:avoid-page;page-break-inside:avoid}table{border-collapse:collapse;width:100%;font-size:13px}thead{display:table-header-group}tr{break-inside:avoid-page;page-break-inside:avoid}th,td{padding:8px 10px;text-align:left;border-bottom:1px solid ${theme.borderColor}}th{background:${theme.backgroundColor}}
|
@page{size:A4;margin:${theme.pageMarginTop}mm ${theme.pageMarginRight}mm ${theme.pageMarginBottom}mm ${theme.pageMarginLeft}mm}*{box-sizing:border-box}html,body{min-height:100%}body{font:${theme.reportDensity === 'compact' ? 13 : 15}px/${theme.reportDensity === 'compact' ? 1.35 : 1.45} ${theme.fontFamily},sans-serif;color:${theme.textColor};margin:0;background:${theme.backgroundColor};-webkit-print-color-adjust:exact;print-color-adjust:exact}.document{position:relative;isolation:isolate;overflow:hidden;min-height:calc(297mm - ${theme.pageMarginTop + theme.pageMarginBottom}mm);background:${theme.surfaceColor};padding:${theme.pagePadding}px;border:${borderEnabled ? 10 : 0}px solid transparent;border-image:${borderImage};border-radius:${theme.borderRadius}px}.document::before{content:"";position:absolute;z-index:-1;inset:0;background-image:${backgroundImage};background-size:${backgroundSize};background-position:center top;background-repeat:no-repeat;opacity:${hasBackground ? theme.pageBackgroundOpacity : 0};pointer-events:none}.block{margin:0 0 ${theme.reportDensity === 'compact' ? 6 : 10}px;break-inside:avoid-page;page-break-inside:avoid}.block:last-child{margin-bottom:0}.image-logo{margin-bottom:6px}.image-hero{margin-top:2px;margin-bottom:10px}h2{font-size:${theme.reportDensity === 'compact' ? 19 : 23}px;line-height:1.2;margin:0;color:inherit}p{margin:0}.kpi-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:6px;margin:0 0 6px;break-inside:avoid-page;page-break-inside:avoid}.kpi-grid>.block{margin:0;min-width:0}.kpi{border:1px solid ${theme.borderColor};border-radius:${theme.borderRadius}px;padding:${theme.reportDensity === 'compact' ? '8px 10px' : '12px 14px'};display:flex;justify-content:space-between;gap:10px;background:${theme.surfaceColor}}.kpi-grid .kpi{height:100%;display:grid;gap:3px;align-content:center}.kpi span{color:${theme.mutedColor}}.kpi strong{font-size:${theme.reportDensity === 'compact' ? 16 : 18}px}.button{display:inline-block;background:${theme.accentColor};color:${theme.buttonTextColor};text-decoration:none;padding:10px 16px;border-radius:${theme.borderRadius}px}.image{height:auto;vertical-align:middle}hr{border:0;border-top:1px solid ${theme.borderColor};margin:${theme.reportDensity === 'compact' ? 6 : 10}px 0}.table-wrap{overflow:hidden;border:1px solid ${theme.borderColor};border-radius:${theme.borderRadius}px;break-inside:avoid-page;page-break-inside:avoid}.table-wrap.allow-break{overflow:visible;break-inside:auto;page-break-inside:auto}.table-wrap.allow-break table{border-collapse:collapse}.compact-table{border-radius:${Math.min(theme.borderRadius, 6)}px}table{border-collapse:collapse;width:100%;font-size:${theme.reportDensity === 'compact' ? 11.5 : 13}px}thead{display:table-header-group}tfoot{display:table-footer-group}tr{break-inside:avoid-page;page-break-inside:avoid}th,td{padding:${theme.reportDensity === 'compact' ? '5px 6px' : '8px 10px'};text-align:left;border-bottom:1px solid ${theme.borderColor};vertical-align:top}th{background:${theme.backgroundColor};font-weight:700}.compact-table th,.compact-table td{padding:5px 6px;font-size:11.5px}
|
||||||
</style></head><body><main class="document">${blocks.map(block => blockHtml(block, data, theme)).join('\n')}</main></body></html>`
|
</style></head><body><main class="document" data-density="${theme.reportDensity}">${renderReportBlocks(blocks, data, theme)}</main></body></html>`
|
||||||
}
|
}
|
||||||
|
|
||||||
function emailBlock(block, data, theme) {
|
function emailBlock(block, data, theme) {
|
||||||
|
|||||||
@ -109,3 +109,55 @@ test('report page chrome can opt into subtle background and decorative border',
|
|||||||
assert.match(html, /opacity:0\.1/)
|
assert.match(html, /opacity:0\.1/)
|
||||||
assert.match(html, /border:10px solid transparent/)
|
assert.match(html, /border:10px solid transparent/)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
test('auto density compacts data-heavy reports and groups consecutive KPIs', () => {
|
||||||
|
const blocks = [
|
||||||
|
{ type: 'heading', props: { text: 'Dense' } },
|
||||||
|
{ type: 'kpi', props: { label: 'A', value: '1' } },
|
||||||
|
{ type: 'kpi', props: { label: 'B', value: '2' } },
|
||||||
|
{ type: 'kpi', props: { label: 'C', value: '3' } },
|
||||||
|
...Array.from({ length: 16 }, (_, index) => ({ type: 'text', props: { text: `Row ${index}` } }))
|
||||||
|
]
|
||||||
|
const html = renderReportHtml({ blocks }, {})
|
||||||
|
assert.match(html, /data-density="compact"/)
|
||||||
|
assert.match(html, /class="kpi-grid"/)
|
||||||
|
assert.match(html, /grid-template-columns:repeat\(3,minmax\(0,1fr\)\)/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('compact density flows past ordinary page breaks but preserves forced breaks', () => {
|
||||||
|
const html = renderReportHtml({
|
||||||
|
theme: { reportDensity: 'compact' },
|
||||||
|
blocks: [
|
||||||
|
{ type: 'heading', props: { text: 'Auto flow' }, style: { pageBreakBefore: true } },
|
||||||
|
{ type: 'heading', props: { text: 'Forced' }, style: { pageBreakBefore: true, forcePageBreak: true } }
|
||||||
|
]
|
||||||
|
}, {})
|
||||||
|
assert.match(html, /Auto flow/)
|
||||||
|
assert.match(html, /break-before:auto/)
|
||||||
|
assert.match(html, /Forced/)
|
||||||
|
assert.match(html, /break-before:page/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('splittable tables repeat headers and use compact table styling', () => {
|
||||||
|
const html = renderReportHtml({
|
||||||
|
theme: { reportDensity: 'compact' },
|
||||||
|
blocks: [{
|
||||||
|
type: 'table',
|
||||||
|
props: { rowsPath: 'rows', columns: [{ label: 'Name', key: 'name' }] },
|
||||||
|
style: { allowPageBreak: true }
|
||||||
|
}]
|
||||||
|
}, { rows: [{ name: 'A' }, { name: 'B' }] })
|
||||||
|
assert.match(html, /class="table-wrap allow-break compact-table"/)
|
||||||
|
assert.match(html, /thead\{display:table-header-group\}/)
|
||||||
|
assert.match(html, /page-break-inside:auto/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('comfortable density preserves explicit section page breaks', () => {
|
||||||
|
const html = renderReportHtml({
|
||||||
|
theme: { reportDensity: 'comfortable' },
|
||||||
|
blocks: [{ type: 'heading', props: { text: 'Section' }, style: { pageBreakBefore: true } }]
|
||||||
|
}, {})
|
||||||
|
assert.match(html, /data-density="comfortable"/)
|
||||||
|
assert.match(html, /break-before:page/)
|
||||||
|
})
|
||||||
|
|||||||
55
src/App.tsx
55
src/App.tsx
@ -203,6 +203,15 @@ export function App({ user }: Props) {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateBlockStyle(id: string, patch: Record<string, unknown>) {
|
||||||
|
setTemplateDoc(current => ({
|
||||||
|
...current,
|
||||||
|
blocks: current.blocks.map(block =>
|
||||||
|
block.id === id ? { ...block, style: { ...(block.style || {}), ...patch } } : block
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
function updateTheme(patch: Record<string, unknown>) {
|
function updateTheme(patch: Record<string, unknown>) {
|
||||||
setTemplateDoc(current => ({
|
setTemplateDoc(current => ({
|
||||||
...current,
|
...current,
|
||||||
@ -544,6 +553,25 @@ export function App({ user }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
<span>mm</span>
|
<span>mm</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="density-control">
|
||||||
|
<span className="control-label">Layout density</span>
|
||||||
|
<div className="segmented compact">
|
||||||
|
{[
|
||||||
|
['auto', 'Auto'],
|
||||||
|
['comfortable', 'Comfortable'],
|
||||||
|
['compact', 'Compact']
|
||||||
|
].map(([value, label]) => (
|
||||||
|
<button
|
||||||
|
key={value}
|
||||||
|
className={themeString('reportDensity', 'auto') === value ? 'active' : ''}
|
||||||
|
onClick={() => updateTheme({ reportDensity: value })}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<small>Auto switches data-heavy reports to compact flow.</small>
|
||||||
|
</div>
|
||||||
<div className="page-margin-grid">
|
<div className="page-margin-grid">
|
||||||
{[
|
{[
|
||||||
['Top', 'pageMarginTop', 12],
|
['Top', 'pageMarginTop', 12],
|
||||||
@ -630,6 +658,7 @@ export function App({ user }: Props) {
|
|||||||
pageMarginBottom: 12,
|
pageMarginBottom: 12,
|
||||||
pageMarginLeft: 12,
|
pageMarginLeft: 12,
|
||||||
pagePadding: 18,
|
pagePadding: 18,
|
||||||
|
reportDensity: 'auto',
|
||||||
pageBackgroundMode: 'none',
|
pageBackgroundMode: 'none',
|
||||||
pageBackgroundOpacity: 0.08,
|
pageBackgroundOpacity: 0.08,
|
||||||
decorativeBorder: false
|
decorativeBorder: false
|
||||||
@ -864,7 +893,31 @@ export function App({ user }: Props) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{selected.type === 'table' && (
|
{selected.type === 'table' && (
|
||||||
<label>Rows data path<input value={String(selected.props.rowsPath || '')} onChange={event => updateBlock(selected.id, { rowsPath: event.target.value })} placeholder="items" /></label>
|
<>
|
||||||
|
<label>Rows data path<input value={String(selected.props.rowsPath || '')} onChange={event => updateBlock(selected.id, { rowsPath: event.target.value })} placeholder="items" /></label>
|
||||||
|
<label className="toggle-row inline-toggle">
|
||||||
|
<span>
|
||||||
|
<strong>Allow page flow</strong>
|
||||||
|
<small>Split long tables across pages and repeat the header row.</small>
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={selected.style?.allowPageBreak === true}
|
||||||
|
onChange={event => updateBlockStyle(selected.id, { allowPageBreak: event.target.checked })}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="toggle-row inline-toggle">
|
||||||
|
<span>
|
||||||
|
<strong>Compact table</strong>
|
||||||
|
<small>Use tighter row spacing for high-column datasets.</small>
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={selected.style?.compact === true}
|
||||||
|
onChange={event => updateBlockStyle(selected.id, { compact: event.target.checked })}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{bindingField(selected) && !!paths.length && (
|
{bindingField(selected) && !!paths.length && (
|
||||||
|
|||||||
@ -833,3 +833,18 @@ body{
|
|||||||
height:18px;
|
height:18px;
|
||||||
flex:0 0 auto;
|
flex:0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.density-control{
|
||||||
|
display:grid;
|
||||||
|
gap:7px;
|
||||||
|
margin-bottom:12px;
|
||||||
|
}
|
||||||
|
.density-control small{
|
||||||
|
color:#8994a4;
|
||||||
|
font-size:9px;
|
||||||
|
line-height:1.4;
|
||||||
|
}
|
||||||
|
.inline-toggle{
|
||||||
|
margin-top:4px;
|
||||||
|
padding:10px 0;
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user