@docentjs/dom 0.5.2 → 0.7.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.cjs +213 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +40 -6
- package/dist/index.d.ts +40 -6
- package/dist/index.js +211 -5
- package/dist/index.js.map +1 -1
- package/dist/validate.cjs +9 -0
- package/dist/validate.d.cts +1 -0
- package/dist/validate.d.ts +1 -0
- package/dist/validate.js +1 -0
- package/package.json +12 -2
package/dist/index.cjs
CHANGED
|
@@ -79,6 +79,80 @@ function renderMedia(doc, media) {
|
|
|
79
79
|
return video;
|
|
80
80
|
}
|
|
81
81
|
//#endregion
|
|
82
|
+
//#region src/dev.ts
|
|
83
|
+
const checked = /* @__PURE__ */ new WeakSet();
|
|
84
|
+
/**
|
|
85
|
+
* Written exactly like this on purpose: bundlers replace
|
|
86
|
+
* `process.env.NODE_ENV` literally, so a production build turns this into
|
|
87
|
+
* `false`, and the check plus its import are dropped. Without a bundler
|
|
88
|
+
* `process` is simply not defined, and the catch treats that as development.
|
|
89
|
+
*/
|
|
90
|
+
function isProduction() {
|
|
91
|
+
try {
|
|
92
|
+
return process.env.NODE_ENV === "production";
|
|
93
|
+
} catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Check every tour the manager knows, as they load. Tours that never start
|
|
99
|
+
* still get checked, which is where a broken condition usually hides.
|
|
100
|
+
*/
|
|
101
|
+
function warnAboutTours(docent) {
|
|
102
|
+
if (isProduction()) return;
|
|
103
|
+
const check = () => {
|
|
104
|
+
for (const tour of docent.getTours()) warnIfInvalid(tour);
|
|
105
|
+
};
|
|
106
|
+
docent.subscribe(check);
|
|
107
|
+
check();
|
|
108
|
+
}
|
|
109
|
+
/** Warn about anything wrong with this tour, once per tour, in development. */
|
|
110
|
+
function warnIfInvalid(tour) {
|
|
111
|
+
if (!tour || isProduction() || checked.has(tour)) return;
|
|
112
|
+
checked.add(tour);
|
|
113
|
+
import("@docentjs/core/validate").then(({ formatIssues, validateTour }) => {
|
|
114
|
+
const issues = validateTour(tour);
|
|
115
|
+
if (issues.length === 0) return;
|
|
116
|
+
const label = issues.filter((issue) => issue.level === "error").length > 0 ? "error" : "warning";
|
|
117
|
+
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/`);
|
|
118
|
+
}).catch(() => {});
|
|
119
|
+
}
|
|
120
|
+
//#endregion
|
|
121
|
+
//#region src/looks.ts
|
|
122
|
+
const BUILT_IN_LOOKS = {
|
|
123
|
+
/** The default: a dimmed page, a soft cutout, a small caret. */
|
|
124
|
+
spotlight: {},
|
|
125
|
+
/**
|
|
126
|
+
* A light touch for tips beside a feature: the page stays usable and
|
|
127
|
+
* clickable, with a glowing ring and a drawn curve instead of a scrim.
|
|
128
|
+
*/
|
|
129
|
+
hint: {
|
|
130
|
+
overlay: { style: "none" },
|
|
131
|
+
spotlight: {
|
|
132
|
+
ring: "glow",
|
|
133
|
+
padding: 6
|
|
134
|
+
},
|
|
135
|
+
arrow: "curve",
|
|
136
|
+
theme: { width: 300 }
|
|
137
|
+
},
|
|
138
|
+
/**
|
|
139
|
+
* For something to read rather than something to do: the page blurs away,
|
|
140
|
+
* nothing points anywhere, and the card is wider.
|
|
141
|
+
*/
|
|
142
|
+
announcement: {
|
|
143
|
+
overlay: {
|
|
144
|
+
style: "blur",
|
|
145
|
+
blur: 6
|
|
146
|
+
},
|
|
147
|
+
spotlight: {
|
|
148
|
+
ring: "none",
|
|
149
|
+
padding: 10
|
|
150
|
+
},
|
|
151
|
+
arrow: "none",
|
|
152
|
+
theme: { width: 420 }
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
//#endregion
|
|
82
156
|
//#region src/occlusion.ts
|
|
83
157
|
function isPinned(el) {
|
|
84
158
|
const view = el.ownerDocument.defaultView;
|
|
@@ -678,18 +752,82 @@ const THEME_VARS = {
|
|
|
678
752
|
connector: "connector",
|
|
679
753
|
ring: "ring"
|
|
680
754
|
};
|
|
755
|
+
/** Tokens that take a unit when given as a number. */
|
|
756
|
+
const UNITS = {
|
|
757
|
+
radius: "px",
|
|
758
|
+
width: "px",
|
|
759
|
+
duration: "ms"
|
|
760
|
+
};
|
|
761
|
+
/** `12` becomes `12px`, `220` becomes `220ms`, strings are passed through. */
|
|
762
|
+
function cssValue(key, value) {
|
|
763
|
+
return typeof value === "number" ? `${value}${UNITS[key] ?? ""}` : value;
|
|
764
|
+
}
|
|
681
765
|
/** Write theme tokens as inline custom properties on an element. Clears unset ones. */
|
|
682
766
|
function applyTheme(el, theme) {
|
|
683
767
|
for (const key of Object.keys(THEME_VARS)) {
|
|
684
768
|
const value = theme?.[key];
|
|
685
769
|
const prop = `--docent-${THEME_VARS[key]}`;
|
|
686
770
|
if (value === void 0) el.style.removeProperty(prop);
|
|
687
|
-
else el.style.setProperty(prop, value);
|
|
771
|
+
else el.style.setProperty(prop, cssValue(key, value));
|
|
772
|
+
}
|
|
773
|
+
if (theme?.accent !== void 0 && theme.accentForeground === void 0) {
|
|
774
|
+
const light = isLightColor(el, cssValue("accent", theme.accent));
|
|
775
|
+
if (light !== void 0) el.style.setProperty("--docent-accent-fg", light ? "var(--docent-fg)" : "var(--docent-bg)");
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* Is this colour light enough to need dark text on it? Resolves the colour
|
|
780
|
+
* through the browser, so any CSS colour works. Undefined when it cannot tell.
|
|
781
|
+
*/
|
|
782
|
+
function isLightColor(el, color) {
|
|
783
|
+
const view = el.ownerDocument.defaultView;
|
|
784
|
+
if (!view) return void 0;
|
|
785
|
+
const previous = el.style.color;
|
|
786
|
+
el.style.color = color;
|
|
787
|
+
const computed = view.getComputedStyle(el).color;
|
|
788
|
+
el.style.color = previous;
|
|
789
|
+
const rgb = computed.match(/^(?:rgba?|color\(srgb)[( ]([^)]+)\)?/);
|
|
790
|
+
if (rgb?.[1]) {
|
|
791
|
+
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));
|
|
792
|
+
const scale = computed.startsWith("color(") ? 255 : 1;
|
|
793
|
+
return (.2126 * r * scale + .7152 * g * scale + .0722 * b * scale) / 255 > .55;
|
|
688
794
|
}
|
|
795
|
+
const ok = computed.match(/^oklch\(\s*([\d.]+)(%?)/);
|
|
796
|
+
if (ok?.[1]) return Number.parseFloat(ok[1]) / (ok[2] === "%" ? 100 : 1) > .62;
|
|
689
797
|
}
|
|
690
798
|
function mergeThemes(...themes) {
|
|
691
799
|
return Object.assign({}, ...themes.filter(Boolean));
|
|
692
800
|
}
|
|
801
|
+
/**
|
|
802
|
+
* Layer one theme over another when either may be a preset name. Used by the
|
|
803
|
+
* framework adapters to merge a provider's defaults with a local theme.
|
|
804
|
+
*/
|
|
805
|
+
function mergeThemeSpecs(base, override) {
|
|
806
|
+
if (base === void 0) return override;
|
|
807
|
+
if (override === void 0) return base;
|
|
808
|
+
const first = typeof base === "string" ? { preset: base } : base;
|
|
809
|
+
const second = typeof override === "string" ? { preset: override } : override;
|
|
810
|
+
return {
|
|
811
|
+
...first,
|
|
812
|
+
...second
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
/** `{ theme }` when there is one, or nothing, for spreading into options. */
|
|
816
|
+
function themeOption(theme) {
|
|
817
|
+
return theme === void 0 ? {} : { theme };
|
|
818
|
+
}
|
|
819
|
+
/** True when this theme cannot be resolved without the built-in presets. */
|
|
820
|
+
function needsPresets(spec) {
|
|
821
|
+
return typeof spec === "string" || !!spec && typeof spec === "object" && "preset" in spec;
|
|
822
|
+
}
|
|
823
|
+
/** A preset name, tokens, or a preset with tokens on top, flattened to tokens. */
|
|
824
|
+
function resolveTheme(spec, presets) {
|
|
825
|
+
if (spec === void 0) return void 0;
|
|
826
|
+
if (typeof spec === "string") return presets?.[spec];
|
|
827
|
+
const { preset, ...tokens } = spec;
|
|
828
|
+
if (preset === void 0) return tokens;
|
|
829
|
+
return mergeThemes(presets?.[preset], tokens);
|
|
830
|
+
}
|
|
693
831
|
//#endregion
|
|
694
832
|
//#region src/renderer.ts
|
|
695
833
|
const DEFAULT_LOOK = {
|
|
@@ -721,6 +859,11 @@ var DomRenderer = class {
|
|
|
721
859
|
connectorLoading;
|
|
722
860
|
/** Arrow, spotlight and overlay settings for the current step. */
|
|
723
861
|
look = DEFAULT_LOOK;
|
|
862
|
+
/** Preset tokens, once loaded. */
|
|
863
|
+
presets;
|
|
864
|
+
presetLoad;
|
|
865
|
+
/** Set while `appearance: 'auto'` is following the system setting. */
|
|
866
|
+
schemeQuery;
|
|
724
867
|
/** Until then the step's own transition runs; scroll updates may animate. */
|
|
725
868
|
settleUntil = 0;
|
|
726
869
|
/** Play the connector draw-in on its next render. */
|
|
@@ -744,6 +887,12 @@ var DomRenderer = class {
|
|
|
744
887
|
return `${pathname}${search}`;
|
|
745
888
|
}
|
|
746
889
|
show(ctx) {
|
|
890
|
+
if (this.presets === void 0 && this.usesPresets(ctx)) return this.loadPresets().then(() => {
|
|
891
|
+
this.showNow(ctx);
|
|
892
|
+
});
|
|
893
|
+
this.showNow(ctx);
|
|
894
|
+
}
|
|
895
|
+
showNow(ctx) {
|
|
747
896
|
const firstStep = !this.host;
|
|
748
897
|
const host = this.mount();
|
|
749
898
|
const from = this.popover?.style.transform || null;
|
|
@@ -751,7 +900,8 @@ var DomRenderer = class {
|
|
|
751
900
|
this.ctx = ctx;
|
|
752
901
|
this.target = ctx.step.target === void 0 ? null : resolveTarget(ctx.step.target, this.doc);
|
|
753
902
|
const template = this.template(ctx);
|
|
754
|
-
|
|
903
|
+
this.watchAppearance(ctx);
|
|
904
|
+
applyTheme(host, this.themeFor(ctx, template));
|
|
755
905
|
this.setTemplateCss(template?.css);
|
|
756
906
|
this.applyLook(host, this.resolveLook(ctx, template));
|
|
757
907
|
this.settleUntil = performance.now() + this.duration(host) * 1.5;
|
|
@@ -781,6 +931,8 @@ var DomRenderer = class {
|
|
|
781
931
|
}
|
|
782
932
|
hide() {
|
|
783
933
|
this.teardownStep();
|
|
934
|
+
this.schemeQuery?.removeEventListener("change", this.onSchemeChange);
|
|
935
|
+
this.schemeQuery = void 0;
|
|
784
936
|
if (this.host) {
|
|
785
937
|
this.host.remove();
|
|
786
938
|
this.host = void 0;
|
|
@@ -965,6 +1117,55 @@ var DomRenderer = class {
|
|
|
965
1117
|
shadow.insertBefore(style, before);
|
|
966
1118
|
shadow.insertBefore(this.connector.el, before);
|
|
967
1119
|
}
|
|
1120
|
+
/** Does anything here need the built-in presets? */
|
|
1121
|
+
usesPresets(ctx) {
|
|
1122
|
+
return this.appearance(ctx) !== "light" || needsPresets(this.options.theme) || needsPresets(ctx.tour.options?.theme) || needsPresets(this.template(ctx)?.theme);
|
|
1123
|
+
}
|
|
1124
|
+
loadPresets() {
|
|
1125
|
+
this.presetLoad ??= Promise.resolve().then(() => require("./themes.cjs")).then((mod) => {
|
|
1126
|
+
this.presets = {
|
|
1127
|
+
light: mod.light,
|
|
1128
|
+
dark: mod.dark,
|
|
1129
|
+
minimal: mod.minimal,
|
|
1130
|
+
contrast: mod.contrast
|
|
1131
|
+
};
|
|
1132
|
+
});
|
|
1133
|
+
return this.presetLoad;
|
|
1134
|
+
}
|
|
1135
|
+
appearance(ctx) {
|
|
1136
|
+
return ctx.tour.options?.appearance ?? this.options.appearance ?? "light";
|
|
1137
|
+
}
|
|
1138
|
+
/** The surface tokens for the current appearance: dark, or nothing for light. */
|
|
1139
|
+
appearanceTheme(ctx) {
|
|
1140
|
+
const appearance = this.appearance(ctx);
|
|
1141
|
+
if (appearance === "dark") return this.presets?.dark;
|
|
1142
|
+
if (appearance !== "auto") return void 0;
|
|
1143
|
+
return this.prefersDark() ? this.presets?.dark : this.presets?.light;
|
|
1144
|
+
}
|
|
1145
|
+
prefersDark() {
|
|
1146
|
+
return this.doc.defaultView?.matchMedia?.("(prefers-color-scheme: dark)").matches ?? false;
|
|
1147
|
+
}
|
|
1148
|
+
/** Renderer, then the appearance surface, then template, then tour. */
|
|
1149
|
+
themeFor(ctx, template) {
|
|
1150
|
+
const presets = this.presets;
|
|
1151
|
+
return mergeThemes(resolveTheme(this.options.theme, presets), this.appearanceTheme(ctx), resolveTheme(template?.theme, presets), resolveTheme(ctx.tour.options?.theme, presets));
|
|
1152
|
+
}
|
|
1153
|
+
/** With `appearance: 'auto'`, follow the system setting while the tour runs. */
|
|
1154
|
+
watchAppearance(ctx) {
|
|
1155
|
+
const wanted = this.appearance(ctx) === "auto";
|
|
1156
|
+
if (wanted === (this.schemeQuery !== void 0)) return;
|
|
1157
|
+
if (!wanted) {
|
|
1158
|
+
this.schemeQuery?.removeEventListener("change", this.onSchemeChange);
|
|
1159
|
+
this.schemeQuery = void 0;
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
this.schemeQuery = this.doc.defaultView?.matchMedia?.("(prefers-color-scheme: dark)");
|
|
1163
|
+
this.schemeQuery?.addEventListener("change", this.onSchemeChange);
|
|
1164
|
+
}
|
|
1165
|
+
onSchemeChange = () => {
|
|
1166
|
+
const ctx = this.ctx;
|
|
1167
|
+
if (ctx && this.host) applyTheme(this.host, this.themeFor(ctx, this.template(ctx)));
|
|
1168
|
+
};
|
|
968
1169
|
resolveLook(ctx, template) {
|
|
969
1170
|
const tour = ctx.tour.options ?? {};
|
|
970
1171
|
const step = ctx.step;
|
|
@@ -1088,9 +1289,11 @@ var DomRenderer = class {
|
|
|
1088
1289
|
win.addEventListener("scrollend", finish, { once: true });
|
|
1089
1290
|
this.cleanups.push(stop);
|
|
1090
1291
|
}
|
|
1292
|
+
/** The tour's template: one the app registered, or a built-in look. */
|
|
1091
1293
|
template(ctx) {
|
|
1092
1294
|
const name = ctx.tour.options?.template ?? this.options.template;
|
|
1093
|
-
|
|
1295
|
+
if (name === void 0) return void 0;
|
|
1296
|
+
return this.options.templates?.[name] ?? BUILT_IN_LOOKS[name];
|
|
1094
1297
|
}
|
|
1095
1298
|
buildDefault(ctx, host, template) {
|
|
1096
1299
|
const slots = {
|
|
@@ -1365,6 +1568,7 @@ var DomTourController = class extends _docentjs_core.TourController {
|
|
|
1365
1568
|
followRoutes;
|
|
1366
1569
|
constructor(tour, options = {}) {
|
|
1367
1570
|
const { renderer: rendererOptions, followRoutes, ...rest } = options;
|
|
1571
|
+
warnIfInvalid(tour);
|
|
1368
1572
|
const renderer = new DomRenderer(rendererOptions);
|
|
1369
1573
|
super({
|
|
1370
1574
|
...rest,
|
|
@@ -1470,7 +1674,7 @@ function createDocent(options = {}) {
|
|
|
1470
1674
|
...renderer,
|
|
1471
1675
|
document: doc
|
|
1472
1676
|
} : { ...renderer };
|
|
1473
|
-
|
|
1677
|
+
const docent = new _docentjs_core.Docent({
|
|
1474
1678
|
...rest,
|
|
1475
1679
|
storage: rest.storage ?? createLocalStorage(),
|
|
1476
1680
|
environment: createDomEnvironment(doc),
|
|
@@ -1479,8 +1683,11 @@ function createDocent(options = {}) {
|
|
|
1479
1683
|
renderer: rendererOptions
|
|
1480
1684
|
})
|
|
1481
1685
|
});
|
|
1686
|
+
warnAboutTours(docent);
|
|
1687
|
+
return docent;
|
|
1482
1688
|
}
|
|
1483
1689
|
//#endregion
|
|
1690
|
+
exports.BUILT_IN_LOOKS = BUILT_IN_LOOKS;
|
|
1484
1691
|
exports.CONNECTOR_STYLES = require_arrows.CONNECTOR_STYLES;
|
|
1485
1692
|
exports.DEFAULT_LABELS = DEFAULT_LABELS;
|
|
1486
1693
|
exports.DomRenderer = DomRenderer;
|
|
@@ -1512,6 +1719,7 @@ exports.holePath = holePath;
|
|
|
1512
1719
|
exports.inflate = inflate;
|
|
1513
1720
|
exports.isConnector = require_arrows.isConnector;
|
|
1514
1721
|
exports.isSafeUrl = isSafeUrl;
|
|
1722
|
+
exports.mergeThemeSpecs = mergeThemeSpecs;
|
|
1515
1723
|
exports.mergeThemes = mergeThemes;
|
|
1516
1724
|
exports.parsePlacement = parsePlacement;
|
|
1517
1725
|
exports.queryAllDeep = queryAllDeep;
|
|
@@ -1519,6 +1727,7 @@ exports.renderBody = renderBody;
|
|
|
1519
1727
|
exports.renderMedia = renderMedia;
|
|
1520
1728
|
exports.resolveSlots = resolveSlots;
|
|
1521
1729
|
exports.resolveTarget = resolveTarget;
|
|
1730
|
+
exports.themeOption = themeOption;
|
|
1522
1731
|
exports.toSpec = toSpec;
|
|
1523
1732
|
exports.uncover = uncover;
|
|
1524
1733
|
exports.waitForTarget = waitForTarget;
|