diff --git a/.changeset/clever-hounds-reflect.md b/.changeset/clever-hounds-reflect.md new file mode 100644 index 0000000000..143e6f8058 --- /dev/null +++ b/.changeset/clever-hounds-reflect.md @@ -0,0 +1,10 @@ +--- +'@evidence-dev/evidence': major +'@evidence-dev/component-utilities': major +'@evidence-dev/preprocess': major +'@evidence-dev/sdk': major +'@evidence-dev/core-components': major +'@evidence-dev/tailwind': major +--- + +Theming & Appearances diff --git a/.changeset/cuddly-geckos-tan.md b/.changeset/cuddly-geckos-tan.md new file mode 100644 index 0000000000..94b6d6c071 --- /dev/null +++ b/.changeset/cuddly-geckos-tan.md @@ -0,0 +1,5 @@ +--- +'@evidence-dev/icons': patch +--- + +Icons use currentColor instead of hardcoded color diff --git a/.changeset/large-tips-end.md b/.changeset/large-tips-end.md new file mode 100644 index 0000000000..e74c815283 --- /dev/null +++ b/.changeset/large-tips-end.md @@ -0,0 +1,5 @@ +--- +'@evidence-dev/evidence': patch +--- + +hash parquet file paths after build diff --git a/.eslintignore b/.eslintignore index b7030fc977..bed939e9e6 100644 --- a/.eslintignore +++ b/.eslintignore @@ -31,4 +31,6 @@ packages/evidence/scripts/svelte.config.js packages/ui/icons/src # Don't lint playwright reports -**/playwright-report* \ No newline at end of file +**/playwright-report* + +storybook-static \ No newline at end of file diff --git a/e2e/base-path/tests/tests.spec.js b/e2e/base-path/tests/tests.spec.js index b820f5026a..43ccadb494 100644 --- a/e2e/base-path/tests/tests.spec.js +++ b/e2e/base-path/tests/tests.spec.js @@ -108,7 +108,7 @@ test.describe('Page', () => { await expect(page.getByText('This is Page B', { exact: true })).toBeVisible(); await expect(new URL(page.url()).pathname).toBe(`${basePath}/page-b/`); - const logoLink = await page.getByAltText('Home'); + const logoLink = await page.getByAltText('Home').first(); await logoLink.click(); await waitForPageToLoad(page); diff --git a/e2e/test-utils.js b/e2e/test-utils.js index 415bd6a450..6241c43f2c 100644 --- a/e2e/test-utils.js +++ b/e2e/test-utils.js @@ -1,3 +1,5 @@ +// @ts-check + /** @typedef {import('@playwright/test').Page} Page */ import { expect } from '@playwright/test'; @@ -18,3 +20,109 @@ export function waitForWasm(page) { }); }); } + +/** + * @param {import('@playwright/test').Page} page + * @returns {Promise} + */ +export const isKebabMenuOpen = async (page) => { + try { + const menu = await page.getByRole('menu', { name: 'Menu' }); + const isVisible = await menu.isVisible(); + return isVisible; + } catch (e) { + console.error('isKebabMenuOpen failed', e); + throw e; + } +}; + +/** + * @param {import('@playwright/test').Page} page + * @returns {Promise} True if the kebab menu was opened, false if it was already open + */ +export const openKebabMenu = async (page) => { + if (await isKebabMenuOpen(page)) return false; + + try { + // Click button to open menu + await page.getByRole('button', { name: 'Menu' }).click(); + // Make sure menu is open + await expect(page.getByRole('menu', { name: 'Menu' })).toBeVisible(); + return true; + } catch (e) { + console.error('openKebabMenu failed', e); + throw e; + } +}; + +/** + * @param {import('@playwright/test').Page} page + * @returns {Promise} True if the kebab menu was closed, false if it was already closed + */ +export const closeKebabMenu = async (page) => { + if (!(await isKebabMenuOpen(page))) return false; + + try { + // Click button to close menu + await page.getByRole('button', { name: 'Menu' }).click(); + // Make sure menu is closed + await expect(page.getByRole('menu', { name: 'Menu' })).not.toBeVisible(); + return true; + } catch (e) { + console.error('closeKebabMenu failed', e); + throw e; + } +}; + +/** + * @param {import('@playwright/test').Page} page + * @param {{ closeKebabMenu?: boolean }} options + * @returns {Promise<'light' | 'dark' | 'system'>} + */ +const getAppearance = async (page, options = { closeKebabMenu: true }) => { + try { + await openKebabMenu(page); + + const textContent = await page.getByRole('menuitem', { name: 'Appearance' }).textContent(); + const appearance = textContent?.split(/\s+/)[1].toLowerCase(); + if (!appearance || !['light', 'dark', 'system'].includes(appearance)) { + throw new Error(`Invalid appearance ${appearance}`); + } + return /** @type {'light' | 'dark' | 'system'} */ (appearance); + } catch (e) { + console.error('getAppearance failed', e); + throw e; + } finally { + if (options.closeKebabMenu) { + await closeKebabMenu(page); + } + } +}; + +/** + * @param {import('@playwright/test').Page} page + * @param {'light' | 'dark' | 'system'} appearance + */ +export const switchAppearance = async (page, appearance) => { + try { + let current = await getAppearance(page); + if (current === appearance) return; + + await openKebabMenu(page); + // Switch appearance a max of 2 times (since there's 3 options) + for (let i = 0; i < 2; i++) { + await page.getByRole('menuitem', { name: 'Appearance' }).click(); + current = await getAppearance(page, { closeKebabMenu: false }); + if (current === appearance) break; + } + + if (current !== appearance) { + throw new Error(`Appearance ${current} doesnt match ${appearance} after switching`); + } + } catch (e) { + console.error('switchAppearance failed', e); + throw e; + } finally { + await closeKebabMenu(page); + } +}; diff --git a/e2e/themes/evidence.config.yaml b/e2e/themes/evidence.config.yaml index 98e20012a3..915e0b418b 100644 --- a/e2e/themes/evidence.config.yaml +++ b/e2e/themes/evidence.config.yaml @@ -1,8 +1,18 @@ -themes: - light: - mySemanticColor: "#ff0000" - dark: - mySemanticColor: "#00ff00" +appearance: + default: light + switcher: true + +theme: + colors: + base: + light: "#fdf4ff" + dark: "#170118" + primary: + light: "#ff0000" + dark: "#00ff00" + myCustomColor: + light: "#abcdef" + dark: "#fedcba" plugins: components: diff --git a/e2e/themes/package.json b/e2e/themes/package.json index 8e7ca07b69..9e238b2d8e 100644 --- a/e2e/themes/package.json +++ b/e2e/themes/package.json @@ -2,11 +2,11 @@ "name": "e2e-themes", "version": "0.0.19", "scripts": { - "build": "cross-env VITE_EVIDENCE_THEMES=true evidence build", - "build:strict": "cross-env VITE_EVIDENCE_THEMES=true evidence build:strict", - "dev": "cross-env VITE_EVIDENCE_THEMES=true evidence dev", + "build": "cross-env evidence build", + "build:strict": "cross-env evidence build:strict", + "dev": "cross-env evidence dev", "sources": "evidence sources", - "preview": "cross-env VITE_EVIDENCE_THEMES=true evidence preview", + "preview": "cross-env evidence preview", "test:preview": "playwright test", "test:dev": "cross-env DEV=true playwright test" }, diff --git a/e2e/themes/pages/index.md b/e2e/themes/pages/index.md index edfc95bf2f..1da87b7063 100644 --- a/e2e/themes/pages/index.md +++ b/e2e/themes/pages/index.md @@ -1,2 +1,17 @@ -
+
+ div-primary-class
+ +
+ div-primary-var +
+ +
+ div-myCustomColor-class +
+ +
+ div-myCustomColor-var +
+ +This is some body text diff --git a/e2e/themes/tests/tests.spec.js b/e2e/themes/tests/tests.spec.js index 9bb68809a8..2430eb0557 100644 --- a/e2e/themes/tests/tests.spec.js +++ b/e2e/themes/tests/tests.spec.js @@ -1,23 +1,51 @@ // @ts-check import { test, expect } from '@playwright/test'; -import { waitForPageToLoad } from '../../test-utils'; +import { switchAppearance, waitForPageToLoad } from '../../test-utils'; -test('should change color based on theme', async ({ page }) => { +test('should change colors based on theme', async ({ page }) => { await page.goto('/'); await waitForPageToLoad(page); - const divWithBackground = await page.getByTestId('div-with-background'); + const divPrimaryClass = await page.getByTestId('div-primary-class'); + const divPrimaryVar = await page.getByTestId('div-primary-var'); + const divMyCustomColorClass = await page.getByTestId('div-myCustomColor-class'); + const divMyCustomColorVar = await page.getByTestId('div-myCustomColor-var'); - // Starts with system theme (dark) - await expect(divWithBackground).toHaveCSS('background-color', 'rgb(0, 255, 0)'); + await switchAppearance(page, 'system'); + await expect(divPrimaryClass).toHaveCSS('background-color', 'rgb(0, 255, 0)'); + await expect(divPrimaryVar).toHaveCSS('background-color', 'rgb(0, 255, 0)'); + await expect(divMyCustomColorClass).toHaveCSS('background-color', 'rgb(254, 220, 186)'); + await expect(divMyCustomColorVar).toHaveCSS('background-color', 'rgb(254, 220, 186)'); - await page.getByLabel('Menu').click(); + await switchAppearance(page, 'light'); + await expect(divPrimaryClass).toHaveCSS('background-color', 'rgb(255, 0, 0)'); + await expect(divPrimaryVar).toHaveCSS('background-color', 'rgb(255, 0, 0)'); + await expect(divMyCustomColorClass).toHaveCSS('background-color', 'rgb(171, 205, 239)'); + await expect(divMyCustomColorVar).toHaveCSS('background-color', 'rgb(171, 205, 239)'); - // Light theme - await page.getByRole('menuitem', { name: 'Appearance' }).click(); - await expect(divWithBackground).toHaveCSS('background-color', 'rgb(255, 0, 0)'); + await switchAppearance(page, 'dark'); + await expect(divPrimaryClass).toHaveCSS('background-color', 'rgb(0, 255, 0)'); + await expect(divPrimaryVar).toHaveCSS('background-color', 'rgb(0, 255, 0)'); + await expect(divMyCustomColorClass).toHaveCSS('background-color', 'rgb(254, 220, 186)'); + await expect(divMyCustomColorVar).toHaveCSS('background-color', 'rgb(254, 220, 186)'); +}); + +test('body text should be computed from base', async ({ page }) => { + await page.goto('/'); + await waitForPageToLoad(page); + + const body = await page.locator('body'); + const text = await page.getByText('This is some body text'); + + await switchAppearance(page, 'system'); + await expect(text).toHaveCSS('color', 'rgb(205, 200, 206)'); + await expect(body).toHaveCSS('background-color', 'rgb(23, 1, 24)'); + + await switchAppearance(page, 'light'); + await expect(text).toHaveCSS('color', 'rgb(45, 42, 46)'); + await expect(body).toHaveCSS('background-color', 'rgb(253, 244, 255)'); - // Dark theme - await page.getByRole('menuitem', { name: 'Appearance' }).click(); - await expect(divWithBackground).toHaveCSS('background-color', 'rgb(0, 255, 0)'); + await switchAppearance(page, 'dark'); + await expect(text).toHaveCSS('color', 'rgb(205, 200, 206)'); + await expect(body).toHaveCSS('background-color', 'rgb(23, 1, 24)'); }); diff --git a/packages/evidence/cli.js b/packages/evidence/cli.js index 72c3c07e04..f3d6f516fa 100755 --- a/packages/evidence/cli.js +++ b/packages/evidence/cli.js @@ -10,6 +10,7 @@ import sade from 'sade'; import { logQueryEvent } from '@evidence-dev/telemetry'; import { enableDebug } from '@evidence-dev/sdk/utils'; import { loadEnv } from 'vite'; +import { createHash } from 'crypto'; const increaseNodeMemoryLimit = () => { // Don't override the memory limit if it's already set @@ -152,6 +153,11 @@ const watchPatterns = [ } ]; +function removeStaticDir(dir) { + const staticlessDir = path.normalize(dir).split(path.sep).slice(1); + return path.join(...staticlessDir); +} + const strictMode = function () { process.env['VITE_BUILD_STRICT'] = true; }; @@ -159,6 +165,8 @@ const buildHelper = function (command, args) { const watchers = runFileWatcher(watchPatterns); const flatArgs = flattenArguments(args); + const dataDir = process.env.EVIDENCE_DATA_DIR ?? './static/data'; + // Run svelte kit build in the hidden directory const child = spawn(command, flatArgs, { shell: true, @@ -174,8 +182,42 @@ const buildHelper = function (command, args) { }); // Copy the outputs to the root of the project upon successful exit child.on('exit', function (code) { + const outDir = '.evidence/template/build'; if (code === 0) { - fs.copySync('./.evidence/template/build', './build'); + const staticlessDataDir = removeStaticDir(dataDir); + const buildDataDir = path.join(outDir, staticlessDataDir); + const manifestFile = path.join(buildDataDir, 'manifest.json'); + + if (fs.existsSync(manifestFile)) { + const manifest = fs.readJsonSync(manifestFile); + for (const files of Object.values(manifest.renderedFiles)) { + for (let i = 0; i < files.length; i++) { + // /sqlite/transactions/transactions.parquet + // ^^^^^^ ^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^ + const nDiskParts = 3; + + const diskParts = files[i].split('/').slice(-nDiskParts).join('/'); + const filePath = path.posix.join(buildDataDir, diskParts); + if (!fs.existsSync(filePath)) continue; + + const contents = fs.readFileSync(filePath); + const hash = createHash('md5').update(contents).digest('hex'); + + const newDiskPart = path.posix.join( + path.dirname(diskParts), + hash, + path.basename(diskParts) + ); + const newFilePath = path.join(buildDataDir, newDiskPart); + fs.moveSync(filePath, newFilePath); + + files[i] = files[i].replace(diskParts, newDiskPart); + } + } + fs.writeJsonSync(manifestFile, manifest); + } + + fs.copySync(outDir, './build'); console.log(`Build complete --> ${process.env.EVIDENCE_BUILD_DIR ?? './build'} `); } else { console.error('Build failed'); diff --git a/packages/evidence/scripts/build-template.js b/packages/evidence/scripts/build-template.js index 21bb2f8d9b..b9730fe36c 100644 --- a/packages/evidence/scripts/build-template.js +++ b/packages/evidence/scripts/build-template.js @@ -53,6 +53,7 @@ fsExtra.outputFileSync( import { sourceQueryHmr, configVirtual, queryDirectoryHmr } from '@evidence-dev/sdk/build/vite'; import { isDebug } from '@evidence-dev/sdk/utils'; import { log } from "@evidence-dev/sdk/logger"; + import { evidenceThemes } from '@evidence-dev/tailwind/vite-plugin'; const logger = createLogger(); @@ -60,7 +61,7 @@ fsExtra.outputFileSync( /** @type {import('vite').UserConfig} */ const config = { - plugins: [sveltekit(), configVirtual(), queryDirectoryHmr, sourceQueryHmr()], + plugins: [sveltekit(), configVirtual(), queryDirectoryHmr, sourceQueryHmr(), evidenceThemes()], optimizeDeps: { include: ['echarts-stat', 'echarts', 'blueimp-md5', 'nanoid', '@uwdata/mosaic-sql', // We need these to prevent HMR from doing a full page reload @@ -74,7 +75,7 @@ fsExtra.outputFileSync( ]) ], - exclude: ['svelte-icons', '@evidence-dev/universal-sql', '$evidence/config'] + exclude: ['svelte-icons', '@evidence-dev/universal-sql', '$evidence/config', '$evidence/themes'] }, ssr: { external: ['@evidence-dev/telemetry', 'blueimp-md5', 'nanoid', '@uwdata/mosaic-sql', '@evidence-dev/sdk/plugins'] diff --git a/packages/lib/component-utilities/jsconfig.json b/packages/lib/component-utilities/jsconfig.json new file mode 100644 index 0000000000..d4eeb9d470 --- /dev/null +++ b/packages/lib/component-utilities/jsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "types": ["@evidence-dev/tailwind"] + } +} diff --git a/packages/lib/component-utilities/package.json b/packages/lib/component-utilities/package.json index e4881d2e0e..53aeafdc45 100644 --- a/packages/lib/component-utilities/package.json +++ b/packages/lib/component-utilities/package.json @@ -10,6 +10,7 @@ "author": "", "license": "MIT", "devDependencies": { + "@evidence-dev/tailwind": "workspace:^", "@faker-js/faker": "^8.0.2", "vitest": "^2.0.5" }, diff --git a/packages/lib/component-utilities/src/colours.js b/packages/lib/component-utilities/src/colours.js deleted file mode 100644 index ea85e6e058..0000000000 --- a/packages/lib/component-utilities/src/colours.js +++ /dev/null @@ -1,80 +0,0 @@ -export const uiColours = { - blue100: 'hsla(202, 100%, 95%, 1)', - blue200: 'hsla(204, 100%, 85%, 1)', - blue300: 'hsla(206, 95%, 72%, 1)', - blue400: 'hsla(208, 90%, 63%, 1)', - blue500: 'hsla(210, 85%, 54%, 1)', - blue600: 'hsla(212, 96%, 44%, 1)', - blue700: 'hsla(214, 98%, 38%, 1)', - blue800: 'hsla(217, 98%, 33%, 1)', - blue900: 'hsla(220, 99%, 24%, 1)', - blue999: 'hsla(222, 100%, 18%, 1)', - bluelink: 'hsla(205, 62%, 38%, 1)', - green100: 'hsla(167, 100%, 94%, 1)', - green200: 'hsla(166, 100%, 87%, 1)', - green300: 'hsla(163, 93%, 76%, 1)', - green400: 'hsla(161, 90%, 63%, 1)', - green500: 'hsla(159, 88%, 44%, 1)', - green600: 'hsla(158, 91%, 35%, 1)', - green700: 'hsla(156, 93%, 28%, 1)', - green800: 'hsla(154, 95%, 23%, 1)', - green900: 'hsla(152, 100%, 18%, 1)', - green999: 'hsla(150, 100%, 14%, 1)', - grey100: 'hsla(217, 33%, 97%, 1)', - grey200: 'hsla(215, 15%, 91%, 1)', - grey300: 'hsla(211, 16%, 82%, 1)', - grey400: 'hsla(212, 13%, 65%, 1)', - grey500: 'hsla(212, 10%, 53%, 1)', - grey600: 'hsla(212, 12%, 43%, 1)', - grey700: 'hsla(210, 14%, 37%, 1)', - grey800: 'hsla(210, 18%, 30%, 1)', - grey900: 'hsla(210, 20%, 25%, 1)', - grey999: 'hsla(211, 24%, 16%, 1)', - yellow100: 'hsl(49, 100%, 96%, 1)', - yellow200: 'hsl(48, 100%, 88%, 1)', - yellow300: 'hsl(48, 95%, 76%, 1)', - yellow400: 'hsl(48, 94%, 68%, 1)', - yellow500: 'hsl(44, 92%, 63%, 1)', - yellow600: 'hsl(42, 87%, 55%, 1)', - yellow700: 'hsl(36, 77%, 49%, 1)', - yellow800: 'hsl(29, 80%, 44%, 1)', - yellow900: 'hsl(22, 82%, 39%, 1)', - yellow999: 'hsl(15, 86%, 30%, 1)' -}; - -export const chartColours = [ - 'hsla(207, 65%, 39%, 1)', // Navy - 'hsla(195, 49%, 51%, 1)', // Teal - 'hsla(207, 69%, 79%, 1)', // Light Blue - 'hsla(202, 28%, 65%, 1)', // Grey - 'hsla(179, 37%, 65%, 1)', // Light Green - 'hsla(40, 30%, 75%, 1)', // Tan - 'hsla(38, 89%, 62%, 1)', // Yellow - 'hsla(342, 40%, 40%, 1)', // Maroon - 'hsla(207, 86%, 70%, 1)', // Blue - 'hsla(160, 40%, 46%, 1)', // Green - // Grey Scale - '#71777d', - '#7e848a', - '#8c9196', - '#9a9fa3', - '#a8acb0', - '#b7babd', - '#c5c8ca', - '#d4d6d7', - '#e3e4e5', - '#f3f3f3' -]; - -export const mapColours = [ - 'hsla(207, 65%, 39%, 1)', // Navy - 'hsla(195, 49%, 51%, 1)', // Teal - 'hsla(207, 69%, 79%, 1)', // Light Blue - 'hsla(202, 28%, 65%, 1)', // Grey - 'hsla(179, 37%, 65%, 1)', // Light Green - 'hsla(40, 30%, 75%, 1)', // Tan - 'hsla(38, 89%, 62%, 1)', // Yellow - 'hsla(342, 40%, 40%, 1)', // Maroon - 'hsla(207, 86%, 70%, 1)', // Blue - 'hsla(160, 40%, 46%, 1)' // Green -]; diff --git a/packages/lib/component-utilities/src/echarts.js b/packages/lib/component-utilities/src/echarts.js index 897daaf5fe..f1b5e3784f 100644 --- a/packages/lib/component-utilities/src/echarts.js +++ b/packages/lib/component-utilities/src/echarts.js @@ -1,5 +1,5 @@ import { registerTheme, init, connect } from 'echarts'; -import { evidenceThemeLight } from './echartsThemes'; +import { evidenceThemeDark, evidenceThemeLight } from './echartsThemes'; import debounce from 'debounce'; import * as chartWindowDebug from './chartWindowDebug'; @@ -7,14 +7,16 @@ import * as chartWindowDebug from './chartWindowDebug'; * @typedef {import("echarts").EChartsOption & { * dispatch?: ReturnType; * showAllXAxisLabels?: boolean; + * theme: 'light' | 'dark'; * } - * } ActionParams + * } EChartsActionOptions */ const ANIMATION_DURATION = 500; -/** @type {import("svelte/action").Action} */ -export default (node, option) => { +/** @param {HTMLElement} node */ +/** @param {EChartsActionOptions} options */ +const echartsAction = (node, options) => { // https://github.com/evidence-dev/evidence/issues/1323 const useSvg = ['iPad Simulator', 'iPhone Simulator', 'iPod Simulator', 'iPad', 'iPhone', 'iPod'].includes( @@ -22,18 +24,25 @@ export default (node, option) => { // ios breaks w/ canvas if the canvas is too large ) && node.clientWidth * 3 * node.clientHeight * 3 > 16777215; - registerTheme('evidence-light', evidenceThemeLight); + registerTheme('light', evidenceThemeLight); + registerTheme('dark', evidenceThemeDark); - const chart = init(node, 'evidence-light', { - renderer: useSvg ? 'svg' : (option.renderer ?? 'canvas') - }); + let chart; + + const initChart = () => { + chart = init(node, options.theme, { + renderer: useSvg ? 'svg' : (options.renderer ?? 'canvas') + }); + }; + + initChart(); chartWindowDebug.set(chart.id, chart); // If connectGroup supplied, connect chart to other charts matching that connectGroup - if (option.connectGroup) { - chart.group = option.connectGroup; - connect(option.connectGroup); + if (options.connectGroup) { + chart.group = options.connectGroup; + connect(options.connectGroup); } // This function applies overrides to the echarts config generated by our chart components. @@ -44,19 +53,19 @@ export default (node, option) => { // Series Color override const applySeriesColors = () => { - if (option.seriesColors) { + if (options.seriesColors) { /** @type {import("echarts").EChartsOption} */ const prevOption = chart.getOption(); if (!prevOption) return; const newOption = { ...prevOption }; - for (const seriesName of Object.keys(option.seriesColors)) { + for (const seriesName of Object.keys(options.seriesColors)) { const matchingSeriesIndex = prevOption.series.findIndex((s) => s.name === seriesName); if (matchingSeriesIndex !== -1) { newOption.series[matchingSeriesIndex] = { ...newOption.series[matchingSeriesIndex], itemStyle: { ...newOption.series[matchingSeriesIndex].itemStyle, - color: option.seriesColors[seriesName] + color: options.seriesColors[seriesName] } }; } @@ -67,9 +76,9 @@ export default (node, option) => { // Check if echartsOptions are provided and apply them const applyEchartsOptions = () => { - if (option.echartsOptions) { + if (options.echartsOptions) { chart.setOption({ - ...option.echartsOptions + ...options.echartsOptions }); } }; @@ -77,8 +86,8 @@ export default (node, option) => { // seriesOptions - loop through series and apply same changes to each const applySeriesOptions = () => { let tempSeries = []; - if (option.seriesOptions) { - const reference_index = option.config.series.reduce( + if (options.seriesOptions) { + const reference_index = options.config.series.reduce( (acc, { evidenceSeriesType }, reference_index) => { if ( evidenceSeriesType === 'reference_line' || @@ -92,11 +101,11 @@ export default (node, option) => { [] ); - for (let i = 0; i < option.config.series.length; i++) { + for (let i = 0; i < options.config.series.length; i++) { if (reference_index.includes(i)) { tempSeries.push({}); } else { - tempSeries.push({ ...option.seriesOptions }); + tempSeries.push({ ...options.seriesOptions }); } } chart.setOption({ series: tempSeries }); @@ -105,7 +114,7 @@ export default (node, option) => { // Initial options set: chart.setOption({ - ...option.config, + ...options.config, animationDuration: ANIMATION_DURATION, animationDurationUpdate: ANIMATION_DURATION }); @@ -115,7 +124,7 @@ export default (node, option) => { applySeriesOptions(); // Click event handler: - const dispatch = option.dispatch; + const dispatch = options.dispatch; chart.on('click', function (params) { dispatch('click', params); }); @@ -141,7 +150,7 @@ export default (node, option) => { // Label width setting: const updateLabelWidths = () => { - if (option.showAllXAxisLabels) { + if (options.showAllXAxisLabels) { // Make sure we operate on an up-to-date options object /** @type {import("echarts").EChartsOption} */ const prevOption = chart.getOption(); @@ -157,7 +166,7 @@ export default (node, option) => { // We disable this behavior because it doesn't make sense on horizontal bar charts // Category labels will grow to be visible // Value labels are interpolatable anyway - if (!option.swapXY) { + if (!options.swapXY) { /** @type {import("echarts").EChartsOption} */ const newOption = { xAxis: { @@ -173,11 +182,18 @@ export default (node, option) => { } }; - const updateChart = (newOption) => { - option = newOption; + /** @param {EChartsActionOptions} newOptions */ + const updateChart = (newOptions) => { + if (newOptions.theme !== options.theme) { + chart.dispose(); + options = newOptions; + initChart(); + } + + options = newOptions; chart.setOption( { - ...option.config, + ...options.config, animationDuration: ANIMATION_DURATION, animationDurationUpdate: ANIMATION_DURATION }, @@ -199,9 +215,9 @@ export default (node, option) => { window[Symbol.for('chart renders')] ??= 0; window[Symbol.for('chart renders')]++; return { - update(option) { + update(options) { window[Symbol.for('chart renders')]++; - updateChart(option); + updateChart(options); }, destroy() { if (resizeObserver) { @@ -215,3 +231,5 @@ export default (node, option) => { } }; }; + +export default echartsAction; diff --git a/packages/lib/component-utilities/src/echartsCanvasDownload.js b/packages/lib/component-utilities/src/echartsCanvasDownload.js index 54156ef0fe..d215309f1a 100644 --- a/packages/lib/component-utilities/src/echartsCanvasDownload.js +++ b/packages/lib/component-utilities/src/echartsCanvasDownload.js @@ -1,11 +1,17 @@ import { registerTheme, init } from 'echarts'; -import { evidenceThemeLight } from './echartsThemes'; +import { evidenceThemeLight, evidenceThemeDark } from './echartsThemes'; import download from 'downloadjs'; -export default (node, option) => { - registerTheme('evidence-light', evidenceThemeLight); +/** @typedef {{ theme: 'light' | 'dark'; backgroundColor: string }} EChartsCanvasDownloadActionOptions */ - const chart = init(node, 'evidence-light', { renderer: 'canvas' }); +/** @param {HTMLElement} node */ +/** @param {EChartsCanvasDownloadActionOptions} option */ +const echartsCanvasDownloadAction = (node, option) => { + registerTheme('light', evidenceThemeLight); + registerTheme('dark', evidenceThemeDark); + + console.log('echartsCanvasDownloadAction', option.theme); + const chart = init(node, option.theme, { renderer: 'canvas' }); option.config.animation = false; @@ -79,7 +85,7 @@ export default (node, option) => { let src = chart.getConnectedDataURL({ type: 'png', pixelRatio: 3, - backgroundColor: 'white', + backgroundColor: option.backgroundColor, excludeComponents: ['toolbox'] }); @@ -102,3 +108,5 @@ export default (node, option) => { } }; }; + +export default echartsCanvasDownloadAction; diff --git a/packages/lib/component-utilities/src/echartsThemes.js b/packages/lib/component-utilities/src/echartsThemes.js index f4085bd7ea..f51774125a 100644 --- a/packages/lib/component-utilities/src/echartsThemes.js +++ b/packages/lib/component-utilities/src/echartsThemes.js @@ -1,857 +1,429 @@ -import { chartColours, uiColours } from './colours'; +import { themes } from '$evidence/themes'; -// Light Mode Theme -const light_axisBaselineColor = uiColours.grey500; -const light_axisTickColor = uiColours.grey500; -const light_axisLabelColor = uiColours.grey500; -const light_gridlineColor = uiColours.grey200; -const light_axisTitleBackgroundColor = 'white'; -const light_legendTextColor = uiColours.grey500; -const light_legendPageIconColor = uiColours.grey600; -const light_legendPageTextColor = uiColours.grey500; -const light_tooltipBorderColor = uiColours.grey400; -const light_tooltipBackgroundColor = 'white'; -const light_tooltipTextColor = uiColours.grey900; -const light_titleColor = uiColours.grey800; -const light_subtitleColor = uiColours.grey700; +/** @param {'light' | 'dark'} mode */ +const createTheme = (mode) => { + const axisBaselineColor = themes[mode].colors['base-content-muted']; + const axisTickColor = themes[mode].colors['base-content-muted']; + const axisLabelColor = themes[mode].colors['base-content-muted']; + const gridlineColor = themes[mode].colors['base-300']; + const axisTitleBackgroundColor = themes[mode].colors['base-100']; + const legendTextColor = themes[mode].colors['base-content-muted']; + const legendPageIconColor = themes[mode].colors['base-content-muted']; + const legendPageTextColor = themes[mode].colors['base-content-muted']; + const tooltipBorderColor = themes[mode].colors['base-300']; + const tooltipBackgroundColor = themes[mode].colors['base-100']; + const tooltipTextColor = themes[mode].colors['base-content']; + const titleColor = themes[mode].colors['base-heading']; + const subtitleColor = themes[mode].colors['base-content-muted']; -// Dark Mode Theme -const dark_axisBaselineColor = uiColours.grey500; -const dark_axisTickColor = uiColours.grey500; -const dark_axisLabelColor = uiColours.grey500; -const dark_gridlineColor = uiColours.grey900; -const dark_axisTitleBackgroundColor = '#212121'; -const dark_legendTextColor = uiColours.grey500; -const dark_legendPageIconColor = uiColours.grey500; -const dark_legendPageIconInactiveColor = uiColours.grey900; -const dark_legendPageTextColor = uiColours.grey600; -const dark_legendInactiveColor = uiColours.grey800; -const dark_tooltipBorderColor = uiColours.grey600; -const dark_tooltipBackgroundColor = uiColours.grey900; -const dark_tooltipTextColor = uiColours.grey200; -const dark_titleColor = uiColours.grey400; -const dark_subtitleColor = uiColours.grey400; - -export const evidenceThemeLight = { - darkMode: false, // if true, echarts will automatically update the font colour to work better on dark background - textStyle: { - fontFamily: ['Inter', 'sans-serif'] - }, - grid: { - left: '0%', - right: '4%', - bottom: '0%', - top: '15%', - containLabel: true - }, - color: chartColours, - backgroundColor: 'rgba(255, 255, 255, 0)', - title: { - padding: 0, - itemGap: 7, + return { + darkMode: mode === 'dark', // if true, echarts will automatically update the font colour to work better on dark background + backgroundColor: themes[mode].colors['base-100'], textStyle: { - fontSize: 14, - color: light_titleColor - }, - subtextStyle: { - fontSize: 13, - color: light_subtitleColor, - overflow: 'break' - }, - top: '0%' - }, - line: { - itemStyle: { - borderWidth: 0 - }, - lineStyle: { - width: 2, - join: 'round' - }, - symbolSize: 0, - symbol: 'circle', - smooth: false - }, - radar: { - itemStyle: { - borderWidth: 0 - }, - lineStyle: { - width: 2 - }, - symbolSize: 0, - symbol: 'circle', - smooth: false - }, - pie: { - itemStyle: { - borderWidth: 0, - borderColor: '#cccccc' - } - }, - scatter: { - itemStyle: { - borderWidth: 0, - borderColor: '#cccccc' - } - }, - boxplot: { - itemStyle: { - borderWidth: 1.5 - // borderColor: '#cccccc' - } - }, - parallel: { - itemStyle: { - borderWidth: 0, - borderColor: '#cccccc' - } - }, - sankey: { - itemStyle: { - borderWidth: 0, - borderColor: '#cccccc' - } - }, - funnel: { - itemStyle: { - borderWidth: 0, - borderColor: '#cccccc' - } - }, - gauge: { - itemStyle: { - borderWidth: 0, - borderColor: '#cccccc' - } - }, - candlestick: { - itemStyle: { - color: '#eb5454', - color0: '#47b262', - borderColor: '#eb5454', - borderColor0: '#47b262', - borderWidth: 1 - } - }, - graph: { - itemStyle: { - borderWidth: 0, - borderColor: '#cccccc' - }, - lineStyle: { - width: 1, - color: '#aaaaaa' - }, - symbolSize: 0, - symbol: 'circle', - smooth: false, - color: [ - '#923d59', - '#488f96', - '#518eca', - '#b3a9a0', - '#ffc857', - '#495867', - '#bfdbf7', - '#bc4749', - '#eeebd0' - ], - label: { - color: '#f2f2f2' - } - }, - map: { - itemStyle: { - areaColor: '#eee', - borderColor: '#444', - borderWidth: 0.5 - }, - label: { - color: '#000' - }, - emphasis: { - itemStyle: { - areaColor: 'rgba(255,215,0,0.8)', - borderColor: '#444', - borderWidth: 1 + fontFamily: ['Inter', 'sans-serif'] + }, + grid: { + left: '0%', + right: '4%', + bottom: '0%', + top: '15%', + containLabel: true + }, + color: themes[mode].colorPalettes.default, + title: { + padding: 0, + itemGap: 7, + textStyle: { + fontSize: 14, + color: titleColor }, - label: { - color: 'rgb(100,0,0)' - } - } - }, - geo: { - itemStyle: { - areaColor: '#eee', - borderColor: '#444', - borderWidth: 0.5 - }, - label: { - color: '#000' + subtextStyle: { + fontSize: 13, + color: subtitleColor, + overflow: 'break' + }, + top: '0%' }, - emphasis: { + line: { itemStyle: { - areaColor: 'rgba(255,215,0,0.8)', - borderColor: '#444', - borderWidth: 1 + borderWidth: 0 }, - label: { - color: 'rgb(100,0,0)' - } - } - }, - categoryAxis: { - axisLine: { - show: true, - lineStyle: { - color: light_axisBaselineColor - } - }, - axisTick: { - show: false, lineStyle: { - color: light_axisTickColor + width: 2, + join: 'round' }, - length: 3, - alignWithLabel: true + symbolSize: 0, + symbol: 'circle', + smooth: false }, - axisLabel: { - show: true, - color: light_axisLabelColor - }, - splitLine: { - show: false, - lineStyle: { - color: [light_gridlineColor] - } - }, - splitArea: { - show: false, - areaStyle: { - color: ['rgba(250,250,250,0.2)', 'rgba(210,219,238,0.2)'] - } - } - }, - valueAxis: { - axisLine: { - show: false, - lineStyle: { - color: light_axisBaselineColor - } - }, - axisTick: { - show: false, - lineStyle: { - color: light_axisTickColor + radar: { + itemStyle: { + borderWidth: 0 }, - length: 2 - }, - axisLabel: { - show: true, - color: light_axisLabelColor - }, - splitLine: { - show: true, - lineStyle: { - color: [light_gridlineColor], - width: 1 - } - }, - splitArea: { - show: false, - areaStyle: { - color: ['rgba(250,250,250,0.2)', 'rgba(210,219,238,0.2)'] - } - }, - nameTextStyle: { - backgroundColor: light_axisTitleBackgroundColor - } - }, - logAxis: { - axisLine: { - show: false, - lineStyle: { - color: light_axisBaselineColor - } - }, - axisTick: { - show: false, lineStyle: { - color: light_axisTickColor + width: 2 }, - length: 2 + symbolSize: 0, + symbol: 'circle', + smooth: false }, - axisLabel: { - show: true, - color: light_axisLabelColor - }, - splitLine: { - show: true, - lineStyle: { - color: [light_gridlineColor] + pie: { + itemStyle: { + borderWidth: 0, + borderColor: '#cccccc' } }, - splitArea: { - show: false, - areaStyle: { - color: ['rgba(250,250,250,0.2)', 'rgba(210,219,238,0.2)'] + scatter: { + itemStyle: { + borderWidth: 0, + borderColor: '#cccccc' } }, - nameTextStyle: { - backgroundColor: light_axisTitleBackgroundColor - } - }, - timeAxis: { - axisLine: { - show: true, - lineStyle: { - color: light_axisBaselineColor + boxplot: { + itemStyle: { + borderWidth: 1.5 + // borderColor: '#cccccc' } }, - axisTick: { - show: true, - lineStyle: { - color: light_axisTickColor - }, - length: 3 - }, - axisLabel: { - show: true, - color: light_axisLabelColor - }, - splitLine: { - show: false, - lineStyle: { - color: [light_gridlineColor] + parallel: { + itemStyle: { + borderWidth: 0, + borderColor: '#cccccc' } }, - splitArea: { - show: false, - areaStyle: { - color: ['rgba(250,250,250,0.2)', 'rgba(210,219,238,0.2)'] + sankey: { + itemStyle: { + borderWidth: 0, + borderColor: '#cccccc' } - } - }, - toolbox: { - iconStyle: { - borderColor: '#999999' }, - emphasis: { - iconStyle: { - borderColor: '#459cde' + funnel: { + itemStyle: { + borderWidth: 0, + borderColor: '#cccccc' } - } - }, - legend: { - textStyle: { - padding: [0, 0, 0, -7], - color: light_legendTextColor - }, - // "padding": [15,0,0,0], - icon: 'circle', - pageIcons: { - horizontal: [ - 'M 17 3 h 2 c 0.386 0 0.738 0.223 0.904 0.572 s 0.115 0.762 -0.13 1.062 L 11.292 15 l 8.482 10.367 c 0.245 0.299 0.295 0.712 0.13 1.062 S 19.386 27 19 27 h -2 c -0.3 0 -0.584 -0.135 -0.774 -0.367 l -9 -11 c -0.301 -0.369 -0.301 -0.898 0 -1.267 l 9 -11 C 16.416 3.135 16.7 3 17 3 Z', - 'M 12 27 h -2 c -0.386 0 -0.738 -0.223 -0.904 -0.572 s -0.115 -0.762 0.13 -1.062 L 17.708 15 L 9.226 4.633 c -0.245 -0.299 -0.295 -0.712 -0.13 -1.062 S 9.614 3 10 3 h 2 c 0.3 0 0.584 0.135 0.774 0.367 l 9 11 c 0.301 0.369 0.301 0.898 0 1.267 l -9 11 C 12.584 26.865 12.3 27 12 27 Z' - ] }, - pageIconColor: light_legendPageIconColor, - pageIconSize: 12, - pageTextStyle: { - color: light_legendPageTextColor - }, - pageButtonItemGap: -2, - animationDurationUpdate: 300 - }, - tooltip: { - axisPointer: { - lineStyle: { - color: '#cccccc', - width: 1 - }, - crossStyle: { - color: '#cccccc', - width: 1 + gauge: { + itemStyle: { + borderWidth: 0, + borderColor: '#cccccc' } }, - borderRadius: 4, - borderWidth: 1, - borderColor: light_tooltipBorderColor, - backgroundColor: light_tooltipBackgroundColor, - textStyle: { - color: light_tooltipTextColor, - fontSize: 12, - fontWeight: 400 - }, - padding: 6 - }, - timeline: { - lineStyle: { - color: '#e3e3e3', - width: 2 - }, - itemStyle: { - color: '#d6d6d6', - borderWidth: 1 - }, - controlStyle: { - color: '#bfbfbf', - borderColor: '#bfbfbf', - borderWidth: 1 - }, - checkpointStyle: { - color: '#8f8f8f', - borderColor: '#ffffff' - }, - label: { - color: '#c9c9c9' - }, - emphasis: { + candlestick: { itemStyle: { - color: '#9c9c9c' - }, - controlStyle: { - color: '#bfbfbf', - borderColor: '#bfbfbf', + color: '#eb5454', + color0: '#47b262', + borderColor: '#eb5454', + borderColor0: '#47b262', borderWidth: 1 - }, - label: { - color: '#c9c9c9' } - } - }, - visualMap: { - color: ['#c41621', '#e39588', '#f5ed98'] - }, - dataZoom: { - handleSize: 'undefined%', - textStyle: {} - }, - markPoint: { - label: { - color: '#f2f2f2' }, - emphasis: { + graph: { + itemStyle: { + borderWidth: 0, + borderColor: '#cccccc' + }, + lineStyle: { + width: 1, + color: '#aaaaaa' + }, + symbolSize: 0, + symbol: 'circle', + smooth: false, + color: [ + '#923d59', + '#488f96', + '#518eca', + '#b3a9a0', + '#ffc857', + '#495867', + '#bfdbf7', + '#bc4749', + '#eeebd0' + ], label: { color: '#f2f2f2' } - } - } -}; - -export const evidenceThemeDark = { - darkMode: true, // if true, echarts will automatically update the font colour to work better on dark background - textStyle: { - fontFamily: 'sans-serif' - }, - grid: { - left: '0%', - right: '4%', - bottom: '0%', - top: '15%', - containLabel: true - }, - color: chartColours, - backgroundColor: 'rgba(255, 255, 255, 0)', - title: { - padding: 0, - itemGap: 7, - textStyle: { - fontSize: 14, - color: dark_titleColor - }, - subtextStyle: { - fontSize: 13, - color: dark_subtitleColor, - overflow: 'break' - }, - top: '0%' - }, - line: { - itemStyle: { - borderWidth: 0 - }, - lineStyle: { - width: 2, - join: 'round' }, - symbolSize: 0, - symbol: 'circle', - smooth: false - }, - radar: { - itemStyle: { - borderWidth: 0 - }, - lineStyle: { - width: 2 - }, - symbolSize: 0, - symbol: 'circle', - smooth: false - }, - bar: { - itemStyle: { - barBorderWidth: 1, - barBorderColor: '#cccccc' - } - }, - pie: { - itemStyle: { - borderWidth: 0, - borderColor: '#cccccc' - } - }, - scatter: { - itemStyle: { - borderWidth: 0, - borderColor: '#cccccc' - } - }, - boxplot: { - itemStyle: { - borderWidth: 0, - borderColor: '#cccccc' - } - }, - parallel: { - itemStyle: { - borderWidth: 0, - borderColor: '#cccccc' - } - }, - sankey: { - itemStyle: { - borderWidth: 0, - borderColor: '#cccccc' - } - }, - funnel: { - itemStyle: { - borderWidth: 0, - borderColor: '#cccccc' - } - }, - gauge: { - itemStyle: { - borderWidth: 0, - borderColor: '#cccccc' - } - }, - candlestick: { - itemStyle: { - color: '#eb5454', - color0: '#47b262', - borderColor: '#eb5454', - borderColor0: '#47b262', - borderWidth: 1 - } - }, - graph: { - itemStyle: { - borderWidth: 0, - borderColor: '#cccccc' - }, - lineStyle: { - width: 1, - color: '#aaaaaa' - }, - symbolSize: 0, - symbol: 'circle', - smooth: false, - color: [ - '#923d59', - '#488f96', - '#518eca', - '#b3a9a0', - '#ffc857', - '#495867', - '#bfdbf7', - '#bc4749', - '#eeebd0' - ], - label: { - color: '#f2f2f2' - } - }, - map: { - itemStyle: { - areaColor: '#eee', - borderColor: '#444', - borderWidth: 0.5 - }, - label: { - color: '#000' - }, - emphasis: { + map: { itemStyle: { - areaColor: 'rgba(255,215,0,0.8)', + areaColor: '#eee', borderColor: '#444', - borderWidth: 1 + borderWidth: 0.5 }, label: { - color: 'rgb(100,0,0)' - } - } - }, - geo: { - itemStyle: { - areaColor: '#eee', - borderColor: '#444', - borderWidth: 0.5 - }, - label: { - color: '#000' - }, - emphasis: { + color: '#000' + }, + emphasis: { + itemStyle: { + areaColor: 'rgba(255,215,0,0.8)', + borderColor: '#444', + borderWidth: 1 + }, + label: { + color: 'rgb(100,0,0)' + } + } + }, + geo: { itemStyle: { - areaColor: 'rgba(255,215,0,0.8)', + areaColor: '#eee', borderColor: '#444', - borderWidth: 1 + borderWidth: 0.5 }, label: { - color: 'rgb(100,0,0)' - } - } - }, - categoryAxis: { - axisLine: { - show: true, - lineStyle: { - color: dark_axisBaselineColor - } - }, - axisTick: { - show: false, - lineStyle: { - color: dark_axisTickColor + color: '#000' }, - length: 3, - alignWithLabel: true - }, - axisLabel: { - show: true, - color: dark_axisLabelColor - }, - splitLine: { - show: false, - lineStyle: { - color: [dark_gridlineColor] - } - }, - splitArea: { - show: false, - areaStyle: { - color: ['rgba(250,250,250,0.2)', 'rgba(210,219,238,0.2)'] - } - } - }, - valueAxis: { - axisLine: { - show: false, - lineStyle: { - color: dark_axisBaselineColor - } - }, - axisTick: { - show: false, - lineStyle: { - color: dark_axisTickColor + emphasis: { + itemStyle: { + areaColor: 'rgba(255,215,0,0.8)', + borderColor: '#444', + borderWidth: 1 + }, + label: { + color: 'rgb(100,0,0)' + } + } + }, + categoryAxis: { + axisLine: { + show: true, + lineStyle: { + color: axisBaselineColor + } }, - length: 2 - }, - axisLabel: { - show: true, - color: dark_axisLabelColor - }, - splitLine: { - show: true, - lineStyle: { - color: [dark_gridlineColor], - width: 1 - } - }, - splitArea: { - show: false, - areaStyle: { - color: ['rgba(250,250,250,0.2)', 'rgba(210,219,238,0.2)'] - } - }, - nameTextStyle: { - backgroundColor: dark_axisTitleBackgroundColor - } - }, - logAxis: { - axisLine: { - show: false, - lineStyle: { - color: dark_axisBaselineColor - } - }, - axisTick: { - show: false, - lineStyle: { - color: dark_axisTickColor + axisTick: { + show: false, + lineStyle: { + color: axisTickColor + }, + length: 3, + alignWithLabel: true }, - length: 2 - }, - axisLabel: { - show: true, - color: dark_axisLabelColor - }, - splitLine: { - show: true, - lineStyle: { - color: [dark_gridlineColor] - } - }, - splitArea: { - show: false, - areaStyle: { - color: ['rgba(250,250,250,0.2)', 'rgba(210,219,238,0.2)'] - } - }, - nameTextStyle: { - backgroundColor: dark_axisTitleBackgroundColor - } - }, - timeAxis: { - axisLine: { - show: true, - lineStyle: { - color: dark_axisBaselineColor + axisLabel: { + show: true, + color: axisLabelColor + }, + splitLine: { + show: false, + lineStyle: { + color: [gridlineColor] + } + }, + splitArea: { + show: false, + areaStyle: { + color: ['rgba(250,250,250,0.2)', 'rgba(210,219,238,0.2)'] + } + } + }, + valueAxis: { + axisLine: { + show: false, + lineStyle: { + color: axisBaselineColor + } + }, + axisTick: { + show: false, + lineStyle: { + color: axisTickColor + }, + length: 2 + }, + axisLabel: { + show: true, + color: axisLabelColor + }, + splitLine: { + show: true, + lineStyle: { + color: [gridlineColor], + width: 1 + } + }, + splitArea: { + show: false, + areaStyle: { + color: ['rgba(250,250,250,0.2)', 'rgba(210,219,238,0.2)'] + } + }, + nameTextStyle: { + backgroundColor: axisTitleBackgroundColor } }, - axisTick: { - show: true, - lineStyle: { - color: dark_axisTickColor + logAxis: { + axisLine: { + show: false, + lineStyle: { + color: axisBaselineColor + } }, - length: 3 - }, - axisLabel: { - show: true, - color: dark_axisLabelColor - }, - splitLine: { - show: false, - lineStyle: { - color: [dark_gridlineColor] + axisTick: { + show: false, + lineStyle: { + color: axisTickColor + }, + length: 2 + }, + axisLabel: { + show: true, + color: axisLabelColor + }, + splitLine: { + show: true, + lineStyle: { + color: [gridlineColor] + } + }, + splitArea: { + show: false, + areaStyle: { + color: ['rgba(250,250,250,0.2)', 'rgba(210,219,238,0.2)'] + } + }, + nameTextStyle: { + backgroundColor: axisTitleBackgroundColor } }, - splitArea: { - show: false, - areaStyle: { - color: ['rgba(250,250,250,0.2)', 'rgba(210,219,238,0.2)'] + timeAxis: { + axisLine: { + show: true, + lineStyle: { + color: axisBaselineColor + } + }, + axisTick: { + show: true, + lineStyle: { + color: axisTickColor + }, + length: 3 + }, + axisLabel: { + show: true, + color: axisLabelColor + }, + splitLine: { + show: false, + lineStyle: { + color: [gridlineColor] + } + }, + splitArea: { + show: false, + areaStyle: { + color: ['rgba(250,250,250,0.2)', 'rgba(210,219,238,0.2)'] + } } - } - }, - toolbox: { - iconStyle: { - borderColor: '#999999' }, - emphasis: { + toolbox: { iconStyle: { - borderColor: '#459cde' + borderColor: '#999999' + }, + emphasis: { + iconStyle: { + borderColor: '#459cde' + } } - } - }, - legend: { - textStyle: { - padding: [0, 0, 0, -7], - color: dark_legendTextColor - }, - inactiveColor: dark_legendInactiveColor, - // "padding": [15,0,0,0], - icon: 'circle', - pageIcons: { - horizontal: [ - 'M 17 3 h 2 c 0.386 0 0.738 0.223 0.904 0.572 s 0.115 0.762 -0.13 1.062 L 11.292 15 l 8.482 10.367 c 0.245 0.299 0.295 0.712 0.13 1.062 S 19.386 27 19 27 h -2 c -0.3 0 -0.584 -0.135 -0.774 -0.367 l -9 -11 c -0.301 -0.369 -0.301 -0.898 0 -1.267 l 9 -11 C 16.416 3.135 16.7 3 17 3 Z', - 'M 12 27 h -2 c -0.386 0 -0.738 -0.223 -0.904 -0.572 s -0.115 -0.762 0.13 -1.062 L 17.708 15 L 9.226 4.633 c -0.245 -0.299 -0.295 -0.712 -0.13 -1.062 S 9.614 3 10 3 h 2 c 0.3 0 0.584 0.135 0.774 0.367 l 9 11 c 0.301 0.369 0.301 0.898 0 1.267 l -9 11 C 12.584 26.865 12.3 27 12 27 Z' - ] }, - pageIconColor: dark_legendPageIconColor, - pageIconInactiveColor: dark_legendPageIconInactiveColor, - pageIconSize: 12, - pageTextStyle: { - color: dark_legendPageTextColor + legend: { + textStyle: { + padding: [0, 0, 0, -7], + color: legendTextColor + }, + // "padding": [15,0,0,0], + icon: 'circle', + pageIcons: { + horizontal: [ + 'M 17 3 h 2 c 0.386 0 0.738 0.223 0.904 0.572 s 0.115 0.762 -0.13 1.062 L 11.292 15 l 8.482 10.367 c 0.245 0.299 0.295 0.712 0.13 1.062 S 19.386 27 19 27 h -2 c -0.3 0 -0.584 -0.135 -0.774 -0.367 l -9 -11 c -0.301 -0.369 -0.301 -0.898 0 -1.267 l 9 -11 C 16.416 3.135 16.7 3 17 3 Z', + 'M 12 27 h -2 c -0.386 0 -0.738 -0.223 -0.904 -0.572 s -0.115 -0.762 0.13 -1.062 L 17.708 15 L 9.226 4.633 c -0.245 -0.299 -0.295 -0.712 -0.13 -1.062 S 9.614 3 10 3 h 2 c 0.3 0 0.584 0.135 0.774 0.367 l 9 11 c 0.301 0.369 0.301 0.898 0 1.267 l -9 11 C 12.584 26.865 12.3 27 12 27 Z' + ] + }, + pageIconColor: legendPageIconColor, + pageIconSize: 12, + pageTextStyle: { + color: legendPageTextColor + }, + pageButtonItemGap: -2, + animationDurationUpdate: 300 + }, + tooltip: { + axisPointer: { + lineStyle: { + color: '#cccccc', + width: 1 + }, + crossStyle: { + color: '#cccccc', + width: 1 + } + }, + borderRadius: 4, + borderWidth: 1, + borderColor: tooltipBorderColor, + backgroundColor: tooltipBackgroundColor, + textStyle: { + color: tooltipTextColor, + fontSize: 12, + fontWeight: 400 + }, + padding: 6 }, - pageButtonItemGap: -2, - animationDurationUpdate: 300 - }, - tooltip: { - axisPointer: { + timeline: { lineStyle: { - color: '#cccccc', - width: 1 + color: '#e3e3e3', + width: 2 }, - crossStyle: { - color: '#cccccc', - width: 1 - } - }, - borderRadius: 4, - borderWidth: 1, - borderColor: dark_tooltipBorderColor, - backgroundColor: dark_tooltipBackgroundColor, - textStyle: { - color: dark_tooltipTextColor, - fontSize: 12, - fontWeight: 400 - }, - padding: 6 - }, - timeline: { - lineStyle: { - color: '#e3e3e3', - width: 2 - }, - itemStyle: { - color: '#d6d6d6', - borderWidth: 1 - }, - controlStyle: { - color: '#bfbfbf', - borderColor: '#bfbfbf', - borderWidth: 1 - }, - checkpointStyle: { - color: '#8f8f8f', - borderColor: '#ffffff' - }, - label: { - color: '#c9c9c9' - }, - emphasis: { itemStyle: { - color: '#9c9c9c' + color: '#d6d6d6', + borderWidth: 1 }, controlStyle: { color: '#bfbfbf', borderColor: '#bfbfbf', borderWidth: 1 }, + checkpointStyle: { + color: '#8f8f8f', + borderColor: '#ffffff' + }, label: { color: '#c9c9c9' - } - } - }, - visualMap: { - color: ['#c41621', '#e39588', '#f5ed98'] - }, - dataZoom: { - handleSize: 'undefined%', - textStyle: {} - }, - markPoint: { - label: { - color: '#f2f2f2' - }, - emphasis: { + }, + emphasis: { + itemStyle: { + color: '#9c9c9c' + }, + controlStyle: { + color: '#bfbfbf', + borderColor: '#bfbfbf', + borderWidth: 1 + }, + label: { + color: '#c9c9c9' + } + } + }, + visualMap: { + color: ['#c41621', '#e39588', '#f5ed98'] + }, + dataZoom: { + handleSize: 'undefined%', + textStyle: {} + }, + markPoint: { label: { color: '#f2f2f2' + }, + emphasis: { + label: { + color: '#f2f2f2' + } } } - } + }; }; + +export const evidenceThemeLight = createTheme('light'); +export const evidenceThemeDark = createTheme('dark'); diff --git a/packages/lib/component-utilities/src/generateBoxPlotData.js b/packages/lib/component-utilities/src/generateBoxPlotData.js index 4491f80114..0424d5bac2 100644 --- a/packages/lib/component-utilities/src/generateBoxPlotData.js +++ b/packages/lib/component-utilities/src/generateBoxPlotData.js @@ -1,4 +1,29 @@ -export default function generateBoxPlotData( +// @ts-check + +/** @template T @typedef {import('svelte/store').Readable} Readable */ + +/** + * @typedef {{ + * data: unknown[][] + * names: string[] + * colors: Readable<(string | undefined)[]> + * }} BoxPlotData + */ + +/** + * @param {unknown[]} data + * @param {string} min + * @param {string} minInterval + * @param {string} midpoint + * @param {string} maxInterval + * @param {string} max + * @param {string} name + * @param {string | undefined} color + * @param {string} confidenceInterval + * @param {(color: unknown[]) => Readable<(string | undefined)[]>} resolveColor + * @returns {BoxPlotData} + */ +export const generateBoxPlotData = ( data, min = undefined, minInterval, @@ -7,8 +32,9 @@ export default function generateBoxPlotData( max = undefined, name, color, - confidenceInterval = undefined -) { + confidenceInterval = undefined, + resolveColor +) => { let boxData = { data: [], names: [], @@ -34,8 +60,11 @@ export default function generateBoxPlotData( boxData.names.push(data[i][name]); - boxData.colors.push(data[i][color]); + boxData.colors.push(data[i][color] ?? color); } - return boxData; -} + return { + ...boxData, + colors: resolveColor(boxData.colors) + }; +}; diff --git a/packages/lib/component-utilities/src/stores.js b/packages/lib/component-utilities/src/stores.js index d96584de1e..e22e609d04 100644 --- a/packages/lib/component-utilities/src/stores.js +++ b/packages/lib/component-utilities/src/stores.js @@ -55,7 +55,7 @@ function createToastsObject() { }; } -/** @typedef {"error" | "warning" | "success" | "info"} ToastStatus */ +/** @typedef {"negative" | "warning" | "positive" | "info"} ToastStatus */ /** @typedef {{ id: string; status?: ToastStatus; title: string; message: string; }} Toast */ /** @type {import('svelte/store').Readable & { add: (toast: Toast, timeout: number) => void }} */ export const toasts = createToastsObject(); @@ -71,15 +71,21 @@ const getStoreVal = (store) => { return v; }; +/** @template T @typedef {{ serialize: (value: T) => string; deserialize: (raw: string) => T }} SerializeAndDeserialize */ + /** * Implementation of a writable store that also saves it's values to localStorage * @template T * @param {string} key localStorage key * @param {T} init + * @param {SerializeAndDeserialize} [serializeAndDeserialize] * @returns {Writable} */ -export const localStorageStore = (key, init) => { - const store = writable(browser ? (JSON.parse(localStorage.getItem(key)) ?? init) : init); +export const localStorageStore = (key, init, serializeAndDeserialize) => { + const serialize = serializeAndDeserialize?.serialize ?? JSON.stringify; + const deserialize = serializeAndDeserialize?.deserialize ?? JSON.parse; + + const store = writable(browser ? (deserialize(localStorage.getItem(key)) ?? init) : init); const { subscribe, set } = store; /** @type {(v: T) => void} */ @@ -88,7 +94,7 @@ export const localStorageStore = (key, init) => { if (typeof v === 'undefined' || v === null) { localStorage.removeItem(key); } else { - localStorage.setItem(key, JSON.stringify(v)); + localStorage.setItem(key, serialize(v)); } } }; diff --git a/packages/lib/preprocess/src/extract-queries/get-query-ids.fixture.cjs b/packages/lib/preprocess/src/extract-queries/get-query-ids.fixture.cjs index 67057c29d0..576df3c6d5 100644 --- a/packages/lib/preprocess/src/extract-queries/get-query-ids.fixture.cjs +++ b/packages/lib/preprocess/src/extract-queries/get-query-ids.fixture.cjs @@ -108,7 +108,6 @@ z = x + y \`\`\`css pre { overflow: scroll; - background: var(--grey-800); border-radius: 3px; display: flex; flex-direction: row; diff --git a/packages/lib/sdk/src/build-dev/svelte/processors/transformQueries/transformQueries.js b/packages/lib/sdk/src/build-dev/svelte/processors/transformQueries/transformQueries.js index 915583b74b..911f753f65 100644 --- a/packages/lib/sdk/src/build-dev/svelte/processors/transformQueries/transformQueries.js +++ b/packages/lib/sdk/src/build-dev/svelte/processors/transformQueries/transformQueries.js @@ -34,7 +34,7 @@ export const transformQueries = { target.classList.add('text-xs'); target.classList.add('leading-tight'); - target.classList.add('bg-gray-200'); + target.classList.add('bg-base-300'); target.classList.add('p-2'); target.classList.add('w-fit'); target.classList.add('min-w-[60ch]'); diff --git a/packages/lib/sdk/src/configuration/getEvidenceConfig.js b/packages/lib/sdk/src/configuration/getEvidenceConfig.js index a50c18d67e..7f6625dfb7 100644 --- a/packages/lib/sdk/src/configuration/getEvidenceConfig.js +++ b/packages/lib/sdk/src/configuration/getEvidenceConfig.js @@ -13,7 +13,7 @@ import { unnestZodError } from '../lib/unnest-zod-error.js'; /** @typedef {z.infer} EvidenceConfig */ /** - * @template {AnyZodObject} [Schema=EvidenceConfigSchema] + * @template {z.ZodSchema} [Schema=EvidenceConfigSchema] * @param {Schema} [schema] * @returns {import("zod").infer} */ diff --git a/packages/lib/sdk/src/utils/svelte/inputs.js b/packages/lib/sdk/src/utils/svelte/inputs.js index ff02101de2..34d6b54bc1 100644 --- a/packages/lib/sdk/src/utils/svelte/inputs.js +++ b/packages/lib/sdk/src/utils/svelte/inputs.js @@ -16,7 +16,7 @@ export const InputStoreKey = Symbol('InputStore'); * @param {unknown} v * @returns {v is Readable} */ -const isReadable = (v) => { +export const isReadable = (v) => { if (typeof v !== 'object') return false; if (v === null) return false; return 'subscribe' in v; diff --git a/packages/ui/core-components/.storybook/main.js b/packages/ui/core-components/.storybook/main.js index b4b0d33663..958ee22932 100644 --- a/packages/ui/core-components/.storybook/main.js +++ b/packages/ui/core-components/.storybook/main.js @@ -1,10 +1,12 @@ import { mergeConfig } from 'vite'; +import { evidenceThemes } from '@evidence-dev/tailwind/vite-plugin'; /** @type { import('@storybook/sveltekit').StorybookConfig } */ const config = { stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx|svelte)'], addons: [ '@storybook/addon-essentials', + '@storybook/addon-themes', '@storybook/addon-interactions', '@storybook/addon-svelte-csf', '@chromatic-com/storybook' @@ -14,6 +16,7 @@ const config = { }, async viteFinal(config) { return mergeConfig(config, { + plugins: [evidenceThemes()], server: { fs: { strict: false diff --git a/packages/ui/core-components/.storybook/preview.js b/packages/ui/core-components/.storybook/preview.js index 5170de3549..50c32f27a7 100644 --- a/packages/ui/core-components/.storybook/preview.js +++ b/packages/ui/core-components/.storybook/preview.js @@ -1,3 +1,5 @@ +import { withThemeByDataAttribute } from '@storybook/addon-themes'; + import '../src/app.postcss'; import WithEvidence from '../src/lib/storybook-helpers/WithEvidence.svelte'; import { initialize } from '../src/lib/storybook-helpers/initializeUSQL.js'; @@ -20,7 +22,17 @@ const preview = { evidenceInclude: { table: { disable: true } }, series: { table: { disable: true } } }, - decorators: [() => WithEvidence], + decorators: [ + withThemeByDataAttribute({ + themes: { + light: 'light', + dark: 'dark' + }, + attributeName: 'data-theme', + defaultTheme: 'light' + }), + () => WithEvidence + ], loaders: [ async () => ({ usqlLoaded: await initialize() diff --git a/packages/ui/core-components/jsconfig.json b/packages/ui/core-components/jsconfig.json index b9a47da4db..cf0567566d 100644 --- a/packages/ui/core-components/jsconfig.json +++ b/packages/ui/core-components/jsconfig.json @@ -9,6 +9,6 @@ "moduleResolution": "NodeNext", "module": "NodeNext", "strict": true, - "types": ["@evidence-dev/sdk"] + "types": ["@evidence-dev/sdk", "@evidence-dev/tailwind"] } } diff --git a/packages/ui/core-components/package.json b/packages/ui/core-components/package.json index 754b9d766c..a35d703d72 100644 --- a/packages/ui/core-components/package.json +++ b/packages/ui/core-components/package.json @@ -47,7 +47,7 @@ "@steeze-ui/simple-icons": "^1.7.1", "@steeze-ui/svelte-icon": "1.5.0", "@steeze-ui/tabler-icons": "2.1.1", - "@storybook/test": "^8.2.8", + "@storybook/test": "^8.3.4", "@types/leaflet": "^1.9.12", "bits-ui": "^0.21.9", "chroma-js": "^2.4.2", @@ -77,24 +77,26 @@ "@evidence-dev/sdk": "workspace:*", "@evidence-dev/universal-sql": "workspace:*", "@fakerjs/faker": "^3.0.0", - "@storybook/addon-essentials": "^8.1.3", - "@storybook/addon-interactions": "^8.1.3", - "@storybook/addon-links": "^8.1.3", + "@storybook/addon-essentials": "^8.3.4", + "@storybook/addon-interactions": "^8.3.4", + "@storybook/addon-links": "^8.3.4", "@storybook/addon-svelte-csf": "^4.1.3", - "@storybook/blocks": "^8.1.3", - "@storybook/builder-vite": "^8.1.10", + "@storybook/addon-themes": "^8.3.4", + "@storybook/blocks": "^8.3.4", + "@storybook/builder-vite": "^8.3.4", "@storybook/jest": "^0.2.3", "@storybook/manager-api": "^8.1.6", - "@storybook/svelte": "^8.1.3", - "@storybook/sveltekit": "^8.1.3", + "@storybook/svelte": "^8.3.4", + "@storybook/sveltekit": "^8.3.4", "@storybook/testing-library": "^0.2.2", - "@storybook/theming": "^8.1.3", + "@storybook/theming": "^8.3.4", "@sveltejs/adapter-auto": "3.1.1", "@sveltejs/kit": "2.8.4", "@sveltejs/package": "^2.3.1", "@sveltejs/vite-plugin-svelte": "3.0.2", + "@types/chroma-js": "^2.4.4", "@types/lodash.debounce": "^4.0.9", - "@vitest/coverage-v8": "^2.0.5", + "@vitest/coverage-v8": "2.0.5", "autoprefixer": "^10.4.19", "chromatic": "^11.4.0", "eslint": "8.45.0", @@ -109,7 +111,7 @@ "publint": "^0.1.16", "react": "^17.0.2", "react-dom": "^17.0.2", - "storybook": "^8.2.8", + "storybook": "^8.3.4", "svelte": "4.2.19", "svelte-check": "3.6.7", "svelte-preprocess": "5.1.3", @@ -117,7 +119,7 @@ "tslib": "^2.6.2", "typescript": "5.4.2", "vite": "5.4.11", - "vitest": "^2.0.5" + "vitest": "2.0.5" }, "overrides": { "svelte2tsx": "^0.6.15" diff --git a/packages/ui/core-components/src/lib/atoms/accordion/Accordion.stories.svelte b/packages/ui/core-components/src/lib/atoms/accordion/Accordion.stories.svelte index c1027e955b..d9aae97009 100644 --- a/packages/ui/core-components/src/lib/atoms/accordion/Accordion.stories.svelte +++ b/packages/ui/core-components/src/lib/atoms/accordion/Accordion.stories.svelte @@ -69,7 +69,7 @@ - +

