@docentjs/dom 0.5.2 → 0.6.0

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/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { Alignment, ArrowStyle, ControllerOptions, Docent, DocentEnvironment, DocentOptions, Labels, Media, OverlayOptions, Placement, RenderContext, Renderer, Side, SpotlightOptions, SpotlightShape, StorageAdapter, Target, TargetSpec, Theme, Tour, TourController, defineTour } from "@docentjs/core";
1
+ import { Alignment, Appearance, ArrowStyle, ControllerOptions, Docent, DocentEnvironment, DocentOptions, Labels, Media, OverlayOptions, Placement, RenderContext, Renderer, Side, SpotlightOptions, SpotlightShape, StorageAdapter, Target, TargetSpec, Theme, ThemeSpec, Tour, TourController, defineTour } from "@docentjs/core";
2
2
  //#region src/arrows.d.ts
3
3
  export declare const CONNECTOR_STYLES: readonly ["line", "dashed", "dotted", "curve", "curve-dashed", "squiggle", "loop", "elbow", "sketch", "pin"];
4
4
  type ConnectorStyle = (typeof CONNECTOR_STYLES)[number];
@@ -17,6 +17,15 @@ export declare const THEME_VARS: Record<keyof Theme, string>;
17
17
  /** Write theme tokens as inline custom properties on an element. Clears unset ones. */
18
18
  export declare function applyTheme(el: HTMLElement, theme: Theme | undefined): void;
19
19
  export declare function mergeThemes(...themes: Array<Theme | undefined>): Theme;
20
+ /**
21
+ * Layer one theme over another when either may be a preset name. Used by the
22
+ * framework adapters to merge a provider's defaults with a local theme.
23
+ */
24
+ export declare function mergeThemeSpecs(base: ThemeSpec | undefined, override: ThemeSpec | undefined): ThemeSpec | undefined;
25
+ /** `{ theme }` when there is one, or nothing, for spreading into options. */
26
+ export declare function themeOption(theme: ThemeSpec | undefined): {
27
+ theme?: ThemeSpec;
28
+ };
20
29
  /**
21
30
  * Regions of the built-in popover that can be replaced. Custom content is
22
31
  * projected through native Shadow DOM slots, so it lives in the page's DOM
@@ -39,7 +48,7 @@ type PopoverSlots = Partial<Record<SlotName, SlotRenderer>>;
39
48
  * that defines the template stays in the app.
40
49
  */
