From ca061764977951585a6d4f4919ec0ddad1338b0c Mon Sep 17 00:00:00 2001 From: Andrew Stein Date: Tue, 10 Nov 2026 12:04:46 -0500 Subject: [PATCH 1/3] Fix types (again) Signed-off-by: Andrew Stein --- rust/perspective-js/src/rust/table.rs | 3 + rust/perspective-js/src/rust/utils/futures.rs | 4 +- rust/perspective-js/src/rust/utils/mod.rs | 2 + .../src/rust/utils/ts_promise.rs | 51 ++++ .../rust/components/main_panel/reconcile.rs | 52 +--- .../src/rust/components/main_panel/update.rs | 12 +- .../src/rust/custom_elements/viewer.rs | 103 ++++---- rust/perspective-viewer/src/rust/lib.rs | 7 + .../restore_workspace_throttled.spec.ts | 240 ++++++++++++++++++ 9 files changed, 370 insertions(+), 104 deletions(-) create mode 100644 rust/perspective-js/src/rust/utils/ts_promise.rs create mode 100644 rust/perspective-viewer/test/js/multi_panel/restore_workspace_throttled.spec.ts diff --git a/rust/perspective-js/src/rust/table.rs b/rust/perspective-js/src/rust/table.rs index 3c4fa58951..dea5021600 100644 --- a/rust/perspective-js/src/rust/table.rs +++ b/rust/perspective-js/src/rust/table.rs @@ -66,6 +66,9 @@ extern "C" { #[wasm_bindgen(typescript_type = "ViewConfigUpdate")] pub type JsViewConfig; + #[wasm_bindgen(typescript_type = "ViewConfig")] + pub type JsResolvedViewConfig; + #[wasm_bindgen(typescript_type = "UpdateOptions")] pub type JsUpdateOptions; diff --git a/rust/perspective-js/src/rust/utils/futures.rs b/rust/perspective-js/src/rust/utils/futures.rs index f8fc2a1407..f1b32a0a98 100644 --- a/rust/perspective-js/src/rust/utils/futures.rs +++ b/rust/perspective-js/src/rust/utils/futures.rs @@ -24,6 +24,7 @@ use wasm_bindgen::describe::WasmDescribe; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::{JsFuture, future_to_promise}; +use super::TsPromise; use super::errors::*; #[wasm_bindgen] @@ -171,9 +172,10 @@ where impl WasmDescribe for ApiFuture where Result: IntoJsResult + 'static, + T: TsPromise, { fn describe() { - ::describe() + ::describe() } } diff --git a/rust/perspective-js/src/rust/utils/mod.rs b/rust/perspective-js/src/rust/utils/mod.rs index 6464e522b9..c1bc1d517a 100644 --- a/rust/perspective-js/src/rust/utils/mod.rs +++ b/rust/perspective-js/src/rust/utils/mod.rs @@ -21,6 +21,7 @@ mod local_poll_loop; mod serde; #[cfg(feature = "trace-allocator")] mod trace_allocator; +mod ts_promise; pub use browser::*; pub use console_logger::*; @@ -30,3 +31,4 @@ pub use local_poll_loop::*; pub use serde::*; #[cfg(feature = "trace-allocator")] pub(crate) use trace_allocator::get_used; +pub use ts_promise::*; diff --git a/rust/perspective-js/src/rust/utils/ts_promise.rs b/rust/perspective-js/src/rust/utils/ts_promise.rs new file mode 100644 index 0000000000..9692695f4d --- /dev/null +++ b/rust/perspective-js/src/rust/utils/ts_promise.rs @@ -0,0 +1,51 @@ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ +// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ +// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ +// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ +// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ +// ┃ Copyright (c) 2017, the Perspective Authors. ┃ +// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ +// ┃ This file is part of the Perspective library, distributed under the terms ┃ +// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ +// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +use wasm_bindgen::describe::WasmDescribe; +use wasm_bindgen::prelude::*; + +/// The `Promise` TypeScript type an [`super::ApiFuture`] is declared +/// as, carried by a `#[wasm_bindgen]` extern type whose `typescript_type` +/// names it (the wasm-bindgen descriptor interpreter runs no loops, so the +/// name must be a compile-time literal). +pub trait TsPromise { + type Promise: WasmDescribe; +} + +/// Declare the `Promise` TypeScript type of each listed `ApiFuture` +/// payload, e.g. `ts_promise! { View => JsViewPromise = "Promise"; }`. +#[macro_export] +macro_rules! ts_promise { + ($($t:ty => $alias:ident = $ts:literal;)*) => {$( + #[::wasm_bindgen::prelude::wasm_bindgen] + extern "C" { + #[wasm_bindgen(typescript_type = $ts)] + pub type $alias; + } + + impl $crate::utils::TsPromise for $t { + type Promise = $alias; + } + )*}; +} + +ts_promise! { + () => JsVoidPromise = "Promise"; + u32 => JsNumberPromise = "Promise"; + String => JsStringPromise = "Promise"; + Vec => JsUint8ArrayPromise = "Promise"; + Vec => JsStringArrayPromise = "Promise"; + crate::Client => JsClientPromise = "Promise"; + crate::Table => JsTablePromise = "Promise"; + crate::View => JsViewPromise = "Promise"; + crate::JsResolvedViewConfig => JsResolvedViewConfigPromise = "Promise"; +} diff --git a/rust/perspective-viewer/src/rust/components/main_panel/reconcile.rs b/rust/perspective-viewer/src/rust/components/main_panel/reconcile.rs index d8fff1aaf2..fc75e4aa9a 100644 --- a/rust/perspective-viewer/src/rust/components/main_panel/reconcile.rs +++ b/rust/perspective-viewer/src/rust/components/main_panel/reconcile.rs @@ -120,31 +120,17 @@ impl MainPanel { }) } - /// Reconcile the `` grid against `panel_ids`: `insertPanel` - /// newly-rendered cells (flipping each from `display:none` to a visible - /// grid cell) and `removePanel` cells whose panels are gone. + /// Reconcile the `` grid against the live `Workspace` + /// panel set, never the `panel_ids` props snapshot, because Yew's + /// scheduler yield can run this hook after a `restoreWorkspace` staged a + /// layout the snapshot predates: `insertPanel` newly-rendered cells + /// (flipping each from `display:none` to a visible grid cell) and + /// `removePanel` cells whose panels are gone. pub(super) fn reconcile(&mut self, ctx: &Context) { - // The `` element is rendered unconditionally and so is - // stable for this component's lifetime — zero panels is just another - // count (empty insert loop; the retain below removes the last cell, - // collapsing the layout tree to a childless split). Do NOT reset - // `listener_target`/`inserted` on empty: the listeners stay attached - // to the persistent element. let Some(el) = self.layout_ref.cast::() else { return; }; - // Attach the close-detection + active-panel-sync listeners once per - // ELEMENT (the `` element is keyed into a fully-keyed - // sibling list, so Yew reuses one instance for this MainPanel's - // lifetime), and configure resize physics — `GRID_DIVIDER_SIZE` is the - // divider hit tolerance (0 by default → not resizable). The cell-edge - // band it grabs must be left uncovered: each `.rl-panel` carries a - // `margin` (viewer.css) so pointerdowns there reach `regular-layout` - // instead of the frame. Comparing the ELEMENT (not a latched bool) - // self-heals an instance swap: a fresh element boots an EMPTY layout - // tree with no listeners, so everything must be re-bound and every - // placed panel re-inserted (see `listener_target`). if self.listener_target.as_ref() != Some(&el) { if self.listener_target.is_some() { self.inserted.clear(); @@ -175,14 +161,8 @@ impl MainPanel { } let layout: RegularLayout = el.unchecked_into(); - let panel_ids = &ctx.props().panel_ids; - - // `restoreWorkspace` stages its saved layout tree on the Workspace - // (the model; regular-layout is a slave view). Apply it here, BEFORE - // the insert reconcile, and seed `inserted` from its panel names — so - // restored panels mount directly at their saved positions in ONE - // layout commit, never transiting the synthetic equal-split inserts - // below. + let panel_ids = ctx.props().workspace.panel_ids(); + if let Some(tree) = ctx.props().workspace.take_pending_layout() && let Ok(js) = JsValue::from_serde_ext(&tree) { @@ -194,35 +174,21 @@ impl MainPanel { } } - // Insert cells that are newly present. - for id in panel_ids { + for id in &panel_ids { let name = id.as_str(); if self.inserted.iter().any(|n| n == name) { continue; } - // STAGED panels are withheld from the layout: their first draw - // is completing in the hidden staging wrapper (see - // `PanelPhase::Staging`). The promote re-render flips the phase, - // and this loop then inserts the already-drawn panel. if ctx.props().workspace.is_staged(id) { continue; } - // Already placed in the layout tree (the layout is the placement - // source of truth — e.g. a restored tree naming a panel this - // component hasn't tracked yet): record it, never re-split it. if layout.contains_panel(name) { self.inserted.push(name.to_owned()); continue; } - // Insert at this panel's index as a split (orientation = `true` → - // horizontal) rather than the default (path `[]`), which would - // *stack* into the root tab-layout — a stack only renders its - // selected tab, and without frame chrome there's no tab-bar to - // reach the others. Splitting keeps every panel visible - // side-by-side. let index = self.inserted.len(); let path = JsValue::from(js_sys::Array::of1(&JsValue::from_f64(index as f64))); let _ = layout.insert_panel(name, path, JsValue::from_bool(true)); diff --git a/rust/perspective-viewer/src/rust/components/main_panel/update.rs b/rust/perspective-viewer/src/rust/components/main_panel/update.rs index 006705739f..e18e19329f 100644 --- a/rust/perspective-viewer/src/rust/components/main_panel/update.rs +++ b/rust/perspective-viewer/src/rust/components/main_panel/update.rs @@ -43,8 +43,9 @@ impl MainPanel { false } - /// Report closed every panel this commit dropped from the layout, and - /// re-render so its frame leaves the DOM in the commit's paint. + /// Report closed every panel this commit dropped from the layout, judged + /// against the live `Workspace` panel set (see `reconcile`), and re-render + /// so its frame leaves the DOM in the commit's paint. pub(super) fn on_layout_updated(&mut self, ctx: &Context) -> bool { let Some(el) = self.layout_ref.cast::() else { return false; @@ -57,15 +58,14 @@ impl MainPanel { self.panel_resize_observers.clear(); let layout: RegularLayout = el.unchecked_into(); + let panel_ids = ctx.props().workspace.panel_ids(); let mut closed = Vec::new(); let on_screen = &mut self.on_screen; self.inserted.retain(|id| { if layout.contains_panel(id) { on_screen.insert(id.clone()); true - } else if on_screen.remove(id) - || !ctx.props().panel_ids.iter().any(|p| p.as_str() == id) - { + } else if on_screen.remove(id) || !panel_ids.iter().any(|p| p.as_str() == id) { closed.push(id.clone()); false } else { @@ -85,7 +85,7 @@ impl MainPanel { // plugin to match its new cell size — same per-panel // `resize_callback` the insert path uses (idempotent, and each // `Renderer` debounces its own resize). - for id in ctx.props().panel_ids.iter() { + for id in &panel_ids { // Skip any panel regular-layout reports as absent — it's // leaving the layout (a close / drag-out in flight). Spawning // a resize draw for it would race its disposal, and spawned diff --git a/rust/perspective-viewer/src/rust/custom_elements/viewer.rs b/rust/perspective-viewer/src/rust/custom_elements/viewer.rs index 1580bf680f..c6b780d123 100644 --- a/rust/perspective-viewer/src/rust/custom_elements/viewer.rs +++ b/rust/perspective-viewer/src/rust/custom_elements/viewer.rs @@ -20,7 +20,7 @@ use futures::future::join_all; use js_sys::{Array, JsString}; use perspective_client::config::ViewConfigUpdate; use perspective_js::utils::global; -use perspective_js::{JsViewConfig, JsViewWindow, Table, View, apierror}; +use perspective_js::{JsResolvedViewConfig, JsViewWindow, Table, View, apierror, ts_promise}; use wasm_bindgen::JsCast; use wasm_bindgen::prelude::*; use wasm_bindgen_derive::try_from_js_option; @@ -98,18 +98,23 @@ extern "C" { #[wasm_bindgen(typescript_type = "SaveWorkspaceOptions")] pub type JsSaveWorkspaceOptions; + /// `save()` return: a single-panel config. + #[wasm_bindgen(typescript_type = "ViewerConfig")] + pub type JsViewerConfig; + /// `saveWorkspace()` return: a workspace config. - #[wasm_bindgen(typescript_type = "Promise")] - pub type JsWorkspaceConfigPromise; + #[wasm_bindgen(typescript_type = "WorkspaceConfig")] + pub type JsWorkspaceConfig; - /// A `Promise` return, used by the `restore` family (whose - /// `ApiFuture<()>` would otherwise erase to `Promise`). - #[wasm_bindgen(typescript_type = "Promise")] - pub type JsVoidPromise; + /// `export()` return, whose shape follows the `ExportMethod`. + #[wasm_bindgen(typescript_type = "ExportData")] + pub type JsExportData; +} - /// `save()` return: a single-panel config. - #[wasm_bindgen(typescript_type = "Promise")] - pub type JsViewerConfigPromise; +ts_promise! { + JsViewerConfig => JsViewerConfigPromise = "Promise"; + JsWorkspaceConfig => JsWorkspaceConfigPromise = "Promise"; + JsExportData => JsExportDataPromise = "Promise"; } #[derive(serde::Deserialize, Default)] @@ -848,7 +853,10 @@ impl PerspectiveViewerElement { /// server (unlike [`PerspectiveViewerElement::getView`] followed by /// [`View::get_config`]) #[wasm_bindgen] - pub fn getViewConfig(&self, options: Option) -> ApiFuture { + pub fn getViewConfig( + &self, + options: Option, + ) -> ApiFuture { let this = self.clone(); ApiFuture::new(async move { let PanelOptions { panel: name } = parse_options(options)?; @@ -1089,11 +1097,11 @@ impl PerspectiveViewerElement { &self, update: JsViewerConfigUpdate, options: Option, - ) -> JsVoidPromise { + ) -> ApiFuture<()> { let effect = self.workspace.effects().guard(); let this = self.clone(); let saved = saved_version(&update); - let fut = ApiFuture::new_throttled(noting_saved_version(saved, async move { + ApiFuture::new_throttled(noting_saved_version(saved, async move { let RestoreOptions { panel: name, suppress_errors, @@ -1209,9 +1217,7 @@ impl PerspectiveViewerElement { Ok(()) }, } - })); - - js_sys::Promise::from(fut).unchecked_into() + })) } /// Apply a [`WorkspaceConfigUpdate`] to the element — the multi-panel @@ -1243,11 +1249,11 @@ impl PerspectiveViewerElement { &self, update: JsWorkspaceConfigUpdate, options: Option, - ) -> JsVoidPromise { + ) -> ApiFuture<()> { let effect = self.workspace.effects().guard(); let this = self.clone(); let saved = saved_version(&update); - let fut = ApiFuture::new(noting_saved_version(saved, async move { + ApiFuture::new(noting_saved_version(saved, async move { let _effect = effect; let RestoreWorkspaceOptions { wait_for_table } = parse_options(options)?; let missing = MissingTable::from_wait(wait_for_table); @@ -1312,9 +1318,7 @@ impl PerspectiveViewerElement { ejected?; Ok(()) - })); - - js_sys::Promise::from(fut).unchecked_into() + })) } /// If this element is in an _errored_ state, this method will clear it and @@ -1377,9 +1381,9 @@ impl PerspectiveViewerElement { /// await viewer.restore(token); /// }); /// ``` - pub fn save(&self, options: Option) -> JsViewerConfigPromise { + pub fn save(&self, options: Option) -> ApiFuture { let this = self.clone(); - let fut = ApiFuture::new(async move { + ApiFuture::new(async move { let PanelOptions { panel: name } = parse_options(options)?; this.workspace.effects().settle().await; let panel = this.resolve_panel(name)?; @@ -1391,12 +1395,11 @@ impl PerspectiveViewerElement { }) .await?; - viewer_config + Ok(viewer_config .map_table(|name| this.workspace.inline_table_ref(&name)) - .encode() - }); - - js_sys::Promise::from(fut).unchecked_into() + .encode()? + .unchecked_into()) + }) } /// Save the ENTIRE element to a [`WorkspaceConfig`] @@ -1415,14 +1418,14 @@ impl PerspectiveViewerElement { pub fn saveWorkspace( &self, options: Option, - ) -> JsWorkspaceConfigPromise { + ) -> ApiFuture { let this = self.clone(); - let fut = ApiFuture::new(async move { + ApiFuture::new(async move { let SaveWorkspaceOptions { full_palette } = parse_options(options)?; - Self::workspace_config(this, full_palette.unwrap_or(false)).await - }); - - js_sys::Promise::from(fut).unchecked_into() + Ok(Self::workspace_config(this, full_palette.unwrap_or(false)) + .await? + .unchecked_into()) + }) } /// Download this viewer's internal [`View`] data via a browser download @@ -1491,7 +1494,7 @@ impl PerspectiveViewerElement { /// ```javascript /// const data = await viewer.export("plugin"); /// ``` - pub fn export(&self, options: Option) -> ApiFuture { + pub fn export(&self, options: Option) -> ApiFuture { let this = self.clone(); ApiFuture::new(async move { let ExportOptions { @@ -1824,7 +1827,7 @@ impl PerspectiveViewerElement { /// await viewer.restyleElement(); /// ``` #[wasm_bindgen] - pub fn restyleElement(&self) -> ApiFuture { + pub fn restyleElement(&self) -> ApiFuture<()> { clone!(self.workspace); let effect = workspace.effects().guard(); ApiFuture::new(async move { @@ -1837,23 +1840,14 @@ impl PerspectiveViewerElement { panel.renderer.restyle_all().await?; } - Ok(JsValue::UNDEFINED) + Ok(()) }) } #[wasm_bindgen] - pub fn getThemes(&self) -> ApiFuture { + pub fn getThemes(&self) -> ApiFuture> { clone!(self.presentation); - ApiFuture::new(async move { - let x = presentation - .get_available_themes() - .await? - .iter() - .cloned() - .collect::>(); - - Ok(JsValue::from(x)) - }) + ApiFuture::new(async move { Ok(presentation.get_available_themes().await?.to_vec()) }) } /// Set the available theme names available in the status bar UI. @@ -1871,7 +1865,7 @@ impl PerspectiveViewerElement { /// viewer.resetThemes(["Pro Light", "Pro Dark"]) /// ``` #[wasm_bindgen] - pub fn resetThemes(&self, themes: Option>) -> ApiFuture { + pub fn resetThemes(&self, themes: Option>) -> ApiFuture<()> { clone!(self.workspace, self.presentation); let effect = workspace.effects().guard(); ApiFuture::new(async move { @@ -1911,7 +1905,7 @@ impl PerspectiveViewerElement { } presentation.publish_theme_config().await?; - Ok(JsValue::UNDEFINED) + Ok(()) }) } @@ -1957,7 +1951,7 @@ impl PerspectiveViewerElement { /// await viewer.toggleConfig(); /// ``` #[wasm_bindgen] - pub fn toggleConfig(&self, force: Option) -> ApiFuture { + pub fn toggleConfig(&self, force: Option) -> ApiFuture<()> { let effect = self.workspace.effects().guard(); let root = self.root.clone(); ApiFuture::new(async move { @@ -1968,7 +1962,8 @@ impl PerspectiveViewerElement { PerspectiveViewerMsg::ToggleSettingsInit(force, true, Some(sender)), ); - receiver.await.map_err(|_| JsValue::from("Cancelled"))? + receiver.await.map_err(|_| JsValue::from("Cancelled"))??; + Ok(()) }) } @@ -2024,7 +2019,7 @@ impl PerspectiveViewerElement { &self, config: JsViewerConfigInitial, options: Option, - ) -> ApiFuture { + ) -> ApiFuture { clone!(self.elem, self.presentation, self.workspace); let effect = workspace.effects().guard(); let notify = self.layout_changed_notify(); @@ -2044,7 +2039,7 @@ impl PerspectiveViewerElement { MissingTable::from_wait(wait_for_table), ) .await?; - Ok(JsValue::from_str(id.as_str())) + Ok(id.as_str().to_owned()) })) } diff --git a/rust/perspective-viewer/src/rust/lib.rs b/rust/perspective-viewer/src/rust/lib.rs index 2990ed97b5..d9df74db79 100644 --- a/rust/perspective-viewer/src/rust/lib.rs +++ b/rust/perspective-viewer/src/rust/lib.rs @@ -112,6 +112,13 @@ export type * from "../ts-rs/DatetimeFormat.d.ts"; export type * from "../ts-rs/ConfigDefaultsOptions.d.ts"; export type * from "../ts-rs/ColumnConfigDefaultsOptions.d.ts"; +export type ExportData = + | string + | ArrayBuffer + | Blob + | Record + | ViewerConfig; + import type {GetTableOptions} from "../ts-rs/GetTableOptions.d.ts"; import type {PanelOptions} from "../ts-rs/PanelOptions.d.ts"; import type {RestoreOptions} from "../ts-rs/RestoreOptions.d.ts"; diff --git a/rust/perspective-viewer/test/js/multi_panel/restore_workspace_throttled.spec.ts b/rust/perspective-viewer/test/js/multi_panel/restore_workspace_throttled.spec.ts new file mode 100644 index 0000000000..947c5c9fb7 --- /dev/null +++ b/rust/perspective-viewer/test/js/multi_panel/restore_workspace_throttled.spec.ts @@ -0,0 +1,240 @@ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ +// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ +// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ +// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ +// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ +// ┃ Copyright (c) 2017, the Perspective Authors. ┃ +// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ +// ┃ This file is part of the Perspective library, distributed under the terms ┃ +// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ +// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +import type { Page } from "@playwright/test"; +import { test, expect } from "../helpers.ts"; +import { armInvariants, treeSlots, type LayoutTree } from "./harness.ts"; + +const TABLE = "load-viewer-csv"; +const THROTTLE_RATE = 8; +const STACKS = 4; +const PER_STACK = 3; + +function workspace(prefix: string) { + const names = Array.from( + { length: STACKS * PER_STACK }, + (_, i) => `${prefix}-${i}`, + ); + + const layout: LayoutTree = { + type: "split-layout", + orientation: "horizontal", + sizes: Array(STACKS).fill(1 / STACKS), + children: Array.from({ length: STACKS }, (_, i) => ({ + type: "tab-layout", + tabs: names.slice(i * PER_STACK, (i + 1) * PER_STACK), + selected: 0, + })), + }; + + return { + layout, + panels: Object.fromEntries( + names.map((id) => [ + id, + { + table: TABLE, + title: id, + plugin: "Debug", + group_by: ["Region"], + columns: ["Sales"], + }, + ]), + ), + }; +} + +async function throttle(page: Page, rate: number) { + const cdp = await page.context().newCDPSession(page); + await cdp.send("Emulation.setCPUThrottlingRate", { rate }); + return cdp; +} + +function collect_errors(page: Page) { + const errors: string[] = []; + page.on("console", (msg) => { + if (msg.type() === "error") { + errors.push(msg.text()); + } + }); + + page.on("pageerror", (error) => errors.push(error.message)); + return errors; +} + +test.beforeEach(async ({ page }) => { + await page.goto("/rust/perspective-viewer/test/html/superstore.html"); + await page.evaluate(async () => { + while (!window["__TEST_PERSPECTIVE_READY__"]) { + await new Promise((x) => setTimeout(x, 10)); + } + }); +}); + +armInvariants(test); + +test.describe("restoreWorkspace under CPU throttling", () => { + test.skip( + ({ browserName }) => browserName !== "chromium", + "CDP CPU throttling is a Chromium-only API", + ); + + test("restoreWorkspace right after load() keeps every panel", async ({ + page, + }) => { + test.setTimeout(120_000); + const errors = collect_errors(page); + const cdp = await throttle(page, THROTTLE_RATE); + const expected = workspace("panel"); + const result = await page.evaluate(async (config) => { + const worker = (window as any).__TEST_WORKER__; + const viewer = document.createElement("perspective-viewer") as any; + document.body.appendChild(viewer); + await viewer.load(worker); + let error: string | null = null; + try { + await viewer.restoreWorkspace(config, { + wait_for_table: false, + }); + } catch (e) { + error = String(e); + } + + await viewer.flush(); + const saved = await viewer.saveWorkspace(); + return { + error, + names: viewer.getPanelNames(), + panels: Object.keys(saved.panels), + layout: saved.layout, + }; + }, expected); + + await cdp.send("Emulation.setCPUThrottlingRate", { rate: 1 }); + expect(result.error).toBeNull(); + expect(result.names.sort()).toEqual( + Object.keys(expected.panels).sort(), + ); + + expect(result.panels.sort()).toEqual( + Object.keys(expected.panels).sort(), + ); + + expect(treeSlots(result.layout)).toEqual(treeSlots(expected.layout)); + expect(errors.filter((e) => e.includes("Panel disposed"))).toEqual([]); + }); + + test("a second restoreWorkspace replaces every panel", async ({ page }) => { + test.setTimeout(120_000); + const errors = collect_errors(page); + const cdp = await throttle(page, THROTTLE_RATE); + const first = workspace("first"); + const second = workspace("second"); + const result = await page.evaluate( + async ([first, second]) => { + const worker = (window as any).__TEST_WORKER__; + const viewer = document.createElement( + "perspective-viewer", + ) as any; + document.body.appendChild(viewer); + await viewer.load(worker); + let error: string | null = null; + try { + await viewer.restoreWorkspace(first, { + wait_for_table: false, + }); + + await viewer.restoreWorkspace(second, { + wait_for_table: false, + }); + } catch (e) { + error = String(e); + } + + await viewer.flush(); + const saved = await viewer.saveWorkspace(); + return { + error, + names: viewer.getPanelNames(), + panels: Object.keys(saved.panels), + layout: saved.layout, + }; + }, + [first, second], + ); + + await cdp.send("Emulation.setCPUThrottlingRate", { rate: 1 }); + expect(result.error).toBeNull(); + expect(result.names.sort()).toEqual(Object.keys(second.panels).sort()); + expect(result.panels.sort()).toEqual(Object.keys(second.panels).sort()); + + expect(treeSlots(result.layout)).toEqual(treeSlots(second.layout)); + expect(errors.filter((e) => e.includes("Panel disposed"))).toEqual([]); + }); + + test("a layout-only restoreWorkspace keeps the panel set", async ({ + page, + }) => { + test.setTimeout(120_000); + const errors = collect_errors(page); + const cdp = await throttle(page, THROTTLE_RATE); + const initial = workspace("panel"); + const reversed = { + layout: { + ...initial.layout, + children: [...(initial.layout as any).children].reverse(), + } as LayoutTree, + }; + + const result = await page.evaluate( + async ([initial, reversed]) => { + const worker = (window as any).__TEST_WORKER__; + const viewer = document.createElement( + "perspective-viewer", + ) as any; + document.body.appendChild(viewer); + await viewer.load(worker); + let error: string | null = null; + try { + await viewer.restoreWorkspace(initial, { + wait_for_table: false, + }); + + await viewer.restoreWorkspace(reversed); + } catch (e) { + error = String(e); + } + + await viewer.flush(); + const saved = await viewer.saveWorkspace(); + return { + error, + names: viewer.getPanelNames(), + panels: Object.keys(saved.panels), + layout: saved.layout, + }; + }, + [initial, reversed], + ); + + await cdp.send("Emulation.setCPUThrottlingRate", { rate: 1 }); + expect(result.error).toBeNull(); + expect(result.names.sort()).toEqual(Object.keys(initial.panels).sort()); + + expect(result.panels.sort()).toEqual( + Object.keys(initial.panels).sort(), + ); + + expect(treeSlots(result.layout)).toEqual(treeSlots(reversed.layout)); + expect(errors.filter((e) => e.includes("Panel disposed"))).toEqual([]); + }); +}); From 4be53103e5408e57e67269f8fd550d1297d3aead Mon Sep 17 00:00:00 2001 From: Andrew Stein Date: Wed, 11 Nov 2026 10:36:11 -0500 Subject: [PATCH 2/3] Fix agent corpus missing docs warning Signed-off-by: Andrew Stein --- rust/perspective-viewer/tools.mjs | 46 +++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/rust/perspective-viewer/tools.mjs b/rust/perspective-viewer/tools.mjs index 377b41ca31..cb48268682 100644 --- a/rust/perspective-viewer/tools.mjs +++ b/rust/perspective-viewer/tools.mjs @@ -91,8 +91,12 @@ const DOCS_DTS_FILES = [ "CustomNumberFormatConfig", "NumberFormatStyle", "Notation", - "CustomDatetimeStyleConfig", - "SimpleDatetimeStyleConfig", + "DatetimeFormat", + "SimpleDatetimeFormat", + "DatetimeTextPart", + "DatetimeNumericPart", + "DatetimeMonthPart", + "FractionalSecondDigits", ].map((x) => ({ file: `./dist/ts-rs/${x}.d.ts`, prefix: "Column Format" })), ...[ "WorkspaceConfigUpdate", @@ -114,7 +118,7 @@ function repoPath(file) { // Every page in the `SUMMARY.md` table of contents that is one of `roots` // or nested beneath one. -function summarySubtrees(summary, roots) { +function summarySubtrees(summary, roots, missing) { const files = []; const found = new Set(); let depth = null; @@ -138,7 +142,7 @@ function summarySubtrees(summary, roots) { } for (const root of roots.filter((x) => !found.has(x))) { - console.warn(`docs corpus: \`${root}\` is not in ${summary}`); + missing.push(`\`${root}\` is not in ${summary}`); } return files; @@ -227,7 +231,7 @@ function declarationEnd(text, start, kind) { // The source text of each `scope` declaration (e.g. `"interface Foo"`), // including its leading JSDoc. -function scopeDeclarations(file, text, scope) { +function scopeDeclarations(file, text, scope, missing) { return scope .map((decl) => { const [kind, name] = decl.split(/\s+/); @@ -237,7 +241,7 @@ function scopeDeclarations(file, text, scope) { const match = re.exec(text); if (!match) { - console.warn(`docs corpus: \`${decl}\` not found in ${file}`); + missing.push(`\`${decl}\` not found in ${file}`); return ""; } @@ -391,14 +395,12 @@ function inlineMangledDefinitions(schema) { return out; } -async function buildToolSchemas() { +async function buildToolSchemas(missing) { const { createGenerator } = await import("ts-json-schema-generator"); const schemas = {}; for (const [type, path] of TOOL_SCHEMA_TYPES) { if (!fs.existsSync(path)) { - console.warn( - `docs corpus: ${path} missing, skipping ${type} schema`, - ); + missing.push(`${path} (${type} tool schema)`); continue; } @@ -417,14 +419,19 @@ async function buildToolSchemas() { /// Build the `dist/docs/perspective-docs.json` agent metadata bundle /// (`{schemas, chunks}`). Returns summary stats. +/// +/// Every input is required: a missing file or scoped declaration (e.g. a +/// ts-rs type renamed in Rust but not here) fails the build rather than +/// silently dropping that API from the corpus. export async function buildDocsCorpus() { const chunks = []; + const missing = []; const md_files = [ - ...summarySubtrees(DOCS_SUMMARY, DOCS_SUBTREES), + ...summarySubtrees(DOCS_SUMMARY, DOCS_SUBTREES, missing), ...DOCS_MD_FILES, ].filter((file) => { if (!fs.existsSync(file)) { - console.warn(`docs corpus: ${file} missing, skipping`); + missing.push(file); return false; } @@ -438,15 +445,24 @@ export async function buildDocsCorpus() { for (const { file, prefix, scope } of DOCS_DTS_FILES) { if (fs.existsSync(file)) { const text = fs.readFileSync(file, "utf8"); - const scoped = scope ? scopeDeclarations(file, text, scope) : text; + const scoped = scope + ? scopeDeclarations(file, text, scope, missing) + : text; chunks.push(...chunkDts(file, scoped, prefix)); } else { - console.warn(`docs corpus: ${file} missing, skipping API chunks`); + missing.push(file); } } + const schemas = await buildToolSchemas(missing); + if (missing.length > 0) { + throw new Error( + `docs corpus: ${missing.length} input(s) missing - update DOCS_DTS_FILES / TOOL_SCHEMA_TYPES / DOCS_SUBTREES in tools.mjs:\n${missing.map((x) => ` ${x}`).join("\n")}`, + ); + } + const corpus = normalizeChunks(chunks); - const bundle = { schemas: await buildToolSchemas(), chunks: corpus }; + const bundle = { schemas, chunks: corpus }; const json = JSON.stringify(bundle); if (json.length > DOCS_RAW_BUDGET) { console.warn( From 330ead4cbf7d6cc1c8b9ed397ac3426f12601b72 Mon Sep 17 00:00:00 2001 From: Andrew Stein Date: Tue, 10 Nov 2026 21:42:50 -0500 Subject: [PATCH 3/3] Combine default light/dark themes Signed-off-by: Andrew Stein --- docs/md/how_to/javascript/theming.md | 11 +- examples/esbuild-example/src/index.js | 2 +- package.json | 1 + .../jupyterlab/test/jupyter/widget.spec.mjs | 12 +- .../src/ts/theme/theme-snapshot.ts | 15 +- .../viewer-datagrid/src/ts/model/create.ts | 4 +- .../src/ts/plugin/plugin_config_schema.ts | 19 +- .../viewer-datagrid/test/js/restyle.spec.ts | 5 +- rust/perspective-js/src/rust/utils/futures.rs | 3 +- .../src/rust/utils/ts_promise.rs | 1 - rust/perspective-viewer/Cargo.toml | 2 + rust/perspective-viewer/build.mjs | 13 +- rust/perspective-viewer/docs/viewer.md | 6 +- rust/perspective-viewer/src/css/panel-tab.css | 48 +- rust/perspective-viewer/src/css/viewer.css | 109 +++-- .../src/rust/components/main_panel.rs | 17 - .../rust/components/main_panel/frame_theme.rs | 130 ------ .../rust/components/main_panel/reconcile.rs | 13 +- .../src/rust/custom_elements/viewer.rs | 8 +- rust/perspective-viewer/src/rust/js/plugin.rs | 8 +- .../src/rust/presentation.rs | 30 +- .../src/rust/presentation/sheets.rs | 137 ++++-- .../src/rust/renderer/dispatch.rs | 55 ++- rust/perspective-viewer/src/rust/tasks/mod.rs | 4 +- .../src/rust/tasks/update_theme.rs | 5 +- .../src/rust/utils/css_vars.rs | 33 +- .../src/themes/defaults.css | 420 ++++++++++++++++++ .../src/themes/pro-dark.css | 93 +--- .../src/themes/pro-light.css | 24 + rust/perspective-viewer/src/themes/pro.css | 119 +---- .../src/themes/pro/openlayers.css | 56 +++ .../src/themes/pro/palette.css | 105 +++++ rust/perspective-viewer/src/themes/themes.css | 1 + rust/perspective-viewer/test/html/blank.html | 2 +- .../test/html/column-selector-modes.html | 2 +- .../test/html/column-settings-enabled.html | 2 +- .../perspective-viewer/test/html/filters.html | 2 +- .../test/html/plugin-priority-order.html | 2 +- .../test/html/plugin-resize.html | 2 +- .../test/html/superstore-all.html | 2 +- .../test/html/superstore-debug.html | 2 +- .../test/html/superstore-single-threaded.html | 2 +- .../test/html/superstore.html | 2 +- .../test/html/superstore_lazy_viewer.html | 2 +- .../test/html/superstore_with_input.html | 2 +- .../test/js/viewer_api/theme.spec.ts | 2 +- .../test/js/viewer_api/theme_auto.spec.ts | 322 ++++++++++++++ .../js/viewer_api/theme_discovery.spec.ts | 169 +++++++ rust/perspective-viewer/tools.mjs | 122 ++++- tools/bench/src/html/charts-bench.html | 2 +- .../all-types-small-multi-arrow-test.html | 2 +- tools/test/src/html/basic-test.html | 2 +- tools/test/src/html/superstore-test.html | 2 +- tools/test/src/html/two-chart-test.html | 2 +- 54 files changed, 1658 insertions(+), 500 deletions(-) delete mode 100644 rust/perspective-viewer/src/rust/components/main_panel/frame_theme.rs create mode 100644 rust/perspective-viewer/src/themes/pro-light.css create mode 100644 rust/perspective-viewer/src/themes/pro/openlayers.css create mode 100644 rust/perspective-viewer/src/themes/pro/palette.css create mode 100644 rust/perspective-viewer/test/js/viewer_api/theme_auto.spec.ts create mode 100644 rust/perspective-viewer/test/js/viewer_api/theme_discovery.spec.ts diff --git a/docs/md/how_to/javascript/theming.md b/docs/md/how_to/javascript/theming.md index 6f76d6dd2a..748eb1a911 100644 --- a/docs/md/how_to/javascript/theming.md +++ b/docs/md/how_to/javascript/theming.md @@ -6,8 +6,12 @@ these themes directly into your app, and the `perspective-viewer`s will be themed accordingly: ```javascript -// Themes based on Thought Merchants's Prospective design +// Themes based on Thought Merchants's Prospective design. `pro.css` ("Pro") +// follows the page's `color-scheme`, or `prefers-color-scheme` when the page +// declares none; `pro-light.css` ("Pro Light") and `pro-dark.css` +// ("Pro Dark") pin one scheme. import "@perspective-dev/viewer/dist/css/pro.css"; +import "@perspective-dev/viewer/dist/css/pro-light.css"; import "@perspective-dev/viewer/dist/css/pro-dark.css"; // Other themes @@ -73,7 +77,10 @@ await viewer.restore({ theme: "Pro Dark" }); The best way to write a new theme is to [fork and modify an existing theme](https://github.com/perspective-dev/perspective/tree/master/rust/perspective-viewer/src/themes), which are _just_ collections of regular CSS variables — Perspective's own -themes are plain `.css` files, with no preprocessor involved. +themes are plain `.css` files, with no preprocessor involved (the one +exception is the `Pro` family, whose three themes share one `light-dark()` +palette). + `` is not "themed" by default and will lack icons and label text in addition to colors and fonts, so starting from an empty theme forces you to define _every_ diff --git a/examples/esbuild-example/src/index.js b/examples/esbuild-example/src/index.js index db18aeedb1..db636c8345 100644 --- a/examples/esbuild-example/src/index.js +++ b/examples/esbuild-example/src/index.js @@ -15,7 +15,7 @@ import perspective_viewer from "@perspective-dev/viewer"; import "@perspective-dev/viewer-datagrid"; import "@perspective-dev/viewer-charts"; -import "@perspective-dev/viewer/dist/css/pro-dark.css"; +import "@perspective-dev/viewer/dist/css/themes.css"; import "./index.css"; import arrow from "superstore-arrow/superstore.lz4.arrow"; diff --git a/package.json b/package.json index 9a2845eca8..3f077707e1 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "build": "node tools/scripts/build.mjs", "build_python": "node tools/scripts/build.mjs", "bench": "node tools/scripts/bench.mjs", + "start": "npm run start --workspace", "setup": "node tools/scripts/setup.mjs", "once": "PSP_ONCE=1 node tools/scripts/setup.mjs", "docs": "node tools/scripts/docs.mjs", diff --git a/packages/jupyterlab/test/jupyter/widget.spec.mjs b/packages/jupyterlab/test/jupyter/widget.spec.mjs index 1b0ec0fc25..76cdf5bc16 100644 --- a/packages/jupyterlab/test/jupyter/widget.spec.mjs +++ b/packages/jupyterlab/test/jupyter/widget.spec.mjs @@ -388,7 +388,7 @@ async_table = await async_client.open_table(sync_table.get_name())`, sort: [], split_by: [], table: expect.any(String), - theme: "Pro Light", + theme: "Pro", title: null, }); @@ -483,7 +483,7 @@ w.theme = "Pro Dark"`, sort: [], split_by: [], table: expect.any(String), - theme: "Pro Light", + theme: "Pro", title: null, }); @@ -716,9 +716,7 @@ assert w2.group_by == ["bool"] async ({ page }) => { await default_body(page); await expect - .poll(() => - page.locator("regular-table tbody tr").count(), - ) + .poll(() => page.locator("regular-table tbody tr").count()) .toEqual(10); }, ); @@ -824,9 +822,7 @@ assert w2.group_by == ["bool"] const viewer = await default_body(page); await expect .poll(() => - viewer.evaluate( - async (v) => (await v.save()).group_by, - ), + viewer.evaluate(async (v) => (await v.save()).group_by), ) .toEqual(["date"]); const columns = await viewer.evaluate( diff --git a/packages/viewer-charts/src/ts/theme/theme-snapshot.ts b/packages/viewer-charts/src/ts/theme/theme-snapshot.ts index 53213c9106..7ea605a7aa 100644 --- a/packages/viewer-charts/src/ts/theme/theme-snapshot.ts +++ b/packages/viewer-charts/src/ts/theme/theme-snapshot.ts @@ -37,6 +37,12 @@ const THEME_VARS = [ "--psp-charts--sunburst-gap--px", ]; +/** + * The computed value of an unset `--psp-*` color, which the theme defaults + * register with `@property` so `light-dark()` resolves before it is read. + */ +const UNSET_COLOR = "rgba(0, 0, 0, 0)"; + /** * Capture every CSS variable the renderer cares about into a * structured-cloneable map. Series-palette colours are walked until the @@ -44,9 +50,14 @@ const THEME_VARS = [ */ export function snapshotThemeVars(el: Element): ThemeSnapshot { const style = getComputedStyle(el); + const read = (name: string) => { + const raw = style.getPropertyValue(name).trim(); + return raw === UNSET_COLOR ? "" : raw; + }; + const out: ThemeSnapshot = {}; for (const v of THEME_VARS) { - const raw = style.getPropertyValue(v).trim(); + const raw = read(v); if (raw) { out[v] = raw; } @@ -54,7 +65,7 @@ export function snapshotThemeVars(el: Element): ThemeSnapshot { for (let i = 1; ; i++) { const key = `--psp-charts--series-${i}--color`; - const raw = style.getPropertyValue(key).trim(); + const raw = read(key); if (!raw) { break; } diff --git a/packages/viewer-datagrid/src/ts/model/create.ts b/packages/viewer-datagrid/src/ts/model/create.ts index 85239515c6..8ee2b8dd1b 100644 --- a/packages/viewer-datagrid/src/ts/model/create.ts +++ b/packages/viewer-datagrid/src/ts/model/create.ts @@ -102,9 +102,11 @@ function nestedArraysChanged(a: T[][], b: T[][]): boolean { return false; } +const UNSET_COLOR = "rgba(0, 0, 0, 0)"; + function get_rule(regular: HTMLElement, tag: string, def: string): string { const color = window.getComputedStyle(regular).getPropertyValue(tag).trim(); - if (color.length > 0) { + if (color.length > 0 && color !== UNSET_COLOR) { return color; } else { return def; diff --git a/packages/viewer-datagrid/src/ts/plugin/plugin_config_schema.ts b/packages/viewer-datagrid/src/ts/plugin/plugin_config_schema.ts index f272043b0e..4915c2a054 100644 --- a/packages/viewer-datagrid/src/ts/plugin/plugin_config_schema.ts +++ b/packages/viewer-datagrid/src/ts/plugin/plugin_config_schema.ts @@ -83,25 +83,26 @@ export function measure_px( return Number.isFinite(px) && px > 0 ? Math.round(px) : fallback; } +const UNSET_COLOR = "rgba(0, 0, 0, 0)"; + /** * The theme's zebra stripe color as opaque `#rrggbb`, a 6% blend of text * into background when the theme defines none. */ export function default_zebra_color(elem: Element): string { const style = window.getComputedStyle(elem); - const own = style.getPropertyValue("--psp-datagrid--zebra--color").trim(); + const read = (name: string) => { + const raw = style.getPropertyValue(name).trim(); + return raw === UNSET_COLOR ? "" : raw; + }; + + const own = read("--psp-datagrid--zebra--color"); if (own) { return rgbToHex(parseColor(own)); } - const background = parseColor( - style.getPropertyValue("--psp--background-color").trim() || "#ffffff", - ); - - const color = parseColor( - style.getPropertyValue("--psp--color").trim() || "#000000", - ); - + const background = parseColor(read("--psp--background-color") || "#ffffff"); + const color = parseColor(read("--psp--color") || "#000000"); return rgbToHex(mixRgb(background, color, 0.06)); } diff --git a/packages/viewer-datagrid/test/js/restyle.spec.ts b/packages/viewer-datagrid/test/js/restyle.spec.ts index 82ba8b3015..11d8aaa00c 100644 --- a/packages/viewer-datagrid/test/js/restyle.spec.ts +++ b/packages/viewer-datagrid/test/js/restyle.spec.ts @@ -27,7 +27,7 @@ async function goto_ready(page: any) { await new Promise((x) => setTimeout(x, 10)); } - // `basic-test.html` loads only `pro.css` (= "Pro Light"); "Pro Dark" + // `basic-test.html` loads only `pro-light.css` (= "Pro Light"); "Pro Dark" // is a separate stylesheet, without which a theme switch changes no // CSS at all. await new Promise((resolve, reject) => { @@ -39,6 +39,9 @@ async function goto_ready(page: any) { link.onerror = reject; document.head.appendChild(link); }); + + const viewer = document.querySelector("perspective-viewer") as any; + await viewer.resetThemes(); }); } diff --git a/rust/perspective-js/src/rust/utils/futures.rs b/rust/perspective-js/src/rust/utils/futures.rs index f1b32a0a98..27c538cff8 100644 --- a/rust/perspective-js/src/rust/utils/futures.rs +++ b/rust/perspective-js/src/rust/utils/futures.rs @@ -182,6 +182,7 @@ where impl IntoWasmAbi for ApiFuture where Result: IntoJsResult + 'static, + T: TsPromise, { type Abi = ::Abi; @@ -194,7 +195,7 @@ where impl FromWasmAbi for ApiFuture where Result: IntoJsResult + 'static, - T: From + Into, + T: From + Into + TsPromise, { type Abi = ::Abi; diff --git a/rust/perspective-js/src/rust/utils/ts_promise.rs b/rust/perspective-js/src/rust/utils/ts_promise.rs index 9692695f4d..7c5053d75b 100644 --- a/rust/perspective-js/src/rust/utils/ts_promise.rs +++ b/rust/perspective-js/src/rust/utils/ts_promise.rs @@ -11,7 +11,6 @@ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ use wasm_bindgen::describe::WasmDescribe; -use wasm_bindgen::prelude::*; /// The `Promise` TypeScript type an [`super::ApiFuture`] is declared /// as, carried by a `#[wasm_bindgen]` extern type whose `typescript_type` diff --git a/rust/perspective-viewer/Cargo.toml b/rust/perspective-viewer/Cargo.toml index 3c5eb4de17..0d2ae69a82 100644 --- a/rust/perspective-viewer/Cargo.toml +++ b/rust/perspective-viewer/Cargo.toml @@ -131,6 +131,8 @@ web-sys.features = [ "CssRuleList", "CssRule", "CssStyleRule", + "CssGroupingRule", + "CssImportRule", "CustomEvent", "CustomEventInit", "DataTransfer", diff --git a/rust/perspective-viewer/build.mjs b/rust/perspective-viewer/build.mjs index c7b0e64529..774bb1942f 100644 --- a/rust/perspective-viewer/build.mjs +++ b/rust/perspective-viewer/build.mjs @@ -27,7 +27,9 @@ import { buildDocsCorpus, get_host, inlineUrlVisitor, + mixinVisitor, resolveNPM, + THEME_AT_RULES, } from "./tools.mjs"; const IS_DEBUG = @@ -162,6 +164,7 @@ export async function build_all() { "icons", "intl", "pro", + "pro-light", "pro-dark", "botanical", "monokai", @@ -180,12 +183,20 @@ export async function build_all() { "themes", ]; + const palettes = fs + .readdirSync("./src/themes/pro") + .map((name) => `./src/themes/pro/${name}`); + for (const name of themes) { const filename = `./src/themes/${name}.css`; const { code } = await bundleAsyncCss({ filename, minify: true, - visitor: inlineUrlVisitor(filename), + customAtRules: THEME_AT_RULES, + visitor: composeVisitors([ + inlineUrlVisitor(filename), + mixinVisitor(palettes), + ]), resolver: resolveNPM(import.meta.url), }); diff --git a/rust/perspective-viewer/docs/viewer.md b/rust/perspective-viewer/docs/viewer.md index 786e6399dd..5347c9fdcd 100644 --- a/rust/perspective-viewer/docs/viewer.md +++ b/rust/perspective-viewer/docs/viewer.md @@ -43,8 +43,12 @@ these themes directly into your app, and the `perspective-viewer`s will be themed accordingly: ```javascript -// Themes based on Thought Merchants's Prospective design +// Themes based on Thought Merchants's Prospective design. `pro.css` ("Pro") +// follows the page's `color-scheme`, or `prefers-color-scheme` when the page +// declares none; `pro-light.css` ("Pro Light") and `pro-dark.css` +// ("Pro Dark") pin one scheme. import "@perspective-dev/viewer/dist/css/pro.css"; +import "@perspective-dev/viewer/dist/css/pro-light.css"; import "@perspective-dev/viewer/dist/css/pro-dark.css"; // Other themes diff --git a/rust/perspective-viewer/src/css/panel-tab.css b/rust/perspective-viewer/src/css/panel-tab.css index f31eb24b1b..1afc2e86af 100644 --- a/rust/perspective-viewer/src/css/panel-tab.css +++ b/rust/perspective-viewer/src/css/panel-tab.css @@ -21,12 +21,23 @@ cursor: pointer; font-size: 11px; white-space: nowrap; - overflow: hidden; + + /* Clip overflowing content, but allow the 4px fill shadows and the 1px + separator lines (`::before`, 5px above/left of the tab) to paint + outside the tab. `overflow: hidden` would clip the lines (a child + box); `clip-path` clips at 6px out instead, 1px clear of the lines so + their edges aren't cut by an un-snapped clip. Content can show at most + 6px past the tab, mostly under the fill. */ + clip-path: inset(-6px); + position: relative; color: var(--psp--color, inherit); background: var(--psp-inactive--border-color, #ffffff) !important; + /* Whole-pixel fill reaching 4px up/left/right, so it abuts (never + overlaps) the frame's 1px separator lines and the 9px titlebar gap + (see viewer.css `.rl-panel`). */ box-shadow: - 2.25px -2.25px 0 2.25px var(--psp-inactive--border-color), - -2.25px -2.25px 0 2.25px var(--psp-inactive--border-color); + 2px -2px 0 2px var(--psp-inactive--border-color), + -2px -2px 0 2px var(--psp-inactive--border-color); } :host([part~="tab"].visible) { @@ -39,6 +50,37 @@ 0 12.5px 0 -11.5px var(--psp-inactive--border-color); */ } +/* The panel's separator lines along the titlebar, in the panel's OWN theme + (the tab is in the light DOM, so per-panel theme rules reach it; the frame + can't). Real borders, snapped to device pixels like the plugin's outline. + Covers this tab's slice of the frame's TOP line (5px above the tab, out to + the middle of each 9px tab gap) and a LEFT line 5px left of the tab: + + - FIRST tab in a stack: lands on the frame's left edge, continuing the + plugin outline's left line down the body. + - Other tabs: lands in the middle of the gap (4px fill + 1px line + 4px + neighbour fill), dividing it from the tab to its left. + - Frame's top-left corner: covered by the first tab's top line, which runs + to its left line. + + The LAST tab's top line ends exactly at the frame's right edge (the frame's + 4px right padding); the line beyond it is the right-hand neighbour's own + left line. Ignores the pointer, so the resize grab band still reaches the + layout. Inside the `clip-path: inset(-6px)` above. */ +:host([part~="tab"])::before { + content: ""; + position: absolute; + top: -5px; + bottom: 0; + left: -5px; + right: -4px; + z-index: 1; + box-sizing: border-box; + border: 0 solid var(--psp-inactive--color); + border-width: 1px 0 0 1px; + pointer-events: none; +} + .psp-tab-grip { flex: 0 0 auto; width: 5px; diff --git a/rust/perspective-viewer/src/css/viewer.css b/rust/perspective-viewer/src/css/viewer.css index 5d8451a5a1..82cd27b288 100644 --- a/rust/perspective-viewer/src/css/viewer.css +++ b/rust/perspective-viewer/src/css/viewer.css @@ -96,38 +96,37 @@ position: absolute; inset: 0; background-color: var(--psp--background-color) !important; - box-shadow: - 2.25px 2.25px 0 2.25px var(--psp--background-color), - /* down-right */ -2.25px 2.25px 0 2.25px var(--psp--background-color), - /* down-left */ 2.75px 2.75px 0 2.75px var(--psp-inactive--color), - -1.75px 2.75px 0 2.75px var(--psp-inactive--color); -} -.overlay - ::slotted( - :not([slot^="statusbar-extra"]):not([slot^="tab-"]):not([slot="modal"]) - ) { + /* Background FILL (in the panel's own theme) for the 4px of frame padding + around the plugin: reaches 4px left, right and down, 0px up (the + titlebar is above). Keep these whole pixels and ending exactly at the + frame edge. */ box-shadow: - 2.25px 2.25px 0 2.25px var(--psp--background-color), - -2.25px 2.25px 0 2.25px var(--psp--background-color), - 2.75px 2.75px 0 2.75px var(--psp-inactive--color), - -2.75px 2.75px 0 2.75px var(--psp-inactive--color); + 2px 2px 0 2px var(--psp--background-color), + -2px 2px 0 2px var(--psp--background-color); + + /* The panel's LEFT separator line beside the plugin body, in the panel's + own theme. Drawn here (not on the frame) because per-panel themes reach + only the light-DOM plugin + tab; the frame sits in the viewer's shadow + and would read the host (active panel's) theme. An outline (unlike a + box-shadow) has its width snapped to device pixels, like a border. The + 4px offset puts it exactly on the frame's left edge; its right/bottom + sides fall outside the frame (clipped by `.rl-panel { overflow: clip }`) + and its top side is clipped by `clip-path` (the tab draws the top line). + The clip-path insets reach past the outline (-6px vs -5px) so the + outline's own edges are never cut by an un-snapped clip. */ + outline: 1px solid var(--psp-inactive--color); + outline-offset: 4px; + clip-path: inset(0 -6px -6px -6px); } +/* Tab fill: reaches 4px up, left and right, 0px down. The separator lines + along the titlebar (top, plus left of each tab) are real borders drawn by + the tab itself (`:host::before` in panel-tab.css), in the panel's theme. */ ::slotted([part="tab"].visible) { box-shadow: - 2.25px -2.25px 0 2.25px var(--psp--background-color), - -2.25px -2.25px 0 2.25px var(--psp--background-color), - 3.5px -2.25px 0 2.25px var(--psp-inactive--color), - 0 12.5px 0 -11.5px var(--psp-inactive--border-color); -} - -.overlay ::slotted([part="tab"].visible) { - box-shadow: - 2.25px -2.25px 0 2.25px var(--psp--background-color), - -2.25px -2.25px 0 2.25px var(--psp--background-color), - 2.75px -2.75px 0 2.75px var(--psp-inactive--color), - -2.75px -2.75px 0 2.75px var(--psp-inactive--color), + 2px -2px 0 2px var(--psp--background-color), + -2px -2px 0 2px var(--psp--background-color), 0 12.5px 0 -11.5px var(--psp-inactive--border-color); } @@ -373,20 +372,66 @@ height: 100%; } - /* The layout engine hosting this panel's plugin fills the container. */ + /* The layout engine hosting the panels fills the container. + + Panel separators: frames TILE their grid cells (no margin) and each + panel draws a 1px line on its LEFT and TOP edge only, so every + internal boundary gets exactly one line (owned by the panel to its + right/below, in THAT panel's theme). The lines are drawn by the + light-DOM plugin (`outline`, beside the body) and tab (`::before`, + along the titlebar) - not the frame, which the per-panel theme + rules can't reach. Borders and outlines (unlike box-shadows) are + snapped to whole device pixels at any DPR. The leading inset (-4px) + pushes the outermost left/top lines outside `#main_panel_container`'s + `overflow: hidden`, so the layout itself has no outer border; the + trailing inset (-3px) leaves the plugins the same ~1px outer gutter + on every side. */ & regular-layout { position: absolute; - inset: -4px -4px -4px -4px; + inset: -4px -3px -3px -4px; box-sizing: border-box; padding: 0px; background: var(--psp--background-color, transparent); + /* The padding is the resize grab band: regular-layout only starts + a divider drag when `event.target` is the layout itself, which + works because `regular-layout-frame` is `pointer-events: none` + outside its container/tabs. Do NOT set `pointer-events` here. + left/top = 1px line + 4px fill; right/bottom = 4px fill. */ & > .rl-panel { position: relative; box-sizing: border-box; background: none; - border: 1px solid transparent; - margin: var(--psp-panel--margin, 4px); + border: none; + margin: 0; + padding: 5px 4px 4px 5px; + + /* Own stacking context, so the front tab's z-index (6) can't + escape the frame and paint over a dragged (overlay) panel, + and the overlay outline below can sit above the tab. */ + isolation: isolate; + + /* Clip to the grid cell. Every fill ends exactly at the frame + edge; the only paint outside it is the plugin outline's + right/bottom sides, which would double up on the + neighbours' left/top lines. `clip`, not `hidden`: no scroll + container. */ + overflow: clip; + } + + /* A dragged panel floats over the grid, so it needs all 4 sides, + including the titlebar's right edge, which no tab can draw (a + tab can't tell it's the last in its stack). Transient, so this + one uses the host theme's color. Painted over the outer pixel + of the 4px fill, so the plugin size doesn't change. */ + & > .rl-panel.overlay::after { + content: ""; + position: absolute; + inset: 0; + z-index: 7; + box-sizing: border-box; + border: 1px solid var(--psp-inactive--color); + pointer-events: none; } & > .rl-panel::part(container) { @@ -395,8 +440,10 @@ /* box-shadow: 0 0 0 4.5px var(--psp--background-color); */ } + /* 4px fill + 1px divider + 4px fill (see the tab box-shadows and + `:host::before` in panel-tab.css). */ & > .rl-panel::part(titlebar-track) { - gap: 10px; + gap: 9px; } & > .rl-panel::part(titlebar) { diff --git a/rust/perspective-viewer/src/rust/components/main_panel.rs b/rust/perspective-viewer/src/rust/components/main_panel.rs index f7ccd5c1fc..7fb97b5070 100644 --- a/rust/perspective-viewer/src/rust/components/main_panel.rs +++ b/rust/perspective-viewer/src/rust/components/main_panel.rs @@ -19,11 +19,8 @@ //! tab/active sync, context menu). //! - [`presize`] — the `BeforeResize` pre-size-every-plugin algorithm. //! - [`reconcile`] — the `rendered` layout reconcile + per-panel theme stamp. -//! - [`frame_theme`] — the `rendered` frame-background mirror (each -//! ``'s panel-theme background var). //! - [`render`] — the `view` (status bar, cells, tabs, menu). -mod frame_theme; mod presize; mod reconcile; mod render; @@ -241,17 +238,6 @@ pub struct MainPanel { /// Maximize/Restore menu label. Cleared when the panel leaves the layout. maximized: Option, - /// Theme-name-keyed cache of backgrounds read off stamped plugin - /// elements, the mirror source for frames whose own plugin is unreadable - /// (see [`frame_theme`]). Cleared when the theme registry changes. - theme_backgrounds: HashMap, - - /// The inputs `stamp_frame_themes` last mirrored from; unchanged inputs - /// skip the pass (and its forced style recalcs) on unrelated re-renders. - /// `None` until the first *fully-resolved* pass — an unresolved frame - /// leaves it unlatched so the mirror retries each render. - stamped_frame_themes: Option, - /// `Workspace::staged_changed` → [`MainPanelMsg::StagedChanged`] on THIS /// component's scope (see the message doc for why not the root's). _staged_sub: crate::utils::Subscription, @@ -388,8 +374,6 @@ impl Component for MainPanel { hidden_tabs: HashSet::new(), context_menu: None, maximized: None, - theme_backgrounds: HashMap::new(), - stamped_frame_themes: None, _staged_sub: staged_sub, _layout_staged_sub: layout_staged_sub, } @@ -433,7 +417,6 @@ impl Component for MainPanel { self.size_staging_wrappers(ctx); self.reconcile(ctx); - self.stamp_frame_themes(ctx); } fn view(&self, ctx: &Context) -> Html { diff --git a/rust/perspective-viewer/src/rust/components/main_panel/frame_theme.rs b/rust/perspective-viewer/src/rust/components/main_panel/frame_theme.rs deleted file mode 100644 index 919cdfb877..0000000000 --- a/rust/perspective-viewer/src/rust/components/main_panel/frame_theme.rs +++ /dev/null @@ -1,130 +0,0 @@ -// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ -// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ -// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ -// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ -// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ -// ┃ Copyright (c) 2017, the Perspective Authors. ┃ -// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ -// ┃ This file is part of the Perspective library, distributed under the terms ┃ -// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ -// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ - -//! `MainPanel::stamp_frame_themes`: mirror each panel's effective theme -//! *background* onto its ``. -//! -//! The frames live in the viewer's shadow DOM, so the document theme rules -//! (`perspective-viewer [theme="X"]`) can never match them — the -//! `--psp--background-color` their `::part(container)` background resolves -//! (viewer.css) inherits down from the viewer host, i.e. always the *host* -//! theme. A frame is normally covered by its slotted plugin (which paints its -//! own per-panel themed background), but while the plugin is missing — not yet -//! mounted, first draw pending, or torn down — the host-theme container shows -//! through a panel themed differently, as a visible artifact. - -use wasm_bindgen::prelude::*; -use yew::prelude::*; - -use super::MainPanel; -use crate::utils::PtrEqRc; -use crate::workspace::PanelId; - -/// The custom property mirrored onto each frame: what viewer.css's -/// `.rl-panel::part(container)` background resolves. -const BACKGROUND_VAR: &str = "--psp--background-color"; - -/// The inputs the frame backgrounds were last computed from. -pub(super) type FrameThemeSnapshot = ( - Vec, - Vec<(String, Option)>, - PtrEqRc>, -); - -impl MainPanel { - /// Mirror each panel's effective theme background. - pub(super) fn stamp_frame_themes(&mut self, ctx: &Context) { - let Some(layout) = self.layout_ref.cast::() else { - return; - }; - - let props = ctx.props(); - let snapshot: FrameThemeSnapshot = ( - props.panel_ids.clone(), - props.panel_themes.clone(), - props.presentation_props.available_themes.clone(), - ); - - if self.stamped_frame_themes.as_ref() == Some(&snapshot) { - return; - } - - if let Some((_, _, prev_themes)) = &self.stamped_frame_themes - && prev_themes != &snapshot.2 - { - self.theme_backgrounds.clear(); - } - - let viewer = props.presentation.viewer_elem().clone(); - let mut complete = true; - let children = layout.children(); - for i in 0..children.length() { - let Some(frame) = children.item(i) else { - continue; - }; - - if !frame - .tag_name() - .eq_ignore_ascii_case("regular-layout-frame") - { - continue; - } - - let Some(name) = frame.get_attribute("name") else { - continue; - }; - - let theme = props.effective_panel_theme(&name); - - let background = theme.as_ref().and_then(|theme| { - let fresh = read_plugin_background(&viewer, &name, theme); - if let Some(color) = &fresh { - self.theme_backgrounds.insert(theme.clone(), color.clone()); - } - - let value = fresh.or_else(|| self.theme_backgrounds.get(theme).cloned()); - complete &= value.is_some(); - value - }); - - let style = frame.unchecked_ref::().style(); - match background { - Some(color) => { - let _ = style.set_property(BACKGROUND_VAR, &color); - }, - None => { - let _ = style.remove_property(BACKGROUND_VAR); - }, - } - } - - self.stamped_frame_themes = complete.then_some(snapshot); - } -} - -/// Read the computed [`BACKGROUND_VAR`] off `slot`'s plugin element. -fn read_plugin_background(viewer: &web_sys::Element, slot: &str, theme: &str) -> Option { - let children = viewer.children(); - let plugin = (0..children.length()) - .filter_map(|i| children.item(i)) - .filter(|el| el.get_attribute("slot").as_deref() == Some(slot)) - .find(|el| el.get_attribute("theme").as_deref() == Some(theme))?; - - let value = web_sys::window()? - .get_computed_style(&plugin) - .ok()?? - .get_property_value(BACKGROUND_VAR) - .ok()?; - - let value = value.trim(); - (!value.is_empty()).then(|| value.to_owned()) -} diff --git a/rust/perspective-viewer/src/rust/components/main_panel/reconcile.rs b/rust/perspective-viewer/src/rust/components/main_panel/reconcile.rs index fc75e4aa9a..71f49e12b7 100644 --- a/rust/perspective-viewer/src/rust/components/main_panel/reconcile.rs +++ b/rust/perspective-viewer/src/rust/components/main_panel/reconcile.rs @@ -184,15 +184,16 @@ impl MainPanel { continue; } - if layout.contains_panel(name) { - self.inserted.push(name.to_owned()); - continue; + if !layout.contains_panel(name) { + let index = self.inserted.len(); + let path = JsValue::from(js_sys::Array::of1(&JsValue::from_f64(index as f64))); + let _ = layout.insert_panel(name, path, JsValue::from_bool(true)); } - let index = self.inserted.len(); - let path = JsValue::from(js_sys::Array::of1(&JsValue::from_f64(index as f64))); - let _ = layout.insert_panel(name, path, JsValue::from_bool(true)); self.inserted.push(name.to_owned()); + if let Some(panel) = ctx.props().workspace.panel(id) { + panel.renderer.stamp_frame_background(); + } } let mut pending = std::mem::take(&mut self.pending_removals); diff --git a/rust/perspective-viewer/src/rust/custom_elements/viewer.rs b/rust/perspective-viewer/src/rust/custom_elements/viewer.rs index c6b780d123..718b01cf02 100644 --- a/rust/perspective-viewer/src/rust/custom_elements/viewer.rs +++ b/rust/perspective-viewer/src/rust/custom_elements/viewer.rs @@ -1513,14 +1513,15 @@ impl PerspectiveViewerElement { }; let panel = this.resolve_panel(name)?; - export_method_to_jsvalue( + Ok(export_method_to_jsvalue( &panel.session, &panel.renderer, &this.presentation, &this.workspace, method, ) - .await + .await? + .unchecked_into()) }) } @@ -1816,7 +1817,8 @@ impl PerspectiveViewerElement { /// /// [`Self::restyleElement`] _must_ be called for many runtime changes to /// CSS properties to be reflected in an already-rendered - /// ``. + /// ``, including a `prefers-color-scheme` change + /// under a preference-aware theme such as `"Pro"`. /// /// /// diff --git a/rust/perspective-viewer/src/rust/js/plugin.rs b/rust/perspective-viewer/src/rust/js/plugin.rs index c1bfabe540..aebd7975ed 100644 --- a/rust/perspective-viewer/src/rust/js/plugin.rs +++ b/rust/perspective-viewer/src/rust/js/plugin.rs @@ -255,10 +255,10 @@ impl JsPerspectiveViewerPlugin { /// restyle-then-`update` pair when no other dispatch is coming /// (theme picker, `resetThemes`, default-theme discovery, restore /// tails). Exception: the public `restyleElement()` API restyles - /// unconditionally — it is the "my external CSS changed" affordance, - /// outside what captured-theme state can know. (Plus one - /// `mount_plugin` restyle on first light-DOM mount.) Tier fallback: - /// no-op — the plugin re-reads CSS at its next render. + /// unconditionally — it is the affordance for CSS that changed under an + /// unchanged theme name, such as host `style` edits or a + /// `prefers-color-scheme` flip under a preference-aware theme. Tier + /// fallback: no-op — the plugin re-reads CSS at its next render. pub fn restyle(&self) { if self.capabilities().restyle { self._restyle() diff --git a/rust/perspective-viewer/src/rust/presentation.rs b/rust/perspective-viewer/src/rust/presentation.rs index 575d46bb08..2b1b9c45c4 100644 --- a/rust/perspective-viewer/src/rust/presentation.rs +++ b/rust/perspective-viewer/src/rust/presentation.rs @@ -74,7 +74,7 @@ pub struct PresentationHandle { /// The available themes as detected in the browser environment or set /// explicitly when CORS prevents detection. - themes: RefCell>>, + themes: RefCell>>>, /// Single-flight guard for the stylesheet parse that populates /// [`Self::themes`] (and exclusion against `reset_available_themes`) — @@ -360,34 +360,39 @@ impl Presentation { } /// Get the available theme names from the browser environment by parsing - /// readable stylesheets. This method is memoized - the state can be - /// flushed by calling `reset()`. + /// readable stylesheets once, memoized until the state is flushed by + /// `reset()`. pub async fn get_available_themes(&self) -> ApiResult>> { let _guard = self.0.theme_init.lock().await; if self.0.themes.borrow().is_none() { await_dom_loaded().await?; - let themes = sheets::get_theme_names(&self.0.viewer_elem)?; - *self.0.themes.borrow_mut() = Some(themes); + let names = sheets::get_theme_names(&self.0.viewer_elem); + let mut slot = self.0.themes.borrow_mut(); + if slot.as_ref().is_none_or(|x| **x != names) { + *slot = Some(names.into()); + } } - Ok(self.0.themes.borrow().clone().unwrap().into()) + Ok(self.0.themes.borrow().clone().unwrap()) } /// Reset the state. `styleSheets` will be re-parsed next time - /// `get_themes()` is called if the `themes` argument is `None`. + /// `get_themes()` is called if the `themes` argument is `None`, and an + /// explicit `themes` list is held until the next reset. /// /// # Returns /// A `bool` indicating whether the internal state changed. pub async fn reset_available_themes(&self, themes: Option>) -> bool { - fn as_set(x: &Option>) -> HashSet<&'_ String> { - x.as_ref() - .map(|x| x.iter().collect::>()) + fn as_set(x: Option<&[String]>) -> HashSet<&'_ String> { + x.map(|x| x.iter().collect::>()) .unwrap_or_default() } let _guard = self.0.theme_init.lock().await; - let changed = as_set(&self.0.themes.borrow()) != as_set(&themes); - *self.0.themes.borrow_mut() = themes; + let current = self.0.themes.borrow(); + let changed = as_set(current.as_ref().map(|x| x.as_slice())) != as_set(themes.as_deref()); + drop(current); + *self.0.themes.borrow_mut() = themes.map(PtrEqRc::new); changed } @@ -430,6 +435,7 @@ impl Presentation { } fn set_theme_attribute(&self, theme: Option<&str>) -> ApiResult<()> { + stamp_page_color_scheme(&self.0.viewer_elem); if let Some(theme) = theme { Ok(self.0.viewer_elem.set_attribute("theme", theme)?) } else { diff --git a/rust/perspective-viewer/src/rust/presentation/sheets.rs b/rust/perspective-viewer/src/rust/presentation/sheets.rs index 1f8d910269..0fdf988510 100644 --- a/rust/perspective-viewer/src/rust/presentation/sheets.rs +++ b/rust/perspective-viewer/src/rust/presentation/sheets.rs @@ -10,69 +10,110 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -use perspective_js::utils::{ApiResult, ToApiError, global}; +use js_sys::{Array, Set}; +use perspective_js::utils::global; use wasm_bindgen::prelude::*; use web_sys::*; -macro_rules! iter_index { - ($x:expr) => { - (0..$x.length()).map(|x| $x.item(x)) +const THEME_NAME: &str = "--psp-theme-name"; + +const SELECTOR_TOKEN: &str = "perspective-viewer"; + +/// The theme names registered by every style rule reachable from `elem`'s +/// document and hosting shadow root which declares `--psp-theme-name` and +/// whose selector names `perspective-viewer`, in discovery order. +pub fn get_theme_names(elem: &HtmlElement) -> Vec { + let mut walk = Walk { + seen: Set::new(&JsValue::UNDEFINED), + names: vec![], }; -} -/// Search the document's `styleSheets` for rules which apply to `elem` and -/// provide the `--psp-theme-name` CSS custom property. -pub fn get_theme_names(elem: &HtmlElement) -> Result, JsValue> { - let sheets = global::document().style_sheets(); - let mut themes: Vec = vec![]; - for sheet in iter_index!(sheets) { - fill_sheet_theme_names(&mut themes, &sheet, elem)?; + let document = global::document(); + walk.visit_sheet_list(&document.style_sheets()); + walk.visit_adopted(&document.adopted_style_sheets()); + if let Some(root) = elem.get_root_node().dyn_ref::() { + walk.visit_sheet_list(&root.style_sheets()); + walk.visit_adopted(&root.adopted_style_sheets()); } - Ok(themes) + walk.names } -fn fill_rule_theme_names( - themes: &mut Vec, - rule: &Option, - elem: &HtmlElement, -) -> ApiResult<()> { - if let Some(rule) = rule.as_ref().into_apierror()?.dyn_ref::() { - let txt = rule.selector_text(); - if elem.matches(&txt)? { - let style = rule.style(); - let x = (0..style.length()).map(|x| style.item(x)); - for property in x { - if property == "--psp-theme-name" { - let name = style.get_property_value("--psp-theme-name")?; - let trimmed = name.trim(); - let theme = &trimmed[1..trimmed.len() - 1]; - if themes.iter().find(|x| x == &theme).is_none() { - themes.push(theme.to_owned()); - } - } +struct Walk { + seen: Set, + names: Vec, +} + +impl Walk { + fn visit_sheet_list(&mut self, sheets: &StyleSheetList) { + for i in 0..sheets.length() { + if let Some(sheet) = sheets.item(i) { + self.visit_sheet(sheet.unchecked_ref::()); } } } - Ok(()) -} + fn visit_adopted(&mut self, sheets: &Array) { + for sheet in sheets.iter() { + if let Some(sheet) = sheet.dyn_ref::() { + self.visit_sheet(sheet); + } + } + } + + fn visit_sheet(&mut self, sheet: &CssStyleSheet) { + if self.seen.has(sheet) { + return; + } + + self.seen.add(sheet); + if let Ok(rules) = sheet.css_rules() { + self.visit_rules(&rules); + } + } -fn fill_sheet_theme_names( - themes: &mut Vec, - sheet: &Option, - elem: &HtmlElement, -) -> ApiResult<()> { - let sheet = sheet - .as_ref() - .into_apierror()? - .unchecked_ref::(); - - if let Ok(rules) = sheet.css_rules() { - for rule in iter_index!(&rules) { - fill_rule_theme_names(themes, &rule, elem).unwrap_or_default(); + fn visit_rules(&mut self, rules: &CssRuleList) { + for i in 0..rules.length() { + if let Some(rule) = rules.item(i) { + self.visit_rule(&rule); + } + } + } + + fn visit_rule(&mut self, rule: &CssRule) { + if let Some(style) = rule.dyn_ref::() { + self.register(style); + } + + if let Some(import) = rule.dyn_ref::() + && let Some(sheet) = import.style_sheet() + { + self.visit_sheet(&sheet); + } + + if let Some(group) = rule.dyn_ref::() { + self.visit_rules(&group.css_rules()); } } - Ok(()) + fn register(&mut self, rule: &CssStyleRule) { + let Ok(value) = rule.style().get_property_value(THEME_NAME) else { + return; + }; + + let value = value.trim(); + if value.is_empty() || !rule.selector_text().contains(SELECTOR_TOKEN) { + return; + } + + let name = value + .strip_prefix('"') + .and_then(|x| x.strip_suffix('"')) + .or_else(|| value.strip_prefix('\'').and_then(|x| x.strip_suffix('\''))) + .unwrap_or(value); + + if !self.names.iter().any(|x| x == name) { + self.names.push(name.to_owned()); + } + } } diff --git a/rust/perspective-viewer/src/rust/renderer/dispatch.rs b/rust/perspective-viewer/src/rust/renderer/dispatch.rs index 3d90a68934..d4444fab61 100644 --- a/rust/perspective-viewer/src/rust/renderer/dispatch.rs +++ b/rust/perspective-viewer/src/rust/renderer/dispatch.rs @@ -68,6 +68,54 @@ impl Renderer { .find(|el| el.get_attribute("slot").as_deref() == Some(slot.as_str())) } + /// Record `theme` as the plugin's captured theme and mirror its + /// background onto the panel's layout frame. + fn capture_theme(&self, plugin: &JsPerspectiveViewerPlugin, theme: Option) { + *self.0.captured_theme.borrow_mut() = Some(theme); + self.mirror_frame_background(plugin); + } + + /// Mirror the active plugin's background onto a `` + /// that appeared after the plugin's last theme capture. + pub fn stamp_frame_background(&self) { + if let Some(plugin) = self.active_plugin() { + self.mirror_frame_background(&plugin); + } + } + + /// Mirror the connected plugin's computed `--psp--background-color` onto + /// its ``, which lives in the viewer shadow where + /// document theme rules cannot reach it. + fn mirror_frame_background(&self, plugin: &JsPerspectiveViewerPlugin) { + let el = plugin.unchecked_ref::(); + if !el.is_connected() { + return; + } + + let Some(slot) = self.slot_name() else { + return; + }; + + let viewer = self.plugin_data.borrow().viewer_elem.clone(); + let selector = format!("regular-layout-frame[name=\"{slot}\"]"); + let Some(frame) = viewer + .shadow_root() + .and_then(|root| root.query_selector(&selector).ok().flatten()) + else { + return; + }; + + let style = frame.unchecked_ref::().style(); + match read_custom_property(el, "--psp--background-color") { + Some(color) => { + let _ = style.set_property("--psp--background-color", &color); + }, + None => { + let _ = style.remove_property("--psp--background-color"); + }, + } + } + /// Restyle the active plugin and re-draw. pub async fn restyle_all(&self) -> ApiResult { self.render_task(|guard| async move { @@ -79,10 +127,11 @@ impl Renderer { let plugin = self.ensure_plugin_selected()?; let meta = self.metadata(); let stamped_theme = self.theme(); + stamp_page_color_scheme(&self.plugin_data.borrow().viewer_elem); self.stamp_theme(Some(&plugin)); self.stamp_active(&plugin); plugin.restyle(); - *self.0.captured_theme.borrow_mut() = Some(stamped_theme); + self.capture_theme(&plugin, stamped_theme); let dimensions = ctx.view.dimensions().await?; let mut limits = get_row_and_col_limits( &dimensions, @@ -486,7 +535,7 @@ impl Renderer { self.stamp_theme(Some(&plugin)); if self.needs_restyle() { plugin.restyle(); - *self.0.captured_theme.borrow_mut() = Some(stamped_theme.clone()); + self.capture_theme(&plugin, stamped_theme.clone()); } self.0.data_stale.set(false); @@ -502,7 +551,7 @@ impl Renderer { if result.is_ok() { self.0.has_drawn.set(true); if first_paint { - *self.0.captured_theme.borrow_mut() = Some(stamped_theme); + self.capture_theme(&plugin, stamped_theme); } } diff --git a/rust/perspective-viewer/src/rust/tasks/mod.rs b/rust/perspective-viewer/src/rust/tasks/mod.rs index 760b97e7be..a07e5c3ca8 100644 --- a/rust/perspective-viewer/src/rust/tasks/mod.rs +++ b/rust/perspective-viewer/src/rust/tasks/mod.rs @@ -51,10 +51,10 @@ mod validate_expression; /// fallback for slow (e.g. remote) tables. pub(crate) const STAGING_DEADLINE_MS: i32 = 500; -/// Fallback panel chrome `(width, height)` px (margin + border + titlebar) +/// Fallback panel chrome `(width, height)` px (frame padding + titlebar) /// when no frame is available to measure live (see /// [`presize_panels::plugin_chrome`]). -pub(crate) const CHROME_FALLBACK: (f64, f64) = (8.0, 33.0); +pub(crate) const CHROME_FALLBACK: (f64, f64) = (9.0, 35.0); pub(crate) use self::apply_config_defaults::*; pub use self::apply_global_filters::*; diff --git a/rust/perspective-viewer/src/rust/tasks/update_theme.rs b/rust/perspective-viewer/src/rust/tasks/update_theme.rs index 15f53e9d7f..580f607685 100644 --- a/rust/perspective-viewer/src/rust/tasks/update_theme.rs +++ b/rust/perspective-viewer/src/rust/tasks/update_theme.rs @@ -89,7 +89,10 @@ pub(crate) async fn seed_default_themes(presentation: &Presentation, workspace: /// also redraws) is therefore scoped to panels whose captured CSS is /// actually STALE (`Renderer::needs_restyle` — effective theme vs. the one /// stamped at the plugin's last capture): the picked panel when the value -/// is genuinely new, and never a panel that has yet to first-paint. +/// is genuinely new, and never a panel that has yet to first-paint. CSS +/// that changes under an UNCHANGED name (a `prefers-color-scheme` flip +/// under a preference-aware theme) is outside this gate by design and is +/// the embedder's `restyleElement()` call. pub fn update_theme( session: &Session, renderer: &Renderer, diff --git a/rust/perspective-viewer/src/rust/utils/css_vars.rs b/rust/perspective-viewer/src/rust/utils/css_vars.rs index 97a1b85ac0..dd29962894 100644 --- a/rust/perspective-viewer/src/rust/utils/css_vars.rs +++ b/rust/perspective-viewer/src/rust/utils/css_vars.rs @@ -10,10 +10,13 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -use web_sys::Element; +use web_sys::{Element, HtmlElement}; + +const PAGE_SCHEME_VAR: &str = "--psp-page-color-scheme"; +const UNSET_COLOR: &str = "rgba(0, 0, 0, 0)"; /// The computed value of custom property `name` on `elem`, trimmed, or -/// `None` when undefined/empty. +/// `None` when undefined, empty, or the unset value of a registered color. pub fn read_custom_property(elem: &Element, name: &str) -> Option { let value = web_sys::window()? .get_computed_style(elem) @@ -22,7 +25,7 @@ pub fn read_custom_property(elem: &Element, name: &str) -> Option { .ok()?; let value = value.trim(); - (!value.is_empty()).then(|| value.to_owned()) + (!value.is_empty() && value != UNSET_COLOR).then(|| value.to_owned()) } /// The intl string `--psp-label--{slug}--content` as inherited by `elem`, @@ -37,3 +40,27 @@ pub fn read_intl_string(elem: &Element, slug: &str) -> Option { Some(unquoted.replace("\\\"", "\"")) } + +/// Copy the `color-scheme` computed on `host`'s parent onto `host` as +/// `--psp-page-color-scheme`, reading `normal` as `light dark`, so a theme +/// rule on a descendant can resolve against the page's scheme rather than +/// the one the host's own theme pins. +pub fn stamp_page_color_scheme(host: &HtmlElement) { + let style = host.style(); + let Some(parent) = host.parent_element() else { + let _ = style.remove_property(PAGE_SCHEME_VAR); + return; + }; + + let scheme = web_sys::window() + .and_then(|window| window.get_computed_style(&parent).ok().flatten()) + .and_then(|computed| computed.get_property_value("color-scheme").ok()) + .unwrap_or_default(); + + let scheme = match scheme.trim() { + "" | "normal" => "light dark", + scheme => scheme, + }; + + let _ = style.set_property(PAGE_SCHEME_VAR, scheme); +} diff --git a/rust/perspective-viewer/src/themes/defaults.css b/rust/perspective-viewer/src/themes/defaults.css index 33c8911e97..cd7295f59b 100644 --- a/rust/perspective-viewer/src/themes/defaults.css +++ b/rust/perspective-viewer/src/themes/defaults.css @@ -14,6 +14,426 @@ @import "./icons.css"; @import "./intl.css"; +@property --psp--background-color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-active--background { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-active--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-charts--axis-lines--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-charts--axis-ticks--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-charts--gradient-stop-1--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-charts--gradient-stop-2--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-charts--gradient-stop-3--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-charts--gradient-stop-4--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-charts--gradient-stop-5--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-charts--gradient-stop-6--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-charts--gradient-stop-7--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-charts--gridline--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-charts--legend--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-charts--legend-border--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-charts--series--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-charts--series-1--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-charts--series-10--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-charts--series-11--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-charts--series-12--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-charts--series-2--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-charts--series-3--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-charts--series-4--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-charts--series-5--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-charts--series-6--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-charts--series-7--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-charts--series-8--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-charts--series-9--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-charts--tooltip--background { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-charts--tooltip--border-color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-charts--tooltip--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-charts--treemap--hover-highlight { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-charts--treemap--labels { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-code-editor--column--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-code-editor--comment--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-code-editor--literal--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-code-editor--operator--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-code-editor--symbol--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-column-type--boolean--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-column-type--date--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-column-type--float--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-column-type--string--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-datagrid--border-color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-datagrid--hover--border-color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-datagrid--neg-cell--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-datagrid--pos-cell--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-datagrid--series-1--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-datagrid--series-10--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-datagrid--series-11--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-datagrid--series-12--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-datagrid--series-2--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-datagrid--series-3--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-datagrid--series-4--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-datagrid--series-5--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-datagrid--series-6--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-datagrid--series-7--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-datagrid--series-8--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-datagrid--series-9--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-datagrid--zebra--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-error--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-expression--function--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-expression--operator--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-expression--psp-error--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-icon-overflow-hint--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-inactive--border-color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-inactive--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-placeholder--background { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-warning--background { + syntax: ""; + inherits: true; + initial-value: transparent; +} + +@property --psp-warning--color { + syntax: ""; + inherits: true; + initial-value: transparent; +} + perspective-viewer, :where(perspective-viewer [theme]) { --psp-button--font-size: 16px; diff --git a/rust/perspective-viewer/src/themes/pro-dark.css b/rust/perspective-viewer/src/themes/pro-dark.css index 48857f88d8..d02a58ed89 100644 --- a/rust/perspective-viewer/src/themes/pro-dark.css +++ b/rust/perspective-viewer/src/themes/pro-dark.css @@ -11,6 +11,8 @@ * ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ */ @import "./defaults.css"; +@import "./pro/palette.css"; +@import "./pro/openlayers.css"; /* Register theme for auto-detection */ perspective-viewer, @@ -21,93 +23,8 @@ perspective-viewer [theme="Pro Dark"] { perspective-viewer[theme="Pro Dark"], perspective-viewer [theme="Pro Dark"] { - /* perspective-viewer-pro-dark--colors (overrides) */ - background-color: #242526; - color: white; color-scheme: dark; - --psp--color: white; - --psp-active--color: #2770a9; - --psp-error--color: #ff9485; - --psp-inactive--color: #61656e; - --psp-inactive--border-color: #4c505b; - --psp--background-color: #242526; - --psp-active--background: rgba(39, 113, 170, 0.5); - --psp-expression--operator--color: #c5c9d0; - --psp-expression--function--color: #22a0ce; - --psp-expression--error--color: rgb(255, 136, 136); - --psp-calendar--filter: invert(1); - --psp-input--filter: invert(1); - --psp-warning--color: #242526; - --psp-warning--background: var(--psp--color); - - /* Syntax */ - --psp-code-editor--symbol--color: white; - --psp-code-editor--literal--color: #7dc3f0; - --psp-code-editor--operator--color: rgb(23, 166, 123); - --psp-code-editor--comment--color: rgb(204, 120, 48); - --psp-code-editor--column--color: #e18ee1; - - /* perspective-viewer-pro-dark--datagrid (overrides) */ - --psp-datagrid--pos-cell--color: #7dc3f0; - --psp-datagrid--neg-cell--color: #ff9485; - - /* perspective-viewer-pro-dark--webgl (overrides) */ - --psp-charts--legend--color: #c5c9d0; - --psp-charts--treemap--labels: white; - --psp-charts--treemap--hover-highlight: white; - --psp-charts--tooltip--color: white; - --psp-charts--axis-ticks--color: #c5c9d0; - --psp-charts--axis-lines--color: #61656e; - --psp-charts--gridline--color: #3b3f46; - --psp-charts--tooltip--background: rgba(42, 44, 47, 1); - --psp-charts--tooltip--border-color: #242526; - --psp-charts--legend--background: var(--psp--background-color); - - --psp-charts--series--color: rgb(71, 120, 194); - --psp-charts--series-1--color: rgb(71, 120, 194); - --psp-charts--series-2--color: rgb(204, 120, 48); - --psp-charts--series-3--color: rgb(158, 84, 192); - --psp-charts--series-4--color: rgb(51, 150, 153); - --psp-charts--series-5--color: rgb(102, 114, 143); - --psp-charts--series-6--color: rgb(211, 103, 189); - --psp-charts--series-7--color: rgb(109, 124, 77); - --psp-charts--series-8--color: rgb(221, 99, 103); - --psp-charts--series-9--color: rgb(120, 104, 206); - --psp-charts--series-10--color: rgb(23, 166, 123); - --psp-datagrid--series-1--color: rgb(71, 120, 194); - --psp-datagrid--series-2--color: rgb(204, 120, 48); - --psp-datagrid--series-3--color: rgb(158, 84, 192); - --psp-datagrid--series-4--color: rgb(51, 150, 153); - --psp-datagrid--series-5--color: rgb(102, 114, 143); - --psp-datagrid--series-6--color: rgb(211, 103, 189); - --psp-datagrid--series-7--color: rgb(109, 124, 77); - --psp-datagrid--series-8--color: rgb(221, 99, 103); - --psp-datagrid--series-9--color: rgb(120, 104, 206); - --psp-datagrid--series-10--color: rgb(23, 166, 123); - - --psp-charts--gradient--background: linear-gradient( - #dd6367 0%, - #242526 50%, - #3289c8 100% - ); - - /* perspective-viewer-pro-dark--openlayers (overrides) */ - --psp-openlayers--tile-url: "http://{a-c}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png"; - --psp-openlayers--attribution--filter: invert(1) hue-rotate(180deg); - --psp-openlayers--element--background: #212c2f; - --psp-openlayers--category-1--color: rgb(71, 120, 194); - --psp-openlayers--category-2--color: rgb(204, 120, 48); - --psp-openlayers--category-3--color: rgb(158, 84, 192); - --psp-openlayers--category-4--color: rgb(51, 150, 153); - --psp-openlayers--category-5--color: rgb(102, 114, 143); - --psp-openlayers--category-6--color: rgb(211, 103, 189); - --psp-openlayers--category-7--color: rgb(109, 124, 77); - --psp-openlayers--category-8--color: rgb(221, 99, 103); - --psp-openlayers--category-9--color: rgb(120, 104, 206); - --psp-openlayers--category-10--color: rgb(23, 166, 123); - --psp-openlayers--gradient--background: linear-gradient( - #dd6367 0%, - #242526 50%, - #3289c8 100% - ); + background-color: #242526; + @apply pro; + @apply pro-openlayers-dark; } diff --git a/rust/perspective-viewer/src/themes/pro-light.css b/rust/perspective-viewer/src/themes/pro-light.css new file mode 100644 index 0000000000..c2e831909b --- /dev/null +++ b/rust/perspective-viewer/src/themes/pro-light.css @@ -0,0 +1,24 @@ +/* ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + * ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ + * ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ + * ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ + * ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ + * ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ + * ┃ Copyright (c) 2017, the Perspective Authors. ┃ + * ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ + * ┃ This file is part of the Perspective library, distributed under the terms ┃ + * ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ + * ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ */ + +@import "./defaults.css"; +@import "./pro/palette.css"; +@import "./pro/openlayers.css"; + +perspective-viewer, +perspective-viewer[theme="Pro Light"], +perspective-viewer [theme="Pro Light"] { + --psp-theme-name: "Pro Light"; + color-scheme: light; + @apply pro; + @apply pro-openlayers-light; +} diff --git a/rust/perspective-viewer/src/themes/pro.css b/rust/perspective-viewer/src/themes/pro.css index 04fee77e93..c2b87c77fe 100644 --- a/rust/perspective-viewer/src/themes/pro.css +++ b/rust/perspective-viewer/src/themes/pro.css @@ -11,107 +11,30 @@ * ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ */ @import "./defaults.css"; +@import "./pro/palette.css"; +@import "./pro/openlayers.css"; perspective-viewer, -perspective-viewer[theme="Pro Light"], -perspective-viewer [theme="Pro Light"] { - --psp-theme-name: "Pro Light"; - color-scheme: light; - --psp--color: #161616; - color: var(--psp--color); - --psp-inactive--color: #ababab; - --psp-inactive--border-color: #dadada; - --psp-active--color: #2670a9; - --psp-error--color: #ff471e; - --psp--background-color: #ffffff; - --psp-icon-overflow-hint--color: rgba(0, 0, 0, 0.2); - --psp-select--background-color: none; - --psp-warning--background: #042121; - --psp-warning--color: #fdfffd; - - --psp-icon-overflow-hint--color: #fdfffd; - --psp-placeholder--background: #8b868045; - --psp-dropdown--box-shadow: 5px 5px 0 #00000020; - - --psp-button--font-size: 16px; - --psp-config-button--padding: 15px 8px 6px 8px; - --psp-column-selector--font-size: 16px; - --psp-column-type--width: 25px; - --psp-select--padding: 0px; - --psp-top-panel-row--display: inline-flex; - --psp-button--min-width: 33px; - - font-family: - "ui-monospace", "SFMono-Regular", "SF Mono", "Menlo", "Consolas", - "Liberation Mono", monospace; - --psp-interface-monospace--font-family: - "ui-monospace", "SFMono-Regular", "SF Mono", "Menlo", "Consolas", - "Liberation Mono", monospace; +perspective-viewer[theme="Pro"], +perspective-viewer [theme="Pro"] { + --psp-theme-name: "Pro"; + @apply pro; + @apply pro-openlayers-light; +} - --psp-label--webgl-y1--content: "arrow_upward"; - --psp-label--webgl-y2--content: "arrow_downward"; - --psp-charts--treemap-axis--lines: none; - --psp-charts--tooltip--background: #ffffff; - --psp-charts--tooltip--color: #161616; - --psp-charts--tooltip--border-color: #ababab; - --psp-charts--gridline--color: #eaedef; - --psp-charts--axis-ticks--color: #161616; - --psp-charts--axis-lines--color: #c5c9d0; - --psp-charts--legend--color: #161616; - --psp-charts--legend--background: var(--psp--background-color); - --psp-charts--series--color: rgba(31, 119, 180, 0.8); - --psp-charts--series-1--color: #0366d6; - --psp-charts--series-2--color: #ff7f0e; - --psp-charts--series-3--color: #2ca02c; - --psp-charts--series-4--color: #d62728; - --psp-charts--series-5--color: #9467bd; - --psp-charts--series-6--color: #8c564b; - --psp-charts--series-7--color: #e377c2; - --psp-charts--series-8--color: #7f7f7f; - --psp-charts--series-9--color: #bcbd22; - --psp-charts--series-10--color: #17becf; - --psp-datagrid--series-1--color: #0366d6; - --psp-datagrid--series-2--color: #ff7f0e; - --psp-datagrid--series-3--color: #2ca02c; - --psp-datagrid--series-4--color: #d62728; - --psp-datagrid--series-5--color: #9467bd; - --psp-datagrid--series-6--color: #8c564b; - --psp-datagrid--series-7--color: #e377c2; - --psp-datagrid--series-8--color: #7f7f7f; - --psp-datagrid--series-9--color: #bcbd22; - --psp-datagrid--series-10--color: #17becf; - --psp-charts--gradient--background: linear-gradient( - #4d342f 0%, - #e4521b 22.5%, - #feeb65 42.5%, - #f0f0f0 50%, - #dcedc8 57.5%, - #42b3d5 67.5%, - #1a237e 100% - ); +perspective-viewer[theme="Pro"], +perspective-viewer [theme="Pro"] { + color-scheme: var(--psp-page-color-scheme, light dark); +} - --psp-datagrid--pos-cell--color: #338dcd; - --psp-datagrid--neg-cell--color: #ff471e; +perspective-viewer[theme="Pro"] { + background-color: light-dark(transparent, #242526); +} - --psp-openlayers--tile-url: "http://{a-c}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png"; - --psp-openlayers--element--background: #fff; - --psp-openlayers--category-1--color: #0366d6; - --psp-openlayers--category-2--color: #ff7f0e; - --psp-openlayers--category-3--color: #2ca02c; - --psp-openlayers--category-4--color: #d62728; - --psp-openlayers--category-5--color: #9467bd; - --psp-openlayers--category-6--color: #8c564b; - --psp-openlayers--category-7--color: #e377c2; - --psp-openlayers--category-8--color: #7f7f7f; - --psp-openlayers--category-9--color: #bcbd22; - --psp-openlayers--category-10--color: #17becf; - --psp-openlayers--gradient--background: linear-gradient( - #4d342f 0%, - #e4521b 22.5%, - #feeb65 42.5%, - #f0f0f0 50%, - #dcedc8 57.5%, - #42b3d5 67.5%, - #1a237e 100% - ); +@media (prefers-color-scheme: dark) { + perspective-viewer, + perspective-viewer[theme="Pro"], + perspective-viewer [theme="Pro"] { + @apply pro-openlayers-dark; + } } diff --git a/rust/perspective-viewer/src/themes/pro/openlayers.css b/rust/perspective-viewer/src/themes/pro/openlayers.css new file mode 100644 index 0000000000..eed0fbeb8b --- /dev/null +++ b/rust/perspective-viewer/src/themes/pro/openlayers.css @@ -0,0 +1,56 @@ +/* ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + * ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ + * ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ + * ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ + * ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ + * ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ + * ┃ Copyright (c) 2017, the Perspective Authors. ┃ + * ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ + * ┃ This file is part of the Perspective library, distributed under the terms ┃ + * ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ + * ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ */ + +@mixin pro-openlayers-light { + --psp-openlayers--tile-url: "http://{a-c}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png"; + --psp-openlayers--element--background: #fff; + --psp-openlayers--category-1--color: #0366d6; + --psp-openlayers--category-2--color: #ff7f0e; + --psp-openlayers--category-3--color: #2ca02c; + --psp-openlayers--category-4--color: #d62728; + --psp-openlayers--category-5--color: #9467bd; + --psp-openlayers--category-6--color: #8c564b; + --psp-openlayers--category-7--color: #e377c2; + --psp-openlayers--category-8--color: #7f7f7f; + --psp-openlayers--category-9--color: #bcbd22; + --psp-openlayers--category-10--color: #17becf; + --psp-openlayers--gradient--background: linear-gradient( + #4d342f 0%, + #e4521b 22.5%, + #feeb65 42.5%, + #f0f0f0 50%, + #dcedc8 57.5%, + #42b3d5 67.5%, + #1a237e 100% + ); +} + +@mixin pro-openlayers-dark { + --psp-openlayers--tile-url: "http://{a-c}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png"; + --psp-openlayers--attribution--filter: invert(1) hue-rotate(180deg); + --psp-openlayers--element--background: #212c2f; + --psp-openlayers--category-1--color: rgb(71, 120, 194); + --psp-openlayers--category-2--color: rgb(204, 120, 48); + --psp-openlayers--category-3--color: rgb(158, 84, 192); + --psp-openlayers--category-4--color: rgb(51, 150, 153); + --psp-openlayers--category-5--color: rgb(102, 114, 143); + --psp-openlayers--category-6--color: rgb(211, 103, 189); + --psp-openlayers--category-7--color: rgb(109, 124, 77); + --psp-openlayers--category-8--color: rgb(221, 99, 103); + --psp-openlayers--category-9--color: rgb(120, 104, 206); + --psp-openlayers--category-10--color: rgb(23, 166, 123); + --psp-openlayers--gradient--background: linear-gradient( + #dd6367 0%, + #242526 50%, + #3289c8 100% + ); +} diff --git a/rust/perspective-viewer/src/themes/pro/palette.css b/rust/perspective-viewer/src/themes/pro/palette.css new file mode 100644 index 0000000000..4977f7e737 --- /dev/null +++ b/rust/perspective-viewer/src/themes/pro/palette.css @@ -0,0 +1,105 @@ +/* ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + * ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ + * ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ + * ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ + * ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ + * ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ + * ┃ Copyright (c) 2017, the Perspective Authors. ┃ + * ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ + * ┃ This file is part of the Perspective library, distributed under the terms ┃ + * ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ + * ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ */ + +@mixin pro { + --psp--color: light-dark(#161616, white); + color: var(--psp--color); + --psp-inactive--color: light-dark(#ababab, #61656e); + --psp-inactive--border-color: light-dark(#dadada, #4c505b); + --psp-active--color: light-dark(#2670a9, #2770a9); + --psp-error--color: light-dark(#ff471e, #ff9485); + --psp--background-color: light-dark(#ffffff, #242526); + --psp-icon-overflow-hint--color: rgba(0, 0, 0, 0.2); + --psp-select--background-color: none; + --psp-warning--background: light-dark(#042121, white); + --psp-warning--color: light-dark(#fdfffd, #242526); + + --psp-icon-overflow-hint--color: #fdfffd; + --psp-placeholder--background: #8b868045; + --psp-dropdown--box-shadow: 5px 5px 0 #00000020; + + --psp-button--font-size: 16px; + --psp-config-button--padding: 15px 8px 6px 8px; + --psp-column-selector--font-size: 16px; + --psp-column-type--width: 25px; + --psp-select--padding: 0px; + --psp-top-panel-row--display: inline-flex; + --psp-button--min-width: 33px; + + font-family: + "ui-monospace", "SFMono-Regular", "SF Mono", "Menlo", "Consolas", + "Liberation Mono", monospace; + --psp-interface-monospace--font-family: + "ui-monospace", "SFMono-Regular", "SF Mono", "Menlo", "Consolas", + "Liberation Mono", monospace; + + --psp-code-editor--symbol--color: light-dark(#242526, white); + --psp-code-editor--literal--color: light-dark(blue, #7dc3f0); + --psp-code-editor--operator--color: light-dark(green, rgb(23, 166, 123)); + --psp-code-editor--comment--color: light-dark(orange, rgb(204, 120, 48)); + --psp-code-editor--column--color: light-dark(purple, #e18ee1); + + --psp-label--webgl-y1--content: "arrow_upward"; + --psp-label--webgl-y2--content: "arrow_downward"; + --psp-charts--treemap-axis--lines: none; + --psp-charts--tooltip--background: light-dark(#ffffff, rgb(42, 44, 47)); + --psp-charts--tooltip--color: light-dark(#161616, white); + --psp-charts--tooltip--border-color: light-dark(#ababab, #242526); + --psp-charts--gridline--color: light-dark(#eaedef, #3b3f46); + --psp-charts--axis-ticks--color: light-dark(#161616, #c5c9d0); + --psp-charts--axis-lines--color: light-dark(#c5c9d0, #61656e); + --psp-charts--legend--color: light-dark(#161616, #c5c9d0); + --psp-charts--legend--background: var(--psp--background-color); + --psp-charts--series--color: light-dark( + rgba(31, 119, 180, 0.8), + rgb(71, 120, 194) + ); + --psp-charts--series-1--color: light-dark(#0366d6, rgb(71, 120, 194)); + --psp-charts--series-2--color: light-dark(#ff7f0e, rgb(204, 120, 48)); + --psp-charts--series-3--color: light-dark(#2ca02c, rgb(158, 84, 192)); + --psp-charts--series-4--color: light-dark(#d62728, rgb(51, 150, 153)); + --psp-charts--series-5--color: light-dark(#9467bd, rgb(102, 114, 143)); + --psp-charts--series-6--color: light-dark(#8c564b, rgb(211, 103, 189)); + --psp-charts--series-7--color: light-dark(#e377c2, rgb(109, 124, 77)); + --psp-charts--series-8--color: light-dark(#7f7f7f, rgb(221, 99, 103)); + --psp-charts--series-9--color: light-dark(#bcbd22, rgb(120, 104, 206)); + --psp-charts--series-10--color: light-dark(#17becf, rgb(23, 166, 123)); + --psp-datagrid--series-1--color: light-dark(#0366d6, rgb(71, 120, 194)); + --psp-datagrid--series-2--color: light-dark(#ff7f0e, rgb(204, 120, 48)); + --psp-datagrid--series-3--color: light-dark(#2ca02c, rgb(158, 84, 192)); + --psp-datagrid--series-4--color: light-dark(#d62728, rgb(51, 150, 153)); + --psp-datagrid--series-5--color: light-dark(#9467bd, rgb(102, 114, 143)); + --psp-datagrid--series-6--color: light-dark(#8c564b, rgb(211, 103, 189)); + --psp-datagrid--series-7--color: light-dark(#e377c2, rgb(109, 124, 77)); + --psp-datagrid--series-8--color: light-dark(#7f7f7f, rgb(221, 99, 103)); + --psp-datagrid--series-9--color: light-dark(#bcbd22, rgb(120, 104, 206)); + --psp-datagrid--series-10--color: light-dark(#17becf, rgb(23, 166, 123)); + --psp-charts--gradient-stop-1--color: light-dark(#4d342f, #dd6367); + --psp-charts--gradient-stop-2--color: light-dark(#e4521b, #8a474a); + --psp-charts--gradient-stop-3--color: light-dark(#feeb65, #402e30); + --psp-charts--gradient-stop-4--color: light-dark(#f0f0f0, #242526); + --psp-charts--gradient-stop-5--color: light-dark(#dcedc8, #26343e); + --psp-charts--gradient-stop-6--color: light-dark(#42b3d5, #29485f); + --psp-charts--gradient-stop-7--color: light-dark(#1a237e, #3289c8); + --psp-charts--gradient--background: linear-gradient( + var(--psp-charts--gradient-stop-1--color) 0%, + var(--psp-charts--gradient-stop-2--color) 22.5%, + var(--psp-charts--gradient-stop-3--color) 42.5%, + var(--psp-charts--gradient-stop-4--color) 50%, + var(--psp-charts--gradient-stop-5--color) 57.5%, + var(--psp-charts--gradient-stop-6--color) 67.5%, + var(--psp-charts--gradient-stop-7--color) 100% + ); + + --psp-datagrid--pos-cell--color: light-dark(#338dcd, #7dc3f0); + --psp-datagrid--neg-cell--color: light-dark(#ff471e, #ff9485); +} diff --git a/rust/perspective-viewer/src/themes/themes.css b/rust/perspective-viewer/src/themes/themes.css index 2cc4482a00..31916d7891 100644 --- a/rust/perspective-viewer/src/themes/themes.css +++ b/rust/perspective-viewer/src/themes/themes.css @@ -11,6 +11,7 @@ * ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ */ @import "./pro.css"; +@import "./pro-light.css"; @import "./pro-dark.css"; @import "./blueprint.css"; @import "./botanical.css"; diff --git a/rust/perspective-viewer/test/html/blank.html b/rust/perspective-viewer/test/html/blank.html index 4951d8e788..29654c01ad 100644 --- a/rust/perspective-viewer/test/html/blank.html +++ b/rust/perspective-viewer/test/html/blank.html @@ -7,7 +7,7 @@ window.WORKER = perspective.worker(); - + diff --git a/rust/perspective-viewer/test/html/column-selector-modes.html b/rust/perspective-viewer/test/html/column-selector-modes.html index d88c2a914a..0ff6457bda 100644 --- a/rust/perspective-viewer/test/html/column-selector-modes.html +++ b/rust/perspective-viewer/test/html/column-selector-modes.html @@ -2,7 +2,7 @@ - + diff --git a/rust/perspective-viewer/test/html/column-settings-enabled.html b/rust/perspective-viewer/test/html/column-settings-enabled.html index c6c81ead5a..3545af6b8f 100644 --- a/rust/perspective-viewer/test/html/column-settings-enabled.html +++ b/rust/perspective-viewer/test/html/column-settings-enabled.html @@ -2,7 +2,7 @@ - + diff --git a/rust/perspective-viewer/test/html/filters.html b/rust/perspective-viewer/test/html/filters.html index bda5ac8cf2..5f5eb1e1b9 100644 --- a/rust/perspective-viewer/test/html/filters.html +++ b/rust/perspective-viewer/test/html/filters.html @@ -3,7 +3,7 @@ - + diff --git a/rust/perspective-viewer/test/html/plugin-priority-order.html b/rust/perspective-viewer/test/html/plugin-priority-order.html index 5aa6ef5a7f..9773059180 100644 --- a/rust/perspective-viewer/test/html/plugin-priority-order.html +++ b/rust/perspective-viewer/test/html/plugin-priority-order.html @@ -36,7 +36,7 @@ - + diff --git a/rust/perspective-viewer/test/html/plugin-resize.html b/rust/perspective-viewer/test/html/plugin-resize.html index f48818246c..b455c36b09 100644 --- a/rust/perspective-viewer/test/html/plugin-resize.html +++ b/rust/perspective-viewer/test/html/plugin-resize.html @@ -42,7 +42,7 @@ - + diff --git a/rust/perspective-viewer/test/html/superstore-all.html b/rust/perspective-viewer/test/html/superstore-all.html index a083c31eac..d7e27dfd25 100644 --- a/rust/perspective-viewer/test/html/superstore-all.html +++ b/rust/perspective-viewer/test/html/superstore-all.html @@ -6,7 +6,7 @@ - + diff --git a/rust/perspective-viewer/test/html/superstore-debug.html b/rust/perspective-viewer/test/html/superstore-debug.html index 8c518a7a6f..648de2487a 100644 --- a/rust/perspective-viewer/test/html/superstore-debug.html +++ b/rust/perspective-viewer/test/html/superstore-debug.html @@ -3,7 +3,7 @@ - + diff --git a/rust/perspective-viewer/test/html/superstore-single-threaded.html b/rust/perspective-viewer/test/html/superstore-single-threaded.html index 2f59928ba7..34e7b0cd85 100644 --- a/rust/perspective-viewer/test/html/superstore-single-threaded.html +++ b/rust/perspective-viewer/test/html/superstore-single-threaded.html @@ -3,7 +3,7 @@ - +