Content 1

diff --git a/packages/ui/core-components/src/lib/atoms/alert/Alert.stories.svelte b/packages/ui/core-components/src/lib/atoms/alert/Alert.stories.svelte index c4659f817f..138e9d5769 100644 --- a/packages/ui/core-components/src/lib/atoms/alert/Alert.stories.svelte +++ b/packages/ui/core-components/src/lib/atoms/alert/Alert.stories.svelte @@ -6,16 +6,7 @@ argTypes: { content: { control: 'text' }, status: { - options: ['default', 'info', 'danger', 'success', 'warning'], - control: { - labels: { - default: 'Default', - info: 'Info', - danger: 'Danger', - success: 'Success', - warning: 'Warning' - } - } + options: ['base', 'info', 'negative', 'positive', 'warning'] }, sticky: { control: 'boolean' @@ -44,12 +35,12 @@ - + This is an alert This is an alert - This is an alert - This is an alert - This is an alert + This is an alert + This is an alert + This is an alert diff --git a/packages/ui/core-components/src/lib/atoms/alert/Alert.svelte b/packages/ui/core-components/src/lib/atoms/alert/Alert.svelte index b0eb3e8eed..17cffdb795 100644 --- a/packages/ui/core-components/src/lib/atoms/alert/Alert.svelte +++ b/packages/ui/core-components/src/lib/atoms/alert/Alert.svelte @@ -1,14 +1,33 @@