41
50
  interface PopoverTemplate {
42
- theme?: Theme;
51
+ theme?: ThemeSpec;
43
52
  slots?: PopoverSlots;
44
53
  /** Arrow style for tours using this template. */
45
54
  arrow?: ArrowStyle;
@@ -72,8 +81,10 @@ interface DomRendererOptions {
72
81
  arrow?: ArrowStyle;
73
82
  /** Overlay defaults when a tour sets none: style, color, opacity, blur. */
74
83
  overlay?: OverlayOptions;
75
- /** Base theme tokens. Tours and templates layer on top. */
76
- theme?: Theme;
84
+ /** Base theme: a preset name, tokens, or both. Tours and templates layer on top. */
85
+ theme?: ThemeSpec;
86
+ /** Light (default), dark, or follow the reader's system setting. */
87
+ appearance?: Appearance;
77
88
  /** Replace regions of the built-in popover. */
78
89
  slots?: PopoverSlots;
79
90
  /** Named templates that tours select with `options.template`. */
@@ -115,6 +126,11 @@ export declare class DomRenderer implements Renderer {
115
126
  private connectorLoading;
116
127
  /** Arrow, spotlight and overlay settings for the current step. */
117
128
  private look;
129
+ /** Preset tokens, once loaded. */
130
+ private presets;
131
+ private presetLoad;
132
+ /** Set while `appearance: 'auto'` is following the system setting. */
133
+ private schemeQuery;
118
134
  /** Until then the step's own transition runs; scroll updates may animate. */
119
135
  private settleUntil;
120
136
  /** Play the connector draw-in on its next render. */
@@ -124,7 +140,8 @@ export declare class DomRenderer implements Renderer {
124
140
  hasTarget(target: Target): boolean;
125
141
  waitForTarget(target: Target, timeoutMs: number, signal: AbortSignal): Promise<boolean>;
126
142
  currentRoute(): string;
127
- show(ctx: RenderContext): void;
143
+ show(ctx: RenderContext): void | Promise<void>;
144
+ private showNow;
128
145
  hide(): void;
129
146
  /**
130
147
  * Re-measure and re-position everything. Safe to call often. `tracking`
@@ -138,6 +155,18 @@ export declare class DomRenderer implements Renderer {
138
155
  private loadConnector;
139
156
  /** Add the connector layer and its styles, beneath any popover. */
140
157
  private attachConnector;
158
+ /** Does anything here need the built-in presets? */
159
+ private usesPresets;
160
+ private loadPresets;
161
+ private appearance;
162
+ /** The surface tokens for the current appearance: dark, or nothing for light. */
163
+ private appearanceTheme;
164
+ private prefersDark;
165
+ /** Renderer, then the appearance surface, then template, then tour. */
166
+ private themeFor;
167
+ /** With `appearance: 'auto'`, follow the system setting while the tour runs. */
168
+ private watchAppearance;
169
+ private onSchemeChange;
141
170
  private resolveLook;
142
171
  /** Expose the look to the stylesheet as host attributes and variables. */
143
172
  private applyLook;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Alignment, ArrowStyle, ControllerOptions, Docent, DocentEnvironment, DocentOptions, Labels, Media, OverlayOptions, Placement, RenderContext, Renderer, Side, SpotlightOptions, SpotlightShape, StorageAdapter, Target, TargetSpec, Theme, Tour, TourController, defineTour } from "@docentjs/core";
1
+ import { Alignment, Appearance, ArrowStyle, ControllerOptions, Docent, DocentEnvironment, DocentOptions, Labels, Media, OverlayOptions, Placement, RenderContext, Renderer, Side, SpotlightOptions, SpotlightShape, StorageAdapter, Target, TargetSpec, Theme, ThemeSpec, Tour, TourController, defineTour } from "@docentjs/core";
2
2
  //#region src/arrows.d.ts
3
3
  export declare const CONNECTOR_STYLES: readonly ["line", "dashed", "dotted", "curve", "curve-dashed", "squiggle", "loop", "elbow", "sketch", "pin"];
4
4
  type ConnectorStyle = (typeof CONNECTOR_STYLES)[number];
@@ -17,6 +17,15 @@ export declare const THEME_VARS: Record<keyof Theme, string>;
17
17
  /** Write theme tokens as inline custom properties on an element. Clears unset ones. */
18
18
  export declare function applyTheme(el: HTMLElement, theme: Theme | undefined): void;
19
19
  export declare function mergeThemes(...themes: Array<Theme | undefined>): Theme;
20
+ /**
21
+ * Layer one theme over another when either may be a preset name. Used by the
22
+ * framework adapters to merge a provider's defaults with a local theme.
23
+ */
24
+ export declare function mergeThemeSpecs(base: ThemeSpec | undefined, override: ThemeSpec | undefined): ThemeSpec | undefined;
25
+ /** `{ theme }` when there is one, or nothing, for spreading into options. */
26
+ export declare function themeOption(theme: ThemeSpec | undefined): {
27
+ theme?: ThemeSpec;
28
+ };
20
29
  /**
21
30
  * Regions of the built-in popover that can be replaced. Custom content is
22
31
  * projected through native Shadow DOM slots, so it lives in the page's DOM
@@ -39,7 +48,7 @@ type PopoverSlots = Partial<Record<SlotName, SlotRenderer>>;
39
48
  * that defines the template stays in the app.
40
49
  */
41
50
  interface PopoverTemplate {
42
- theme?: Theme;
51
+ theme?: ThemeSpec;
43
52
  slots?: PopoverSlots;
44
53
  /** Arrow style for tours using this template. */
45
54
  arrow?: ArrowStyle;
@@ -72,8 +81,10 @@ interface DomRendererOptions {
72
81
  arrow?: ArrowStyle;
73
82
  /** Overlay defaults when a tour sets none: style, color, opacity, blur. */
74
83
  overlay?: OverlayOptions;
75
- /** Base theme tokens. Tours and templates layer on top. */
76
- theme?: Theme;
84
+ /** Base theme: a preset name, tokens, or both. Tours and templates layer on top. */
85
+ theme?: ThemeSpec;
86
+ /** Light (default), dark, or follow the reader's system setting. */
87
+ appearance?: Appearance;
77
88
  /** Replace regions of the built-in popover. */
78
89
  slots?: PopoverSlots;
79
90
  /** Named templates that tours select with `options.template`. */
@@ -115,6 +126,11 @@ export declare class DomRenderer implements Renderer {
115
126
  private connectorLoading;
116
127
  /** Arrow, spotlight and overlay settings for the current step. */
117
128
  private look;
129
+ /** Preset tokens, once loaded. */
130
+ private presets;
131
+ private presetLoad;
132
+ /** Set while `appearance: 'auto'` is following the system setting. */
133
+ private schemeQuery;
118
134
  /** Until then the step's own transition runs; scroll updates may animate. */
119
135
  private settleUntil;
120
136
  /** Play the connector draw-in on its next render. */
@@ -124,7 +140,8 @@ export declare class DomRenderer implements Renderer {
124
140
  hasTarget(target: Target): boolean;
125
141
  waitForTarget(target: Target, timeoutMs: number, signal: AbortSignal): Promise<boolean>;
126
142
  currentRoute(): string;
127
- show(ctx: RenderContext): void;
143
+ show(ctx: RenderContext): void | Promise<void>;
144
+ private showNow;
128
145
  hide(): void;
129
146
  /**
130
147
  * Re-measure and re-position everything. Safe to call often. `tracking`
@@ -138,6 +155,18 @@ export declare class DomRenderer implements Renderer {
138
155
  private loadConnector;
139
156
  /** Add the connector layer and its styles, beneath any popover. */
140
157
  private attachConnector;
158
+ /** Does anything here need the built-in presets? */
159
+ private usesPresets;
160
+ private loadPresets;
161
+ private appearance;
162
+ /** The surface tokens for the current appearance: dark, or nothing for light. */
163
+ private appearanceTheme;
164
+ private prefersDark;
165
+ /** Renderer, then the appearance surface, then template, then tour. */
166
+ private themeFor;
167
+ /** With `appearance: 'auto'`, follow the system setting while the tour runs. */
168
+ private watchAppearance;
169
+ private onSchemeChange;
141
170
  private resolveLook;
142
171
  /** Expose the look to the stylesheet as host attributes and variables. */
143
172
  private applyLook;
package/dist/index.js CHANGED
@@ -78,6 +78,45 @@ function renderMedia(doc, media) {
78
78
  return video;
79
79
  }
80
80
  //#endregion
81
+ //#region src/dev.ts
82
+ const checked = /* @__PURE__ */ new WeakSet();
83
+ /**
84
+ * Written exactly like this on purpose: bundlers replace
85
+ * `process.env.NODE_ENV` literally, so a production build turns this into
86
+ * `false`, and the check plus its import are dropped. Without a bundler
87
+ * `process` is simply not defined, and the catch treats that as development.
88
+ */
89
+ function isProduction() {
90
+ try {
91
+ return process.env.NODE_ENV === "production";
92
+ } catch {
93
+ return false;
94
+ }
95
+ }
96
+ /**
97
+ * Check every tour the manager knows, as they load. Tours that never start
98
+ * still get checked, which is where a broken condition usually hides.
99
+ */
100
+ function warnAboutTours(docent) {
101
+ if (isProduction()) return;
102
+ const check = () => {
103
+ for (const tour of docent.getTours()) warnIfInvalid(tour);
104
+ };
105
+ docent.subscribe(check);
106
+ check();
107
+ }
108
+ /** Warn about anything wrong with this tour, once per tour, in development. */
109
+ function warnIfInvalid(tour) {
110
+ if (!tour || isProduction() || checked.has(tour)) return;
111
+ checked.add(tour);
112
+ import("@docentjs/core/validate").then(({ formatIssues, validateTour }) => {
113
+ const issues = validateTour(tour);
114
+ if (issues.length === 0) return;
115
+ const label = issues.filter((issue) => issue.level === "error").length > 0 ? "error" : "warning";
116
+ console.warn(`[docent] Tour "${tour.id}" has ${issues.length} ${label}${issues.length === 1 ? "" : "s"}:\n${formatIssues(issues)}\nThis check runs in development only. See https://docentjs.dev/reference/schema/`);
117
+ }).catch(() => {});
118
+ }
119
+ //#endregion
81
120
  //#region src/occlusion.ts
82
121
  function isPinned(el) {
83
122
  const view = el.ownerDocument.defaultView;
@@ -677,18 +716,82 @@ const THEME_VARS = {
677
716
  connector: "connector",
678
717
  ring: "ring"
679
718
  };
719
+ /** Tokens that take a unit when given as a number. */
720
+ const UNITS = {
721
+ radius: "px",
722
+ width: "px",
723
+ duration: "ms"
724
+ };
725
+ /** `12` becomes `12px`, `220` becomes `220ms`, strings are passed through. */
726
+ function cssValue(key, value) {
727
+ return typeof value === "number" ? `${value}${UNITS[key] ?? ""}` : value;
728
+ }
680
729
  /** Write theme tokens as inline custom properties on an element. Clears unset ones. */
681
730
  function applyTheme(el, theme) {
682
731
  for (const key of Object.keys(THEME_VARS)) {
683
732
  const value = theme?.[key];
684
733
  const prop = `--docent-${THEME_VARS[key]}`;
685
734
  if (value === void 0) el.style.removeProperty(prop);
686
- else el.style.setProperty(prop, value);
735
+ else el.style.setProperty(prop, cssValue(key, value));
736
+ }
737
+ if (theme?.accent !== void 0 && theme.accentForeground === void 0) {
738
+ const light = isLightColor(el, cssValue("accent", theme.accent));
739
+ if (light !== void 0) el.style.setProperty("--docent-accent-fg", light ? "var(--docent-fg)" : "var(--docent-bg)");
687
740
  }
688
741
  }
742
+ /**
743
+ * Is this colour light enough to need dark text on it? Resolves the colour
744
+ * through the browser, so any CSS colour works. Undefined when it cannot tell.
745
+ */
746
+ function isLightColor(el, color) {
747
+ const view = el.ownerDocument.defaultView;
748
+ if (!view) return void 0;
749
+ const previous = el.style.color;
750
+ el.style.color = color;
751
+ const computed = view.getComputedStyle(el).color;
752
+ el.style.color = previous;
753
+ const rgb = computed.match(/^(?:rgba?|color\(srgb)[( ]([^)]+)\)?/);
754
+ if (rgb?.[1]) {
755
+ const [r = 0, g = 0, b = 0] = rgb[1].split(/[\s,/]+/).slice(0, 3).map((n) => n.endsWith("%") ? Number.parseFloat(n) / 100 * 255 : Number.parseFloat(n));
756
+ const scale = computed.startsWith("color(") ? 255 : 1;
757
+ return (.2126 * r * scale + .7152 * g * scale + .0722 * b * scale) / 255 > .55;
758
+ }
759
+ const ok = computed.match(/^oklch\(\s*([\d.]+)(%?)/);
760
+ if (ok?.[1]) return Number.parseFloat(ok[1]) / (ok[2] === "%" ? 100 : 1) > .62;
761
+ }
689
762
  function mergeThemes(...themes) {
690
763
  return Object.assign({}, ...themes.filter(Boolean));
691
764
  }
765
+ /**
766
+ * Layer one theme over another when either may be a preset name. Used by the
767
+ * framework adapters to merge a provider's defaults with a local theme.
768
+ */
769
+ function mergeThemeSpecs(base, override) {
770
+ if (base === void 0) return override;
771
+ if (override === void 0) return base;
772
+ const first = typeof base === "string" ? { preset: base } : base;
773
+ const second = typeof override === "string" ? { preset: override } : override;
774
+ return {
775
+ ...first,
776
+ ...second
777
+ };
778
+ }
779
+ /** `{ theme }` when there is one, or nothing, for spreading into options. */
780
+ function themeOption(theme) {
781
+ return theme === void 0 ? {} : { theme };
782
+ }
783
+ /** True when this theme cannot be resolved without the built-in presets. */
784
+ function needsPresets(spec) {
785
+ return typeof spec === "string" || !!spec && typeof spec === "object" && "preset" in spec;
786
+ }
787
+ /** A preset name, tokens, or a preset with tokens on top, flattened to tokens. */
788
+ function resolveTheme(spec, presets) {
789
+ if (spec === void 0) return void 0;
790
+ if (typeof spec === "string") return presets?.[spec];
791
+ const { preset, ...tokens } = spec;
792
+ if (preset === void 0) return tokens;
793
+ return mergeThemes(presets?.[preset], tokens);
794
+ }
692
795
  //#endregion
693
796
  //#region src/renderer.ts
694
797
  const DEFAULT_LOOK = {
@@ -720,6 +823,11 @@ var DomRenderer = class {
720
823
  connectorLoading;
721
824
  /** Arrow, spotlight and overlay settings for the current step. */
722
825
  look = DEFAULT_LOOK;
826
+ /** Preset tokens, once loaded. */
827
+ presets;
828
+ presetLoad;
829
+ /** Set while `appearance: 'auto'` is following the system setting. */
830
+ schemeQuery;
723
831
  /** Until then the step's own transition runs; scroll updates may animate. */
724
832
  settleUntil = 0;
725
833
  /** Play the connector draw-in on its next render. */
@@ -743,6 +851,12 @@ var DomRenderer = class {
743
851
  return `${pathname}${search}`;
744
852
  }
745
853
  show(ctx) {
854
+ if (this.presets === void 0 && this.usesPresets(ctx)) return this.loadPresets().then(() => {
855
+ this.showNow(ctx);
856
+ });
857
+ this.showNow(ctx);
858
+ }
859
+ showNow(ctx) {
746
860
  const firstStep = !this.host;
747
861
  const host = this.mount();
748
862
  const from = this.popover?.style.transform || null;
@@ -750,7 +864,8 @@ var DomRenderer = class {
750
864
  this.ctx = ctx;
751
865
  this.target = ctx.step.target === void 0 ? null : resolveTarget(ctx.step.target, this.doc);
752
866
  const template = this.template(ctx);
753
- applyTheme(host, mergeThemes(this.options.theme, template?.theme, ctx.tour.options?.theme));
867
+ this.watchAppearance(ctx);
868
+ applyTheme(host, this.themeFor(ctx, template));
754
869
  this.setTemplateCss(template?.css);
755
870
  this.applyLook(host, this.resolveLook(ctx, template));
756
871
  this.settleUntil = performance.now() + this.duration(host) * 1.5;
@@ -780,6 +895,8 @@ var DomRenderer = class {
780
895
  }
781
896
  hide() {
782
897
  this.teardownStep();
898
+ this.schemeQuery?.removeEventListener("change", this.onSchemeChange);
899
+ this.schemeQuery = void 0;
783
900
  if (this.host) {
784
901
  this.host.remove();
785
902
  this.host = void 0;
@@ -964,6 +1081,55 @@ var DomRenderer = class {
964
1081
  shadow.insertBefore(style, before);
965
1082
  shadow.insertBefore(this.connector.el, before);
966
1083
  }
1084
+ /** Does anything here need the built-in presets? */
1085
+ usesPresets(ctx) {
1086
+ return this.appearance(ctx) !== "light" || needsPresets(this.options.theme) || needsPresets(ctx.tour.options?.theme) || needsPresets(this.template(ctx)?.theme);
1087
+ }
1088
+ loadPresets() {
1089
+ this.presetLoad ??= import("./themes.js").then((mod) => {
1090
+ this.presets = {
1091
+ light: mod.light,
1092
+ dark: mod.dark,
1093
+ minimal: mod.minimal,
1094
+ contrast: mod.contrast
1095
+ };
1096
+ });
1097
+ return this.presetLoad;
1098
+ }
1099
+ appearance(ctx) {
1100
+ return ctx.tour.options?.appearance ?? this.options.appearance ?? "light";
1101
+ }
1102
+ /** The surface tokens for the current appearance: dark, or nothing for light. */
1103
+ appearanceTheme(ctx) {
1104
+ const appearance = this.appearance(ctx);
1105
+ if (appearance === "dark") return this.presets?.dark;
1106
+ if (appearance !== "auto") return void 0;
1107
+ return this.prefersDark() ? this.presets?.dark : this.presets?.light;
1108
+ }
1109
+ prefersDark() {
1110
+ return this.doc.defaultView?.matchMedia?.("(prefers-color-scheme: dark)").matches ?? false;
1111
+ }
1112
+ /** Renderer, then the appearance surface, then template, then tour. */
1113
+ themeFor(ctx, template) {
1114
+ const presets = this.presets;
1115
+ return mergeThemes(resolveTheme(this.options.theme, presets), this.appearanceTheme(ctx), resolveTheme(template?.theme, presets), resolveTheme(ctx.tour.options?.theme, presets));
1116
+ }
1117
+ /** With `appearance: 'auto'`, follow the system setting while the tour runs. */
1118
+ watchAppearance(ctx) {
1119
+ const wanted = this.appearance(ctx) === "auto";
1120
+ if (wanted === (this.schemeQuery !== void 0)) return;
1121
+ if (!wanted) {
1122
+ this.schemeQuery?.removeEventListener("change", this.onSchemeChange);
1123
+ this.schemeQuery = void 0;
1124
+ return;
1125
+ }
1126
+ this.schemeQuery = this.doc.defaultView?.matchMedia?.("(prefers-color-scheme: dark)");
1127
+ this.schemeQuery?.addEventListener("change", this.onSchemeChange);
1128
+ }
1129
+ onSchemeChange = () => {
1130
+ const ctx = this.ctx;
1131
+ if (ctx && this.host) applyTheme(this.host, this.themeFor(ctx, this.template(ctx)));
1132
+ };
967
1133
  resolveLook(ctx, template) {
968
1134
  const tour = ctx.tour.options ?? {};
969
1135
  const step = ctx.step;
@@ -1364,6 +1530,7 @@ var DomTourController = class extends TourController {
1364
1530
  followRoutes;
1365
1531
  constructor(tour, options = {}) {
1366
1532
  const { renderer: rendererOptions, followRoutes, ...rest } = options;
1533
+ warnIfInvalid(tour);
1367
1534
  const renderer = new DomRenderer(rendererOptions);
1368
1535
  super({
1369
1536
  ...rest,
@@ -1469,7 +1636,7 @@ function createDocent(options = {}) {
1469
1636
  ...renderer,
1470
1637
  document: doc
1471
1638
  } : { ...renderer };
1472
- return new Docent({
1639
+ const docent = new Docent({
1473
1640
  ...rest,
1474
1641
  storage: rest.storage ?? createLocalStorage(),
1475
1642
  environment: createDomEnvironment(doc),
@@ -1478,8 +1645,10 @@ function createDocent(options = {}) {
1478
1645
  renderer: rendererOptions
1479
1646
  })
1480
1647
  });
1648
+ warnAboutTours(docent);
1649
+ return docent;
1481
1650
  }
1482
1651
  //#endregion
1483
- export { CONNECTOR_STYLES, DEFAULT_LABELS, DomRenderer, DomTourController, NAME_ATTRIBUTE, Overlay, THEME_VARS, applyTheme, arrowGap, availableSpace, buildHeadlessShell, buildPopover, candidateSelectors, centerPosition, computePosition, createDocent, createDomEnvironment, createLocalStorage, createTour, defineTour, findOccluder, formatProgress, holePath, inflate, isConnector, isSafeUrl, mergeThemes, parsePlacement, queryAllDeep, renderBody, renderMedia, resolveSlots, resolveTarget, toSpec, uncover, waitForTarget };
1652
+ export { CONNECTOR_STYLES, DEFAULT_LABELS, DomRenderer, DomTourController, NAME_ATTRIBUTE, Overlay, THEME_VARS, applyTheme, arrowGap, availableSpace, buildHeadlessShell, buildPopover, candidateSelectors, centerPosition, computePosition, createDocent, createDomEnvironment, createLocalStorage, createTour, defineTour, findOccluder, formatProgress, holePath, inflate, isConnector, isSafeUrl, mergeThemeSpecs, mergeThemes, parsePlacement, queryAllDeep, renderBody, renderMedia, resolveSlots, resolveTarget, themeOption, toSpec, uncover, waitForTarget };
1484
1653
 
1485
1654
  //# sourceMappingURL=index.js.map