All files / app/util theme.ts

80.64% Statements 25/31
75% Branches 12/16
100% Functions 7/7
80.76% Lines 21/26

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122    78x       78x                                                                   24x       8x   8x 8x       8x 24x 24x 8x 240x 240x                       4x 4x 4x 4x             4x       2x 60x     2x               1x                                                      
export type ColorScheme = 'light' | 'dark';
 
/**
 * Standard CSS variables set on <body> by src/theme/common.scss, light.scss and dark.scss.
 * Keep in sync with docs/STRUCTURE.md.
 */
export const THEME_VARS: { name: string, description: string }[] = [
  { name: '--bg', description: $localize`Page background` },
  { name: '--bg-accent', description: $localize`Subtle background for panels and tabs` },
  { name: '--bg-hover', description: $localize`Background on hover and drop targets` },
  { name: '--bg-active', description: $localize`Background while pressed or selected` },
  { name: '--text', description: $localize`Main text colour` },
  { name: '--link', description: $localize`Link colour` },
  { name: '--visited', description: $localize`Visited link colour` },
  { name: '--active', description: $localize`Highlights such as the current page and new response counts` },
  { name: '--info', description: $localize`Secondary text such as row info, quotes and timestamps` },
  { name: '--tag', description: $localize`Inline tag links and accents` },
  { name: '--tag-accent', description: $localize`Inline tag link background (light theme)` },
  { name: '--blue', description: $localize`Tab bar background (light theme)` },
  { name: '--toggle', description: $localize`Toggle button background` },
  { name: '--toggle-accent', description: $localize`Toggle button background on hover` },
  { name: '--toggle-active', description: $localize`Toggle button background while pressed` },
  { name: '--toggle-border', description: $localize`Toggle button border` },
  { name: '--form-bg', description: $localize`Form input background` },
  { name: '--border', description: $localize`Main border colour` },
  { name: '--border-accent', description: $localize`Softer border for editors and cards` },
  { name: '--border-light', description: $localize`Faint border for cards` },
  { name: '--unselected-tab', description: $localize`Background of inactive tabs` },
  { name: '--error', description: $localize`Error text` },
  { name: '--error-bg', description: $localize`Error and login prompt background` },
  { name: '--warning', description: $localize`Warning text and borders` },
  { name: '--mod', description: $localize`Title colour of pinned refs` },
  { name: '--card', description: $localize`Card background (kanban, todo, chat, notes, ...)` },
  { name: '--card-dragging', description: $localize`Card background while dragging` },
  { name: '--placeholder', description: $localize`Drop placeholder background` },
  { name: '--deleting', description: $localize`Background of items about to be deleted` },
  { name: '--quote', description: $localize`Blockquote border in markdown` },
];
 
function matches(selectorText: string, selector: string) {
  return selectorText.split(',').some(s => s.trim() === selector);
}
 
function readRules(doc: Document, selector: string, result: Record<string, string>) {
  for (const sheet of Array.from(doc.styleSheets)) {
    let rules: CSSRuleList;
    try {
      rules = sheet.cssRules;
    } catch {
      continue;
    }
    for (const rule of Array.from(rules)) {
      const style = (rule as CSSStyleRule).style;
      if (!style || !matches((rule as CSSStyleRule).selectorText || '', selector)) continue;
      for (const v of THEME_VARS) {
        const value = style.getPropertyValue(v.name).trim();
        if (value) result[v.name] = value;
      }
    }
  }
}
 
/**
 * Current values of the standard CSS variables for a colour scheme.
 * Reads the loaded stylesheets, and for the active scheme also the computed
 * style of <body> so custom themes in use are included.
 */
export function themeVars(scheme: ColorScheme, doc: Document = document): Record<string, string> {
  const result: Record<string, string> = {};
  readRules(doc, 'body', result);
  readRules(doc, `body.${scheme}-theme`, result);
  Iif (doc.body?.classList.contains(`${scheme}-theme`)) {
    const computed = doc.defaultView?.getComputedStyle(doc.body);
    for (const v of THEME_VARS) {
      const value = computed?.getPropertyValue(v.name).trim();
      if (value) result[v.name] = value;
    }
  }
  return result;
}
 
function themeBlock(scheme: ColorScheme, doc: Document) {
  const values = themeVars(scheme, doc);
  const lines = THEME_VARS.map(v => values[v.name]
    ? `  ${v.name}: ${values[v.name]}; /* ${v.description} */`
    : `  /* ${v.name}: ; ${v.description} ${$localize`(not set)`} */`);
  return `body.${scheme}-theme {\n${lines.join('\n')}\n}`;
}
 
/**
 * Starting point for a new custom theme: every standard CSS variable for
 * light and dark mode, pre-filled with the current values.
 */
export function newTheme(doc: Document = document) {
  return `/*
 * ${$localize`Custom theme`}
 *
 * ${$localize`Jasper adds .light-theme or .dark-theme to <body> depending on the colour scheme,`}
 * ${$localize`so set colours for both. Every standard CSS variable is listed below with the`}
 * ${$localize`value currently in use. Change what you want and delete the rest.`}
 *
 * ${$localize`Row structure and reusable classes for theming:`}
 * https://github.com/cjmalloy/jasper-ui/blob/master/docs/STRUCTURE.md
 */
 
/* ${$localize`Light theme`} */
${themeBlock('light', doc)}
 
/* ${$localize`Dark theme`} */
${themeBlock('dark', doc)}
 
/*
 * ${$localize`Add your own rules below. Use the row slots (.ref, .ext, .link, .info, .actions, ...)`}
 * ${$localize`so your theme keeps working as Jasper changes. For example:`}
 *
 * .ref.list-item > .thumbnail { display: none; }
 * .list-item > .info { font-size: 12px; }
 * body.dark-theme .ext.list-item > .link { color: var(--tag); }
 */
`;
}