feat: add theme typography spacing and color controls
This commit is contained in:
parent
fc77267987
commit
a331c2bb56
@ -205,3 +205,15 @@ Minimal `theme-pack.json`:
|
||||
```
|
||||
|
||||
For themes with no image assets, the same manifest can be uploaded directly as a `.json` file. `appliesTo` may be `"report"`, `"email"`, or `"all"`.
|
||||
|
||||
|
||||
## Theme tokens: typography, spacing and color
|
||||
|
||||
Reusable themes may also define these constrained tokens:
|
||||
|
||||
- Typography: `fontFamily`, `baseFontSize`, `headingSize`, `lineHeight`
|
||||
- Spacing: `blockGap`, `cardPadding`, `tableCellPadding`, `pagePadding`
|
||||
- Shape: `borderRadius`
|
||||
- Palette: `backgroundColor`, `surfaceColor`, `textColor`, `mutedColor`, `accentColor`, `buttonTextColor`, `borderColor`
|
||||
|
||||
These tokens are editable from Preview and remain part of the draft/published theme.
|
||||
|
||||
@ -65,6 +65,9 @@ function resolveDensity(template, mode) {
|
||||
function themeOf(template) {
|
||||
const theme = template?.theme || {}
|
||||
const reportDensityMode = safeDensityMode(theme.reportDensity)
|
||||
const reportDensity = resolveDensity(template, reportDensityMode)
|
||||
const baseFontSize = safeNumber(theme.baseFontSize, 15, 10, 22)
|
||||
const headingSize = safeNumber(theme.headingSize, 23, 14, 42)
|
||||
return {
|
||||
backgroundColor: safeHex(theme.backgroundColor, '#f8fafc'),
|
||||
surfaceColor: safeHex(theme.surfaceColor, '#ffffff'),
|
||||
@ -74,6 +77,14 @@ function themeOf(template) {
|
||||
buttonTextColor: safeHex(theme.buttonTextColor, '#ffffff'),
|
||||
borderColor: safeHex(theme.borderColor, '#dbe2ea'),
|
||||
borderRadius: safeNumber(theme.borderRadius, 12, 0, 32),
|
||||
baseFontSize,
|
||||
bodyFontSize: Math.max(10, baseFontSize - (reportDensity === 'compact' ? 2 : 0)),
|
||||
headingSize,
|
||||
renderedHeadingSize: Math.max(14, headingSize - (reportDensity === 'compact' ? 4 : 0)),
|
||||
lineHeight: safeNumber(theme.lineHeight, 1.45, 1.1, 1.9),
|
||||
blockGap: safeNumber(theme.blockGap, 10, 0, 32),
|
||||
cardPadding: safeNumber(theme.cardPadding, 12, 4, 32),
|
||||
tableCellPadding: safeNumber(theme.tableCellPadding, 8, 2, 20),
|
||||
pagePadding: safeNumber(theme.pagePadding, 18, 0, 80),
|
||||
pageMarginTop: safeNumber(theme.pageMarginTop, 12, 0, 40),
|
||||
pageMarginRight: safeNumber(theme.pageMarginRight, 12, 0, 40),
|
||||
@ -86,7 +97,7 @@ function themeOf(template) {
|
||||
decorativeBorder: theme.decorativeBorder === true,
|
||||
borderImage: safeUrl(theme.borderImage, {}),
|
||||
reportDensityMode,
|
||||
reportDensity: resolveDensity(template, reportDensityMode)
|
||||
reportDensity
|
||||
}
|
||||
}
|
||||
|
||||
@ -232,7 +243,7 @@ export function renderReportHtml(template, data = {}) {
|
||||
return `<!doctype html>
|
||||
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<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:${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}
|
||||
@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.bodyFontSize}px/${theme.lineHeight} ${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' ? Math.max(2, theme.blockGap - 4) : theme.blockGap}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.renderedHeadingSize}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' ? `${Math.max(4, theme.cardPadding - 4)}px ${Math.max(6, theme.cardPadding - 2)}px` : `${theme.cardPadding}px ${theme.cardPadding + 2}px`};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:${Math.max(9.5, theme.bodyFontSize - 2)}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' ? `${Math.max(2, theme.tableCellPadding - 3)}px ${Math.max(3, theme.tableCellPadding - 2)}px` : `${theme.tableCellPadding}px ${theme.tableCellPadding + 2}px`};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:${Math.max(2, theme.tableCellPadding - 3)}px ${Math.max(3, theme.tableCellPadding - 2)}px;font-size:${Math.max(9.5, theme.bodyFontSize - 2)}px}
|
||||
</style></head><body><main class="document" data-density="${theme.reportDensity}">${renderReportBlocks(blocks, data, theme)}</main></body></html>`
|
||||
}
|
||||
|
||||
@ -250,7 +261,7 @@ function emailBlock(block, data, theme) {
|
||||
|
||||
switch (block.type) {
|
||||
case 'heading':
|
||||
return `<mj-text ${common} font-size="24px" font-weight="700" container-background-color="${backgroundColor}">${escapeHtml(interpolate(p.text, data))}</mj-text>`
|
||||
return `<mj-text ${common} font-size="${theme.headingSize}px" line-height="${theme.lineHeight}" font-weight="700" container-background-color="${backgroundColor}">${escapeHtml(interpolate(p.text, data))}</mj-text>`
|
||||
case 'text':
|
||||
return `<mj-text ${common} container-background-color="${backgroundColor}">${escapeHtml(interpolate(p.text, data))}</mj-text>`
|
||||
case 'kpi':
|
||||
@ -270,7 +281,7 @@ export function renderEmailHtml(template, data = {}) {
|
||||
const theme = themeOf(template)
|
||||
const body = (template?.blocks || []).map(block => emailBlock(block, data, theme)).join('\n')
|
||||
const backgroundUrl = theme.pageBackgroundImage === '#' ? '' : ` background-url="${escapeHtml(theme.pageBackgroundImage)}" background-size="cover" background-repeat="no-repeat"`
|
||||
const mjml = `<mjml><mj-head><mj-attributes><mj-all font-family="${theme.fontFamily}, sans-serif" /><mj-text font-size="15px" line-height="1.5" color="${theme.textColor}" /></mj-attributes></mj-head><mj-body background-color="${theme.backgroundColor}"><mj-section background-color="${theme.surfaceColor}" padding="${theme.pagePadding}px"${backgroundUrl}><mj-column>${body}</mj-column></mj-section></mj-body></mjml>`
|
||||
const mjml = `<mjml><mj-head><mj-attributes><mj-all font-family="${theme.fontFamily}, sans-serif" /><mj-text font-size="${theme.baseFontSize}px" line-height="${theme.lineHeight}" color="${theme.textColor}" /></mj-attributes></mj-head><mj-body background-color="${theme.backgroundColor}"><mj-section background-color="${theme.surfaceColor}" padding="${theme.pagePadding}px"${backgroundUrl}><mj-column>${body}</mj-column></mj-section></mj-body></mjml>`
|
||||
const result = mjml2html(mjml, { validationLevel: 'soft' })
|
||||
if (result.errors?.length) console.warn('MJML warnings', result.errors)
|
||||
return result.html
|
||||
|
||||
@ -161,3 +161,50 @@ test('comfortable density preserves explicit section page breaks', () => {
|
||||
assert.match(html, /data-density="comfortable"/)
|
||||
assert.match(html, /break-before:page/)
|
||||
})
|
||||
|
||||
|
||||
test('theme typography and spacing tokens drive report output', () => {
|
||||
const html = renderReportHtml({
|
||||
theme: {
|
||||
fontFamily: 'Georgia',
|
||||
baseFontSize: 17,
|
||||
headingSize: 30,
|
||||
lineHeight: 1.6,
|
||||
blockGap: 18,
|
||||
cardPadding: 20,
|
||||
tableCellPadding: 12
|
||||
},
|
||||
blocks: [
|
||||
{ type: 'heading', props: { text: 'Heading' } },
|
||||
{ type: 'kpi', props: { label: 'A', value: '1' } },
|
||||
{ type: 'table', props: { rowsPath: 'rows', columns: [{ label: 'Name', key: 'name' }] } }
|
||||
]
|
||||
}, { rows: [{ name: 'One' }] })
|
||||
|
||||
assert.match(html, /font:17px\/1\.6 Georgia,sans-serif/)
|
||||
assert.match(html, /h2\{font-size:30px/)
|
||||
assert.match(html, /margin:0 0 18px/)
|
||||
assert.match(html, /padding:20px 22px/)
|
||||
assert.match(html, /padding:12px 14px/)
|
||||
})
|
||||
|
||||
test('theme typography tokens drive email output', () => {
|
||||
const html = renderEmailHtml({
|
||||
theme: {
|
||||
fontFamily: 'Verdana',
|
||||
baseFontSize: 18,
|
||||
headingSize: 31,
|
||||
lineHeight: 1.7,
|
||||
accentColor: '#123456'
|
||||
},
|
||||
blocks: [
|
||||
{ type: 'heading', props: { text: 'Heading' } },
|
||||
{ type: 'button', props: { text: 'Go', url: 'https://example.com' } }
|
||||
]
|
||||
}, {})
|
||||
|
||||
assert.match(html, /font-family:Verdana/)
|
||||
assert.match(html, /font-size:18px/)
|
||||
assert.match(html, /font-size:31px/)
|
||||
assert.match(html, /#123456/i)
|
||||
})
|
||||
|
||||
@ -197,6 +197,12 @@ export const themePackSpec = {
|
||||
borderColor: '#d8e1e8',
|
||||
borderRadius: 8,
|
||||
fontFamily: 'Arial',
|
||||
baseFontSize: 15,
|
||||
headingSize: 24,
|
||||
lineHeight: 1.45,
|
||||
blockGap: 10,
|
||||
cardPadding: 12,
|
||||
tableCellPadding: 8,
|
||||
reportDensity: 'auto',
|
||||
pageMarginTop: 9,
|
||||
pageMarginRight: 9,
|
||||
|
||||
118
src/App.tsx
118
src/App.tsx
@ -107,6 +107,7 @@ export function App({ user }: Props) {
|
||||
|
||||
const loadedId = useRef<string | null>(null)
|
||||
const timer = useRef<number | undefined>(undefined)
|
||||
const previewTimer = useRef<number | undefined>(undefined)
|
||||
|
||||
const selected = templateDoc.blocks.find(block => block.id === selectedId) || null
|
||||
const paths = useMemo(() => flatten(sampleData), [sampleData])
|
||||
@ -167,6 +168,15 @@ export function App({ user }: Props) {
|
||||
return () => window.clearTimeout(timer.current)
|
||||
}, [templateDoc, sampleData, active?.id])
|
||||
|
||||
useEffect(() => {
|
||||
if (screen !== 'preview' || !active || loadedId.current !== active.id) return
|
||||
window.clearTimeout(previewTimer.current)
|
||||
previewTimer.current = window.setTimeout(() => {
|
||||
void refreshPreview().catch(err => setError(String(err)))
|
||||
}, 450)
|
||||
return () => window.clearTimeout(previewTimer.current)
|
||||
}, [templateDoc, sampleData, screen, active?.id])
|
||||
|
||||
async function openTemplate(id: string) {
|
||||
setError('')
|
||||
const remote = await api<TemplateRecord>(`/api/templates/${id}`)
|
||||
@ -663,6 +673,114 @@ export function App({ user }: Props) {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="theme-controls-card">
|
||||
<div className="theme-controls-heading">
|
||||
<div>
|
||||
<span className="eyebrow">THEME TOKENS</span>
|
||||
<h3>Typography, spacing & colors</h3>
|
||||
</div>
|
||||
<span>Live</span>
|
||||
</div>
|
||||
|
||||
<div className="theme-control-section">
|
||||
<span className="control-label">Font</span>
|
||||
<div className="theme-font-row">
|
||||
<select
|
||||
value={themeString('fontFamily', 'Arial')}
|
||||
onChange={event => updateTheme({ fontFamily: event.target.value })}
|
||||
>
|
||||
{['Arial', 'Helvetica', 'Verdana', 'Georgia', 'Times New Roman', 'system-ui'].map(font => (
|
||||
<option key={font} value={font}>{font}</option>
|
||||
))}
|
||||
</select>
|
||||
<label>
|
||||
Body
|
||||
<input
|
||||
type="number"
|
||||
min="10"
|
||||
max="22"
|
||||
step="1"
|
||||
value={themeNumber('baseFontSize', 15)}
|
||||
onChange={event => updateTheme({ baseFontSize: Number(event.target.value) })}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Heading
|
||||
<input
|
||||
type="number"
|
||||
min="14"
|
||||
max="42"
|
||||
step="1"
|
||||
value={themeNumber('headingSize', 23)}
|
||||
onChange={event => updateTheme({ headingSize: Number(event.target.value) })}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<label className="token-slider">
|
||||
<span>Line height</span>
|
||||
<input
|
||||
type="range"
|
||||
min="1.1"
|
||||
max="1.9"
|
||||
step="0.05"
|
||||
value={themeNumber('lineHeight', 1.45)}
|
||||
onChange={event => updateTheme({ lineHeight: Number(event.target.value) })}
|
||||
/>
|
||||
<strong>{themeNumber('lineHeight', 1.45).toFixed(2)}</strong>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="theme-control-section">
|
||||
<span className="control-label">Spacing</span>
|
||||
{[
|
||||
['Block gap', 'blockGap', 10, 0, 32],
|
||||
['Card padding', 'cardPadding', 12, 4, 32],
|
||||
['Table cells', 'tableCellPadding', 8, 2, 20],
|
||||
['Corner radius', 'borderRadius', 12, 0, 32]
|
||||
].map(([label, key, fallback, min, max]) => (
|
||||
<label className="token-slider" key={String(key)}>
|
||||
<span>{label}</span>
|
||||
<input
|
||||
type="range"
|
||||
min={Number(min)}
|
||||
max={Number(max)}
|
||||
step="1"
|
||||
value={themeNumber(String(key), Number(fallback))}
|
||||
onChange={event => updateTheme({ [String(key)]: Number(event.target.value) })}
|
||||
/>
|
||||
<strong>{themeNumber(String(key), Number(fallback))} px</strong>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="theme-control-section">
|
||||
<span className="control-label">Colors</span>
|
||||
<div className="theme-color-grid">
|
||||
{[
|
||||
['Page', 'backgroundColor', '#f8fafc'],
|
||||
['Surface', 'surfaceColor', '#ffffff'],
|
||||
['Text', 'textColor', '#172033'],
|
||||
['Muted', 'mutedColor', '#64748b'],
|
||||
['Accent', 'accentColor', '#111827'],
|
||||
['Border', 'borderColor', '#dbe2ea'],
|
||||
['Button text', 'buttonTextColor', '#ffffff']
|
||||
].map(([label, key, fallback]) => (
|
||||
<label className="color-token" key={String(key)}>
|
||||
<span>{label}</span>
|
||||
<span className="color-input-wrap">
|
||||
<input
|
||||
type="color"
|
||||
value={themeString(String(key), String(fallback))}
|
||||
onChange={event => updateTheme({ [String(key)]: event.target.value })}
|
||||
/>
|
||||
<code>{themeString(String(key), String(fallback)).toUpperCase()}</code>
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{active?.type === 'report' && (
|
||||
<section className="page-setup-card">
|
||||
<div className="page-setup-heading">
|
||||
|
||||
127
src/refine.css
127
src/refine.css
@ -951,3 +951,130 @@ body{
|
||||
font-weight:800;
|
||||
text-decoration:none;
|
||||
}
|
||||
|
||||
|
||||
.theme-controls-card{
|
||||
margin:0 0 14px;
|
||||
padding:14px;
|
||||
border:1px solid var(--line);
|
||||
border-radius:16px;
|
||||
background:white;
|
||||
box-shadow:0 3px 12px rgba(15,23,42,.025);
|
||||
}
|
||||
.theme-controls-heading{
|
||||
display:flex;
|
||||
align-items:flex-start;
|
||||
justify-content:space-between;
|
||||
gap:12px;
|
||||
margin-bottom:12px;
|
||||
}
|
||||
.theme-controls-heading h3{
|
||||
margin:3px 0 0;
|
||||
font-size:15px;
|
||||
}
|
||||
.theme-controls-heading>span{
|
||||
color:#18794e;
|
||||
background:#e7f6ed;
|
||||
border-radius:999px;
|
||||
padding:4px 7px;
|
||||
font-size:8px;
|
||||
font-weight:800;
|
||||
text-transform:uppercase;
|
||||
}
|
||||
.theme-control-section{
|
||||
display:grid;
|
||||
gap:9px;
|
||||
padding:12px 0;
|
||||
border-top:1px solid #edf1f5;
|
||||
}
|
||||
.theme-control-section:first-of-type{
|
||||
border-top:0;
|
||||
padding-top:0;
|
||||
}
|
||||
.theme-font-row{
|
||||
display:grid;
|
||||
grid-template-columns:minmax(0,1.6fr) repeat(2,minmax(64px,.7fr));
|
||||
gap:7px;
|
||||
}
|
||||
.theme-font-row select,
|
||||
.theme-font-row input{
|
||||
width:100%;
|
||||
min-width:0;
|
||||
border:1px solid #d6dde6;
|
||||
border-radius:9px;
|
||||
background:#f9fafb;
|
||||
color:#263449;
|
||||
padding:9px 8px;
|
||||
font-size:11px;
|
||||
}
|
||||
.theme-font-row label{
|
||||
display:grid;
|
||||
gap:4px;
|
||||
color:#7b8797;
|
||||
font-size:8px;
|
||||
font-weight:800;
|
||||
}
|
||||
.token-slider{
|
||||
display:grid;
|
||||
grid-template-columns:90px minmax(0,1fr) 48px;
|
||||
align-items:center;
|
||||
gap:8px;
|
||||
color:#59677a;
|
||||
font-size:9px;
|
||||
font-weight:800;
|
||||
}
|
||||
.token-slider input{
|
||||
width:100%;
|
||||
min-width:0;
|
||||
}
|
||||
.token-slider strong{
|
||||
text-align:right;
|
||||
color:#263449;
|
||||
font-size:9px;
|
||||
font-variant-numeric:tabular-nums;
|
||||
}
|
||||
.theme-color-grid{
|
||||
display:grid;
|
||||
grid-template-columns:repeat(2,minmax(0,1fr));
|
||||
gap:7px;
|
||||
}
|
||||
.color-token{
|
||||
display:grid;
|
||||
gap:5px;
|
||||
padding:8px;
|
||||
border:1px solid #e7ebf0;
|
||||
border-radius:10px;
|
||||
background:#fbfcfd;
|
||||
color:#7b8797;
|
||||
font-size:8px;
|
||||
font-weight:800;
|
||||
}
|
||||
.color-input-wrap{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
gap:7px;
|
||||
min-width:0;
|
||||
}
|
||||
.color-input-wrap input[type="color"]{
|
||||
width:30px;
|
||||
height:30px;
|
||||
flex:0 0 auto;
|
||||
padding:0;
|
||||
border:0;
|
||||
border-radius:7px;
|
||||
overflow:hidden;
|
||||
background:transparent;
|
||||
}
|
||||
.color-input-wrap code{
|
||||
min-width:0;
|
||||
overflow:hidden;
|
||||
white-space:nowrap;
|
||||
text-overflow:ellipsis;
|
||||
color:#344258;
|
||||
font-size:9px;
|
||||
}
|
||||
@media(min-width:760px){
|
||||
.theme-color-grid{
|
||||
grid-template-columns:repeat(4,minmax(0,1fr));
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user