@a2f0/skyline 0.2.22 → 0.2.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/package.md CHANGED
@@ -10,6 +10,16 @@ To start an embed in sunny-day colour, pass `colour: true` to `mountSkyline` alo
10
10
  Omitting `colour` keeps the existing greyscale default. The setting belongs to that instance;
11
11
  it does not use storage or affect another embed. The standalone viewer accepts `?colour=1`.
12
12
 
13
+ Pass `theme: "light"` or `theme: "dark"` from the host's active theme to choose the
14
+ loading backdrop, animated outline and error palette. It applies synchronously,
15
+ before the viewer's code or stylesheets load. After the first frame, the scene
16
+ supplies its own sky; returning to greyscale restores the black sky and stars.
17
+ Without `theme`, `colour: true`
18
+ boots light and greyscale boots dark. The theme does not select scene materials or
19
+ time of day: for a light host, pass both `theme: "light"` and `colour: true`.
20
+ Destroy and remount when the host theme changes. The standalone viewer also accepts
21
+ `?theme=light&colour=1` (or `?theme=dark`); each instance keeps its own setting.
22
+
13
23
  ## Install from npm
14
24
 
15
25
  ```sh
@@ -18,6 +18,8 @@ export interface SkylineOptions {
18
18
  readonly controls?: "open" | "closed";
19
19
  /** Start in the opt-in sunny-day colour view. Greyscale by default. */
20
20
  readonly colour?: boolean;
21
+ /** Loading backdrop and trace. Defaults to light with colour: true, dark otherwise. */
22
+ readonly theme?: "light" | "dark";
21
23
  /**
22
24
  * Show the OpenStreetMap credit with the 3D scene's controls. Shown by default. OpenStreetMap's
23
25
  * licence requires that credit wherever its data appears, so hide it only when the host page
@@ -38,4 +40,4 @@ export interface SkylineOptions {
38
40
  * size and placement: give it an explicit height. Call `destroy` when the hosting component
39
41
  * leaves.
40
42
  */
41
- export declare function mountSkyline(container: HTMLElement, { assetsUrl, title, controls, colour, attribution, three }: SkylineOptions): SkylineInstance;
43
+ export declare function mountSkyline(container: HTMLElement, { assetsUrl, title, controls, colour, theme, attribution, three }: SkylineOptions): SkylineInstance;
@@ -8,10 +8,10 @@ import { mountViewer } from "./skyline-shell.js";
8
8
  * size and placement: give it an explicit height. Call `destroy` when the hosting component
9
9
  * leaves.
10
10
  */
11
- export function mountSkyline(container, { assetsUrl, title = "Interactive Chicago skyline", controls = "closed", colour = false, attribution = true, three }) {
11
+ export function mountSkyline(container, { assetsUrl, title = "Interactive Chicago skyline", controls = "closed", colour = false, theme, attribution = true, three }) {
12
12
  if (controls !== "open" && controls !== "closed")
13
13
  throw new TypeError('Skyline controls must be "open" or "closed".');
14
14
  if (typeof colour !== "boolean")
15
15
  throw new TypeError("Skyline colour must be a boolean.");
16
- return mountViewer(container, { assetsUrl, title, controls, colour, attribution, navigation: false, page: false, three });
16
+ return mountViewer(container, { assetsUrl, title, controls, colour, theme, attribution, navigation: false, page: false, three });
17
17
  }
@@ -13,6 +13,8 @@ export interface ViewerContext {
13
13
  readonly controls: "open" | "closed";
14
14
  /** Start the scene and its building details in sunny-day colour. */
15
15
  readonly colour?: boolean;
16
+ /** Loading backdrop and trace; defaults to light for colour, dark otherwise. */
17
+ readonly theme: "light" | "dark";
16
18
  /** Show the OpenStreetMap credit with the scene's controls. */
17
19
  readonly attribution: boolean;
18
20
  /**
@@ -56,7 +58,8 @@ export interface SkylineInstance {
56
58
  */
57
59
  destroy(): void;
58
60
  }
59
- export interface ViewerOptions extends Omit<ViewerContext, "assets" | "signal"> {
61
+ export interface ViewerOptions extends Omit<ViewerContext, "assets" | "signal" | "theme"> {
62
+ readonly theme?: "light" | "dark" | undefined;
60
63
  /** The assets' directory; relative URLs resolve against the container's document. */
61
64
  readonly assetsUrl: string | URL;
62
65
  /** The viewer's accessible name: `element` is a region with this label. */
@@ -33,7 +33,7 @@ function loaded(link) {
33
33
  // mirroring the viewer's layout, and the rest sizes it to fill the container.
34
34
  const HOST_STYLE = "all:initial;direction:ltr;display:block;position:relative;isolation:isolate;width:100%;height:100%;overflow:hidden";
35
35
  // The failure message's own look, since the viewer's stylesheets may be what failed.
36
- const FAILURE_STYLE = "box-sizing:border-box;height:100%;margin:0;padding:24px;background:#000;color:#aaa;font:12px/1.5 ui-monospace,monospace";
36
+ const FAILURE_STYLE = "box-sizing:border-box;height:100%;margin:0;padding:24px;background:inherit;color:inherit;font:12px/1.5 ui-monospace,monospace";
37
37
  /** Shows why the viewer couldn't start, in place of the viewer. */
38
38
  function showFailure(root, error) {
39
39
  const message = root.ownerDocument.createElement("p");
@@ -49,8 +49,16 @@ function showFailure(root, error) {
49
49
  export function mountViewer(container, { assetsUrl, title, ...options }) {
50
50
  const document = container.ownerDocument;
51
51
  const assets = assetsBase(document, assetsUrl);
52
+ const theme = options.theme === undefined ? (options.colour ? "light" : "dark") : options.theme;
53
+ if (theme !== "light" && theme !== "dark")
54
+ throw new TypeError('Skyline theme must be "light" or "dark".');
52
55
  const element = document.createElement("div");
53
56
  element.style.cssText = HOST_STYLE;
57
+ // Paint before any assets arrive, including when a stylesheet fails to load.
58
+ // Keep the boot palette in sync with viewer.css and index.html.
59
+ element.style.colorScheme = theme;
60
+ element.style.backgroundColor = theme === "light" ? "#f5f5f5" : "#000";
61
+ element.style.color = theme === "light" ? "#444" : "#aaa";
54
62
  element.setAttribute("role", "region");
55
63
  element.setAttribute("aria-label", title);
56
64
  element.setAttribute("aria-busy", "true");
@@ -70,7 +78,7 @@ export function mountViewer(container, { assetsUrl, title, ...options }) {
70
78
  Promise.resolve(options.three).catch(() => { });
71
79
  const controller = new AbortController();
72
80
  let destroyed = false, mounted = false;
73
- const context = { ...options, assets, signal: controller.signal };
81
+ const context = { ...options, theme, assets, signal: controller.signal };
74
82
  const ready = new Promise((resolve, reject) => {
75
83
  Promise.all([importViewer(new URL(VIEWER, assets).href), ...stylesheets.map(loaded)])
76
84
  .then(async ([viewer]) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a2f0/skyline",
3
- "version": "0.2.22",
3
+ "version": "0.2.23",
4
4
  "description": "Interactive Chicago skyline viewer and reusable Three.js building models",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
package/site/index.html CHANGED
@@ -25,7 +25,7 @@
25
25
  height: 100%;
26
26
  margin: 0;
27
27
  overflow: hidden;
28
- background: #000;
28
+ background: inherit;
29
29
  }
30
30
 
31
31
  .notice {
@@ -35,7 +35,20 @@
35
35
  color: #aaa;
36
36
  font: 12px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
37
37
  }
38
+ /* Keep the boot palette in sync with skyline-shell.ts and viewer.css. */
39
+ :root[data-theme="light"] { color-scheme: light; background: #f5f5f5; }
40
+ :root[data-theme="light"] .notice { color: #444; }
38
41
  </style>
42
+ <script>
43
+ // Apply the backdrop before first paint, while the viewer modules load.
44
+ {
45
+ const settings = new URLSearchParams(location.search);
46
+ const light = settings.get("theme") === "light" ||
47
+ (settings.get("theme") !== "dark" && settings.get("colour") === "1");
48
+ document.documentElement.dataset.theme = light ? "light" : "dark";
49
+ document.querySelector('meta[name="theme-color"]').content = light ? "#f5f5f5" : "#000000";
50
+ }
51
+ </script>
39
52
  </head>
40
53
  <body>
41
54
  <!-- The viewer renders into a shadow root inside this element, as in a host's page. -->
@@ -33,7 +33,7 @@ function loaded(link) {
33
33
  // mirroring the viewer's layout, and the rest sizes it to fill the container.
34
34
  const HOST_STYLE = "all:initial;direction:ltr;display:block;position:relative;isolation:isolate;width:100%;height:100%;overflow:hidden";
35
35
  // The failure message's own look, since the viewer's stylesheets may be what failed.
36
- const FAILURE_STYLE = "box-sizing:border-box;height:100%;margin:0;padding:24px;background:#000;color:#aaa;font:12px/1.5 ui-monospace,monospace";
36
+ const FAILURE_STYLE = "box-sizing:border-box;height:100%;margin:0;padding:24px;background:inherit;color:inherit;font:12px/1.5 ui-monospace,monospace";
37
37
  /** Shows why the viewer couldn't start, in place of the viewer. */
38
38
  function showFailure(root, error) {
39
39
  const message = root.ownerDocument.createElement("p");
@@ -49,8 +49,16 @@ function showFailure(root, error) {
49
49
  export function mountViewer(container, { assetsUrl, title, ...options }) {
50
50
  const document = container.ownerDocument;
51
51
  const assets = assetsBase(document, assetsUrl);
52
+ const theme = options.theme === undefined ? (options.colour ? "light" : "dark") : options.theme;
53
+ if (theme !== "light" && theme !== "dark")
54
+ throw new TypeError('Skyline theme must be "light" or "dark".');
52
55
  const element = document.createElement("div");
53
56
  element.style.cssText = HOST_STYLE;
57
+ // Paint before any assets arrive, including when a stylesheet fails to load.
58
+ // Keep the boot palette in sync with viewer.css and index.html.
59
+ element.style.colorScheme = theme;
60
+ element.style.backgroundColor = theme === "light" ? "#f5f5f5" : "#000";
61
+ element.style.color = theme === "light" ? "#444" : "#aaa";
54
62
  element.setAttribute("role", "region");
55
63
  element.setAttribute("aria-label", title);
56
64
  element.setAttribute("aria-busy", "true");
@@ -70,7 +78,7 @@ export function mountViewer(container, { assetsUrl, title, ...options }) {
70
78
  Promise.resolve(options.three).catch(() => { });
71
79
  const controller = new AbortController();
72
80
  let destroyed = false, mounted = false;
73
- const context = { ...options, assets, signal: controller.signal };
81
+ const context = { ...options, theme, assets, signal: controller.signal };
74
82
  const ready = new Promise((resolve, reject) => {
75
83
  Promise.all([importViewer(new URL(VIEWER, assets).href), ...stylesheets.map(loaded)])
76
84
  .then(async ([viewer]) => {
@@ -22,7 +22,7 @@ function element(document, tag, properties = {}, attributes = {}) {
22
22
  created.setAttribute(name, value);
23
23
  return created;
24
24
  }
25
- export function mount(root, { assets, signal, controls, colour, attribution, navigation, page, three }) {
25
+ export function mount(root, { assets, signal, controls, colour, theme, attribution, navigation, page, three }) {
26
26
  const document = root.ownerDocument;
27
27
  const host = root.host;
28
28
  const listening = { signal };
@@ -35,6 +35,7 @@ export function mount(root, { assets, signal, controls, colour, attribution, nav
35
35
  const sceneMarkup = loadMarkup(new URL("skyline-3d.html", assets), signal);
36
36
  sceneMarkup.catch(() => { });
37
37
  const viewer = element(document, "div", { className: "skyline-viewer" });
38
+ viewer.dataset["theme"] = theme;
38
39
  if (!navigation)
39
40
  viewer.dataset["embedded"] = "";
40
41
  const stars = element(document, "div", { className: "stars", id: "stars" }, { "aria-hidden": "true" });
@@ -52,6 +53,7 @@ export function mount(root, { assets, signal, controls, colour, attribution, nav
52
53
  const frames = {};
53
54
  const toolbar = navigation ? createToolbar() : null;
54
55
  function updateView() {
56
+ viewer.toggleAttribute("data-booting", sceneUnavailable && mode === "3d");
55
57
  scene.hidden = mode !== "3d";
56
58
  if (mode === "original" && !drawing.getAttribute("src"))
57
59
  drawing.src = new URL("skyline-original-fit.svg", assets).href;
@@ -6,6 +6,7 @@ import { mountViewer } from "./skyline-shell.js";
6
6
  // bar open or folded; and `?attribution=hidden` leaves out the OpenStreetMap credit. The site
7
7
  // starts the bar open, to show what the scene can do; embedded, it starts folded behind its
8
8
  // star, as `mountSkyline` does.
9
+ // `?colour=1` starts in daylight; `?theme=light` or `?theme=dark` overrides the boot palette.
9
10
  const settings = new URLSearchParams(location.search);
10
11
  const embedded = settings.get("embed") === "1";
11
12
  const controls = settings.get("controls");
@@ -15,6 +16,7 @@ mountViewer(document.querySelector("#viewer"), {
15
16
  controls: controls === "open" || controls === "closed" ? controls : embedded ? "closed" : "open",
16
17
  attribution: settings.get("attribution") !== "hidden",
17
18
  colour: settings.get("colour") === "1",
19
+ theme: document.documentElement.dataset["theme"] === "light" ? "light" : "dark",
18
20
  navigation: !embedded,
19
21
  page: true,
20
22
  });
package/site/viewer.css CHANGED
@@ -35,6 +35,19 @@
35
35
  pointer-events: none;
36
36
  }
37
37
 
38
+ /* Keep this boot palette in sync with skyline-shell.ts and index.html. It gives
39
+ way to the scene's own sky at the first frame. The original drawing always
40
+ keeps its dark sky, including when opened during loading or after a failure. */
41
+ .skyline-viewer[data-theme="light"][data-booting] { color-scheme: light; background: #f5f5f5; color: #444; }
42
+ .skyline-viewer[data-theme="light"][data-booting] > .stars { visibility: hidden; }
43
+ .skyline-viewer[data-theme="light"][data-booting] .loading { color: #444; }
44
+ .skyline-viewer[data-theme="light"][data-booting] .skyline-trace { stroke: #555; }
45
+ .skyline-viewer[data-theme="light"][data-booting] .skyline-trace-tip { stroke: #222; filter: drop-shadow(0 0 9px #22222266); }
46
+ .skyline-viewer[data-theme="light"][data-booting] .controls { color: #444; }
47
+ .skyline-viewer[data-theme="light"][data-booting] .motion-status { color: #555; }
48
+ .skyline-viewer[data-theme="light"][data-booting] .controls :is(button, a):is(:hover, :focus-visible, [aria-pressed="true"]) { color: #111; }
49
+ .skyline-viewer[data-theme="light"][data-booting] .controls :focus-visible { outline-color: #111; }
50
+
38
51
  .stars svg {
39
52
  display: block;
40
53
  width: 100%;