feat(editor): add themed callouts and desktop command boundary
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
/* Semantic tokens inherit every installed theme, including imported themes. */
|
||||
:where(.markdown-callout) { --callout-color: var(--color-callout-info, var(--color-info)); }
|
||||
.markdown-callout, .milkdown .ProseMirror blockquote.markdown-callout {
|
||||
border: 1px solid color-mix(in srgb, var(--callout-color) 35%, transparent);
|
||||
border-inline-start: 4px solid var(--callout-color);
|
||||
border-radius: var(--radius-md, 8px);
|
||||
background: color-mix(in srgb, var(--callout-color) 8%, var(--color-surface-primary, var(--paper, #fffdf7)));
|
||||
color: var(--color-text-primary, var(--ink, inherit));
|
||||
margin: 12px 0; padding: 12px 16px; min-width: 0;
|
||||
}
|
||||
.markdown-callout[data-callout='warning'], .markdown-callout[data-callout='question'] { --callout-color: var(--color-callout-warning, var(--color-warning)); }
|
||||
.markdown-callout[data-callout='danger'], .markdown-callout[data-callout='failure'], .markdown-callout[data-callout='bug'] { --callout-color: var(--color-callout-danger, var(--color-error)); }
|
||||
.markdown-callout[data-callout='success'], .markdown-callout[data-callout='tip'] { --callout-color: var(--color-callout-success, var(--color-success)); }
|
||||
.markdown-callout[data-callout='abstract'], .markdown-callout[data-callout='important'], .markdown-callout[data-callout='example'] { --callout-color: var(--color-callout-important, var(--color-accent-secondary)); }
|
||||
.markdown-callout[data-callout='quote'] { --callout-color: var(--color-callout-quote, var(--color-text-secondary)); }
|
||||
.markdown-callout[data-callout='todo'] { --callout-color: var(--color-callout-info, var(--color-accent-primary)); }
|
||||
.markdown-callout > .callout-title { color: var(--callout-color); font-weight: 700; line-height: 1.5; overflow-wrap: anywhere; }
|
||||
.markdown-callout > button.callout-title { background: transparent; border: 0; padding: 0; width: 100%; text-align: start; cursor: pointer; }
|
||||
.markdown-callout > button.callout-title:disabled { opacity: 1; cursor: default; color: var(--callout-color); }
|
||||
.markdown-callout > summary.callout-title { cursor: pointer; }
|
||||
.markdown-callout > :is(button, summary).callout-title:focus-visible { outline: 2px solid var(--color-border-focus); outline-offset: 4px; border-radius: var(--radius-sm); }
|
||||
.markdown-callout > :is(button:not(:disabled), summary).callout-title:hover { text-decoration: underline; text-underline-offset: 3px; }
|
||||
.markdown-callout .callout-body { min-width: 0; overflow-wrap: anywhere; }
|
||||
.markdown-callout .callout-body > :last-child { margin-bottom: 0; }
|
||||
.markdown-callout[data-collapsed='true'] > .callout-body { display: none; }
|
||||
.callout-marker { display: none; }
|
||||
.callout-title[hidden] { display: none !important; }
|
||||
.callout-marker-editing { opacity: .65; font-family: monospace; }
|
||||
@@ -22,3 +22,47 @@ it.each(mockCommunityThemes)('provides interaction and Markdown colors in $theme
|
||||
}
|
||||
expect(css).toContain(`color-scheme: ${theme.is_dark ? 'dark' : 'light'}`)
|
||||
})
|
||||
|
||||
const calloutThemes = ['light', 'dark', 'sepia', ...mockCommunityThemes.map(theme => theme.theme_id)]
|
||||
const calloutTones = ['info', 'success', 'warning', 'danger', 'important', 'quote']
|
||||
it.each(calloutThemes)('keeps callout headings readable against tinted surfaces in %s', themeId => {
|
||||
const doc = document.implementation.createHTMLDocument('theme contrast')
|
||||
const style = doc.createElement('style')
|
||||
style.textContent = files['styles/tokens.css'] ?? files['styles\\tokens.css']!
|
||||
style.textContent += getCommunityThemePreviewCss(themeId)
|
||||
doc.head.append(style)
|
||||
const values = new Map<string, string>()
|
||||
for (const rule of Array.from(doc.styleSheets[0]!.cssRules) as CSSStyleRule[]) {
|
||||
if (![':root', `[data-theme='${themeId}']`, `[data-theme="${themeId}"]`].includes(rule.selectorText)) continue
|
||||
for (const name of ['--color-surface-primary', ...calloutTones.map(tone => `--color-callout-${tone}`)]) {
|
||||
const value = rule.style.getPropertyValue(name).trim()
|
||||
if (value) values.set(name, value)
|
||||
}
|
||||
}
|
||||
const rgb = (hex: string) => [1, 3, 5].map(offset => parseInt(hex.slice(offset, offset + 2), 16) / 255)
|
||||
const luminance = (color: number[]) => color.map(value => value <= .04045 ? value / 12.92 : ((value + .055) / 1.055) ** 2.4).reduce((sum, value, index) => sum + value * [.2126, .7152, .0722][index]!, 0)
|
||||
const surface = rgb(values.get('--color-surface-primary')!)
|
||||
for (const tone of calloutTones) {
|
||||
const hex = values.get(`--color-callout-${tone}`)!
|
||||
expect(hex).toMatch(/^#[\da-f]{6}$/i)
|
||||
const ink = rgb(hex)
|
||||
const background = surface.map((value, index) => value * .92 + ink[index]! * .08)
|
||||
const first = luminance(ink), second = luminance(background)
|
||||
expect((Math.max(first, second) + .05) / (Math.min(first, second) + .05), tone).toBeGreaterThanOrEqual(4.5)
|
||||
}
|
||||
})
|
||||
|
||||
it('preserves warning and success colors inside the editor selector cascade', () => {
|
||||
const style = document.createElement('style')
|
||||
style.textContent = readFileSync(join(root, 'styles/callouts.css'), 'utf8')
|
||||
document.head.append(style)
|
||||
const host = document.createElement('div')
|
||||
host.className = 'milkdown'
|
||||
host.innerHTML = '<div class="ProseMirror"><blockquote class="markdown-callout" data-callout="warning"></blockquote><blockquote class="markdown-callout" data-callout="success"></blockquote></div>'
|
||||
document.body.append(host)
|
||||
try {
|
||||
const blocks = host.querySelectorAll('blockquote')
|
||||
expect(getComputedStyle(blocks[0]!).getPropertyValue('--callout-color')).toContain('--color-callout-warning')
|
||||
expect(getComputedStyle(blocks[1]!).getPropertyValue('--callout-color')).toContain('--color-callout-success')
|
||||
} finally { host.remove(); style.remove() }
|
||||
})
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
:root {
|
||||
/* Callout heading colors also serve as borders; keep text readable on tint. */
|
||||
--color-callout-info: var(--color-info);
|
||||
--color-callout-success: var(--color-success);
|
||||
--color-callout-warning: var(--color-warning);
|
||||
--color-callout-danger: var(--color-error);
|
||||
--color-callout-important: var(--color-accent-secondary);
|
||||
--color-callout-quote: var(--color-text-secondary);
|
||||
/* Background */
|
||||
--color-background-primary: #ffffff;
|
||||
--color-background-secondary: #f7f8fa;
|
||||
@@ -117,7 +124,22 @@
|
||||
--statusbar-height: 28px;
|
||||
}
|
||||
|
||||
[data-theme='light'] {
|
||||
--color-callout-info: #175da6;
|
||||
--color-callout-success: #236b3b;
|
||||
--color-callout-warning: #855700;
|
||||
--color-callout-danger: #ad2935;
|
||||
--color-callout-important: #7443ad;
|
||||
--color-callout-quote: #59636e;
|
||||
}
|
||||
|
||||
[data-theme='dark'] {
|
||||
--color-callout-info: #8bbdff;
|
||||
--color-callout-success: #80ce93;
|
||||
--color-callout-warning: #efc66f;
|
||||
--color-callout-danger: #ff9b9b;
|
||||
--color-callout-important: #c8a5ff;
|
||||
--color-callout-quote: #abb6c2;
|
||||
--color-background-primary: #0d1117;
|
||||
--color-background-secondary: #161b22;
|
||||
--color-background-tertiary: #21262d;
|
||||
@@ -168,6 +190,12 @@
|
||||
}
|
||||
|
||||
[data-theme='sepia'] {
|
||||
--color-callout-info: #396578;
|
||||
--color-callout-success: #496b3b;
|
||||
--color-callout-warning: #805918;
|
||||
--color-callout-danger: #a04438;
|
||||
--color-callout-important: #785476;
|
||||
--color-callout-quote: #746653;
|
||||
--color-background-primary: #fbf3df;
|
||||
--color-background-secondary: #f4e8ca;
|
||||
--color-background-tertiary: #eadbb8;
|
||||
|
||||
Reference in New Issue
Block a user