@guidelytech/guide-runtime 0.1.0 → 0.2.1

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 CHANGED
@@ -20,8 +20,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
21
21
 
22
22
  // src/index.ts
23
- var index_exports = {};
24
- __export(index_exports, {
23
+ var src_exports = {};
24
+ __export(src_exports, {
25
25
  DEFAULT_THEME: () => DEFAULT_THEME,
26
26
  HOST_ID: () => HOST_ID2,
27
27
  STYLES: () => STYLES,
@@ -68,7 +68,7 @@ __export(index_exports, {
68
68
  urlMatches: () => urlMatches,
69
69
  waitForTarget: () => waitForTarget
70
70
  });
71
- module.exports = __toCommonJS(index_exports);
71
+ module.exports = __toCommonJS(src_exports);
72
72
 
73
73
  // src/selectors.ts
74
74
  var import_runtime = require("@guidelytech/shared/runtime");
@@ -506,6 +506,7 @@ var STYLES = `
506
506
  .g-panel-title{margin:0;font-size:16px;font-weight:600}
507
507
  .g-panel-sub{margin:4px 0 0;font-size:12px;color:var(--g-muted)}
508
508
  .g-panel-body{overflow:auto;padding:8px 0;flex:1}
509
+ .g-help{display:flex;flex-direction:column;min-height:0;flex:1}
509
510
  .g-bar{height:6px;border-radius:999px;background:var(--g-subtle);overflow:hidden;margin-top:10px}
510
511
  .g-bar > span{display:block;height:100%;background:var(--g-primary);transition:width .3s}
511
512
  .g-task{display:flex;gap:12px;align-items:flex-start;width:100%;padding:12px 20px;border:0;background:transparent;text-align:left;font:inherit;color:inherit;cursor:pointer;min-height:44px}
@@ -782,6 +783,7 @@ function trackRect(el2, cb) {
782
783
  }
783
784
 
784
785
  // src/render.ts
786
+ var import_shared = require("@guidelytech/shared");
785
787
  var FOCUSABLE = "button:not([disabled]),[href],input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex='-1'])";
786
788
  var uid = 0;
787
789
  var nextId = (p) => `g-${p}-${++uid}`;
@@ -791,8 +793,8 @@ function h(doc, tag, className, text) {
791
793
  if (text != null) el2.textContent = text;
792
794
  return el2;
793
795
  }
794
- function buildButtons(doc, step, onButton) {
795
- return step.buttons.map((b) => {
796
+ function buildButtons(doc, step, canGoBack, onButton) {
797
+ return (0, import_shared.visibleButtons)(step, canGoBack).map((b) => {
796
798
  const btn = h(doc, "button", `g-btn g-btn-${b.variant}`, b.label);
797
799
  btn.type = "button";
798
800
  btn.setAttribute("data-action", b.action);
@@ -839,7 +841,7 @@ function buildCard(doc, opts, extraClass) {
839
841
  setSanitizedHtml(b, step.body);
840
842
  inner.appendChild(b);
841
843
  }
842
- const buttons = buildButtons(doc, step, opts.onButton);
844
+ const buttons = buildButtons(doc, step, opts.canGoBack ?? opts.index > 0, opts.onButton);
843
845
  if (buttons.length || step.showProgress && opts.total > 1) {
844
846
  const footer = h(doc, "div", "g-footer");
845
847
  if (step.showProgress && opts.total > 1) footer.appendChild(h(doc, "span", "g-progress", `${opts.index + 1} of ${opts.total}`));
@@ -1079,7 +1081,7 @@ function renderBanner(doc, opts) {
1079
1081
  setSanitizedHtml(text, step.body);
1080
1082
  body.appendChild(text);
1081
1083
  banner.appendChild(body);
1082
- buildButtons(doc, step, opts.onButton).forEach((b) => banner.appendChild(b));
1084
+ buildButtons(doc, step, opts.canGoBack ?? opts.index > 0, opts.onButton).forEach((b) => banner.appendChild(b));
1083
1085
  if (step.showClose) {
1084
1086
  const close = h(doc, "button", "g-close", "\xD7");
1085
1087
  close.type = "button";
@@ -1354,6 +1356,7 @@ function runGuide(guide, options = {}) {
1354
1356
  step,
1355
1357
  index,
1356
1358
  total,
1359
+ canGoBack: history.length > 0 || index > 0,
1357
1360
  target,
1358
1361
  asModal,
1359
1362
  onButton: handleButton,
@@ -1918,14 +1921,14 @@ function renderSurvey(options) {
1918
1921
  };
1919
1922
  }
1920
1923
 
1921
- // src/resource-center.ts
1924
+ // src/help-view.ts
1922
1925
  function el(doc, tag, cls, text) {
1923
1926
  const e = doc.createElement(tag);
1924
1927
  if (cls) e.className = cls;
1925
1928
  if (text != null) e.textContent = text;
1926
1929
  return e;
1927
1930
  }
1928
- function item(doc, icon, title, summary) {
1931
+ function listItem(doc, icon, title, summary) {
1929
1932
  const b = el(doc, "button", "g-rc-item");
1930
1933
  b.type = "button";
1931
1934
  const i = el(doc, "span", "g-rc-icon", icon);
@@ -1936,38 +1939,14 @@ function item(doc, icon, title, summary) {
1936
1939
  b.append(i, text);
1937
1940
  return b;
1938
1941
  }
1939
- function renderResourceCenter(options) {
1940
- const { scope, config } = options;
1941
- const doc = scope.ownerDocument;
1942
- const guides = options.guides ?? [];
1943
- let open = false;
1942
+ function createHelpView(options) {
1943
+ const { doc } = options;
1944
1944
  let query = "";
1945
1945
  let serverResults = [];
1946
1946
  let searchSeq = 0;
1947
- let activeArticle = null;
1948
- const pos = `g-pos-${config.launcher.position}`;
1949
- const root = el(doc, "div", "g-resource-center");
1950
- const launcher = el(doc, "button", `g-launcher ${pos}`);
1951
- launcher.type = "button";
1952
- launcher.setAttribute("aria-expanded", "false");
1953
- if (config.launcher.showIcon) {
1954
- const icon = el(doc, "span", "", "?");
1955
- icon.setAttribute("aria-hidden", "true");
1956
- launcher.appendChild(icon);
1957
- }
1958
- launcher.appendChild(el(doc, "span", "", config.launcher.label || config.title));
1959
- launcher.setAttribute("aria-label", config.launcher.label || config.title);
1960
- if (options.hideLauncher) launcher.hidden = true;
1961
- const panel = el(doc, "div", `g-panel ${pos}`);
1962
- panel.setAttribute("role", "dialog");
1963
- panel.setAttribute("aria-label", config.title);
1964
- panel.hidden = true;
1965
- const head = el(doc, "div", "g-panel-head");
1966
- head.appendChild(el(doc, "h2", "g-panel-title", config.title));
1967
- const close = el(doc, "button", "g-close", "\xD7");
1968
- close.type = "button";
1969
- close.setAttribute("aria-label", "Close");
1970
- head.appendChild(close);
1947
+ let openSeq = 0;
1948
+ let active = null;
1949
+ const root = el(doc, "div", "g-help");
1971
1950
  const searchWrap = el(doc, "div", "g-rc-search");
1972
1951
  const input = el(doc, "input", "g-input");
1973
1952
  input.type = "search";
@@ -1975,94 +1954,103 @@ function renderResourceCenter(options) {
1975
1954
  input.setAttribute("aria-label", "Search help articles");
1976
1955
  searchWrap.appendChild(input);
1977
1956
  const body = el(doc, "div", "g-panel-body");
1978
- panel.append(head, searchWrap, body);
1979
- root.append(launcher, panel);
1980
- scope.appendChild(root);
1957
+ root.append(searchWrap, body);
1958
+ const hasBody = (a) => typeof a.body === "string" && !a.bodyOmitted;
1981
1959
  const openArticle = (article) => {
1982
1960
  if (article.externalUrl) {
1983
1961
  window.open(article.externalUrl, "_blank", "noopener,noreferrer");
1984
- options.onArticleView?.(article);
1962
+ if (hasBody(article)) options.onArticleView?.(article);
1985
1963
  return;
1986
1964
  }
1987
- activeArticle = article;
1988
- options.onArticleView?.(article);
1989
- renderBody();
1965
+ const seq = ++openSeq;
1966
+ if (hasBody(article) || !options.loadArticle) {
1967
+ active = { article, loading: false };
1968
+ if (hasBody(article)) options.onArticleView?.(article);
1969
+ return render();
1970
+ }
1971
+ active = { article, loading: true };
1972
+ render();
1973
+ options.loadArticle(article.id).then((full) => {
1974
+ if (seq !== openSeq || !active) return;
1975
+ active = { article: full ?? { ...article, body: "" }, loading: false };
1976
+ if (full) options.onArticleView?.(full);
1977
+ render();
1978
+ }).catch(() => {
1979
+ if (seq !== openSeq || !active) return;
1980
+ active = { article: { ...article, body: "" }, loading: false };
1981
+ render();
1982
+ });
1990
1983
  };
1991
- const renderArticle = (article) => {
1984
+ const renderArticle = ({ article, loading }) => {
1992
1985
  const wrap = el(doc, "div", "g-rc-article");
1993
1986
  const back = el(doc, "button", "g-btn g-btn-link g-rc-back", "\u2190 Back");
1994
1987
  back.type = "button";
1995
1988
  back.addEventListener("click", () => {
1996
- activeArticle = null;
1997
- renderBody();
1989
+ openSeq++;
1990
+ active = null;
1991
+ render();
1998
1992
  });
1999
1993
  const title = el(doc, "h3", "g-title", article.title);
2000
1994
  const content = el(doc, "div", "g-body");
2001
- setSanitizedHtml(content, article.body);
1995
+ if (loading) content.appendChild(el(doc, "div", "g-rc-empty", "Loading\u2026"));
1996
+ else if (article.body) setSanitizedHtml(content, article.body);
1997
+ else content.appendChild(el(doc, "div", "g-rc-empty", "This article couldn't be loaded."));
2002
1998
  wrap.append(back, title, content);
2003
1999
  body.appendChild(wrap);
2004
2000
  back.focus?.({ preventScroll: true });
2005
2001
  };
2006
2002
  const section = (label) => body.appendChild(el(doc, "div", "g-rc-section", label));
2007
- const renderBody = () => {
2003
+ const articleButton = (a) => {
2004
+ const b = listItem(doc, "\u{1F4C4}", a.title, a.summary);
2005
+ b.addEventListener("click", () => openArticle(a));
2006
+ body.appendChild(b);
2007
+ };
2008
+ function render() {
2008
2009
  body.textContent = "";
2009
- if (activeArticle) return renderArticle(activeArticle);
2010
+ if (active) return renderArticle(active);
2011
+ const data = options.data();
2010
2012
  const q = query.trim().toLowerCase();
2011
2013
  if (q) {
2012
- const local = config.articles.filter((a) => a.title.toLowerCase().includes(q) || a.summary.toLowerCase().includes(q));
2014
+ const local = data.articles.filter((a) => a.title.toLowerCase().includes(q) || a.summary.toLowerCase().includes(q));
2013
2015
  const seen = new Set(local.map((a) => a.id));
2014
2016
  const merged = [...local, ...serverResults.filter((a) => !seen.has(a.id))];
2015
2017
  section(`Results (${merged.length})`);
2016
2018
  if (!merged.length) body.appendChild(el(doc, "div", "g-rc-empty", "No articles match your search."));
2017
- merged.forEach((a) => {
2018
- const b = item(doc, "\u{1F4C4}", a.title, a.summary);
2019
- b.addEventListener("click", () => openArticle(a));
2020
- body.appendChild(b);
2021
- });
2019
+ merged.forEach(articleButton);
2022
2020
  return;
2023
2021
  }
2024
- const recommended = config.recommendedGuideIds.map((id) => guides.find((g) => g.id === id || g.key === id)).filter((g) => !!g);
2025
- if (recommended.length) {
2022
+ if (data.recommended?.length) {
2026
2023
  section("Recommended");
2027
- recommended.forEach((g) => {
2028
- const b = item(doc, "\u25B6", g.name, "Start guide");
2024
+ data.recommended.forEach((g) => {
2025
+ const b = listItem(doc, "\u25B6", g.name, "Start guide");
2029
2026
  b.addEventListener("click", () => options.onStartGuide?.(g.id));
2030
2027
  body.appendChild(b);
2031
2028
  });
2032
2029
  }
2033
- const checklists = config.checklistGuideIds.map((id) => guides.find((g) => g.id === id || g.key === id)).filter((g) => !!g);
2034
- if (checklists.length) {
2030
+ if (data.checklists?.length) {
2035
2031
  section("Checklists");
2036
- checklists.forEach((g) => {
2037
- const b = item(doc, "\u2611", g.name, "Open checklist");
2032
+ data.checklists.forEach((g) => {
2033
+ const b = listItem(doc, "\u2611", g.name, "Open checklist");
2038
2034
  b.addEventListener("click", () => options.onOpenChecklist?.(g.id));
2039
2035
  body.appendChild(b);
2040
2036
  });
2041
2037
  }
2042
- const collections = [...config.collections].sort((a, b) => a.order - b.order);
2043
- const uncollected = config.articles.filter((a) => !a.collectionId || !collections.some((c) => c.id === a.collectionId));
2038
+ const collections = [...data.collections].sort((a, b) => a.order - b.order);
2039
+ const uncollected = data.articles.filter((a) => !a.collectionId || !collections.some((c) => c.id === a.collectionId));
2044
2040
  collections.forEach((c) => {
2045
- const articles = config.articles.filter((a) => a.collectionId === c.id);
2041
+ const articles = data.articles.filter((a) => a.collectionId === c.id);
2046
2042
  if (!articles.length) return;
2047
2043
  section(c.name);
2048
- articles.forEach((a) => {
2049
- const b = item(doc, "\u{1F4C4}", a.title, a.summary);
2050
- b.addEventListener("click", () => openArticle(a));
2051
- body.appendChild(b);
2052
- });
2044
+ articles.forEach(articleButton);
2053
2045
  });
2054
2046
  if (uncollected.length) {
2055
2047
  section("Articles");
2056
- uncollected.forEach((a) => {
2057
- const b = item(doc, "\u{1F4C4}", a.title, a.summary);
2058
- b.addEventListener("click", () => openArticle(a));
2059
- body.appendChild(b);
2060
- });
2048
+ uncollected.forEach(articleButton);
2061
2049
  }
2062
- if (config.faqs.length) {
2050
+ if (data.faqs?.length) {
2063
2051
  section("FAQ");
2064
- config.faqs.forEach((f) => {
2065
- const b = item(doc, "?", f.question);
2052
+ data.faqs.forEach((f) => {
2053
+ const b = listItem(doc, "?", f.question);
2066
2054
  b.setAttribute("aria-expanded", "false");
2067
2055
  const a = el(doc, "div", "g-faq-a");
2068
2056
  setSanitizedHtml(a, f.answer);
@@ -2074,9 +2062,9 @@ function renderResourceCenter(options) {
2074
2062
  body.append(b, a);
2075
2063
  });
2076
2064
  }
2077
- if (config.links.length) {
2065
+ if (data.links?.length) {
2078
2066
  section("Links");
2079
- config.links.forEach((l) => {
2067
+ data.links.forEach((l) => {
2080
2068
  const a = el(doc, "a", "g-rc-item");
2081
2069
  a.href = l.url;
2082
2070
  a.target = "_blank";
@@ -2088,12 +2076,13 @@ function renderResourceCenter(options) {
2088
2076
  });
2089
2077
  }
2090
2078
  if (!body.childNodes.length) body.appendChild(el(doc, "div", "g-rc-empty", "Nothing here yet."));
2091
- };
2079
+ }
2092
2080
  let debounce = null;
2093
- input.addEventListener("input", () => {
2081
+ const onInput = () => {
2094
2082
  query = input.value;
2095
- activeArticle = null;
2096
- renderBody();
2083
+ openSeq++;
2084
+ active = null;
2085
+ render();
2097
2086
  if (debounce) clearTimeout(debounce);
2098
2087
  if (!options.search || !query.trim()) {
2099
2088
  serverResults = [];
@@ -2104,22 +2093,92 @@ function renderResourceCenter(options) {
2104
2093
  options.search?.(query.trim()).then((results) => {
2105
2094
  if (seq !== searchSeq) return;
2106
2095
  serverResults = Array.isArray(results) ? results : [];
2107
- renderBody();
2096
+ render();
2108
2097
  }).catch(() => {
2109
2098
  });
2110
2099
  }, 250);
2100
+ };
2101
+ input.addEventListener("input", onInput);
2102
+ return {
2103
+ el: root,
2104
+ render,
2105
+ reset() {
2106
+ openSeq++;
2107
+ active = null;
2108
+ query = "";
2109
+ input.value = "";
2110
+ serverResults = [];
2111
+ },
2112
+ openArticle(id) {
2113
+ const listed = options.data().articles.find((a) => a.id === id) ?? { id, slug: id, title: "Article", summary: "", updatedAt: "", bodyOmitted: true };
2114
+ openArticle(listed);
2115
+ },
2116
+ focusSearch: () => input.focus?.({ preventScroll: true }),
2117
+ setQuery(q) {
2118
+ input.value = q;
2119
+ onInput();
2120
+ },
2121
+ destroy() {
2122
+ if (debounce) clearTimeout(debounce);
2123
+ input.removeEventListener("input", onInput);
2124
+ root.remove();
2125
+ }
2126
+ };
2127
+ }
2128
+
2129
+ // src/resource-center.ts
2130
+ function renderResourceCenter(options) {
2131
+ const { scope, config } = options;
2132
+ const doc = scope.ownerDocument;
2133
+ const guides = options.guides ?? [];
2134
+ let open = false;
2135
+ const pos = `g-pos-${config.launcher.position}`;
2136
+ const byId = (ids) => ids.map((id) => guides.find((g) => g.id === id || g.key === id)).filter((g) => !!g);
2137
+ const root = el(doc, "div", "g-resource-center");
2138
+ const launcher = el(doc, "button", `g-launcher ${pos}`);
2139
+ launcher.type = "button";
2140
+ launcher.setAttribute("aria-expanded", "false");
2141
+ if (config.launcher.showIcon) {
2142
+ const icon = el(doc, "span", "", "?");
2143
+ icon.setAttribute("aria-hidden", "true");
2144
+ launcher.appendChild(icon);
2145
+ }
2146
+ launcher.appendChild(el(doc, "span", "", config.launcher.label || config.title));
2147
+ launcher.setAttribute("aria-label", config.launcher.label || config.title);
2148
+ if (options.hideLauncher) launcher.hidden = true;
2149
+ const panel = el(doc, "div", `g-panel ${pos}`);
2150
+ panel.setAttribute("role", "dialog");
2151
+ panel.setAttribute("aria-label", config.title);
2152
+ panel.hidden = true;
2153
+ const head = el(doc, "div", "g-panel-head");
2154
+ head.appendChild(el(doc, "h2", "g-panel-title", config.title));
2155
+ const close = el(doc, "button", "g-close", "\xD7");
2156
+ close.type = "button";
2157
+ close.setAttribute("aria-label", "Close");
2158
+ head.appendChild(close);
2159
+ const help = createHelpView({
2160
+ doc,
2161
+ data: () => ({ collections: config.collections, articles: config.articles, recommended: byId(config.recommendedGuideIds), checklists: byId(config.checklistGuideIds), faqs: config.faqs, links: config.links }),
2162
+ search: options.search,
2163
+ loadArticle: options.loadArticle,
2164
+ onStartGuide: options.onStartGuide,
2165
+ onOpenChecklist: options.onOpenChecklist,
2166
+ onArticleView: options.onArticleView
2111
2167
  });
2168
+ panel.append(head, help.el);
2169
+ root.append(launcher, panel);
2170
+ scope.appendChild(root);
2112
2171
  const setOpen = (v) => {
2113
2172
  if (open === v) return;
2114
2173
  open = v;
2115
2174
  panel.hidden = !v;
2116
2175
  launcher.setAttribute("aria-expanded", String(v));
2117
2176
  if (v) {
2118
- renderBody();
2119
- input.focus?.({ preventScroll: true });
2177
+ help.render();
2178
+ help.focusSearch();
2120
2179
  options.onOpen?.();
2121
2180
  } else {
2122
- activeArticle = null;
2181
+ help.reset();
2123
2182
  options.onClose?.();
2124
2183
  }
2125
2184
  };
@@ -2140,7 +2199,7 @@ function renderResourceCenter(options) {
2140
2199
  toggle: () => setOpen(!open),
2141
2200
  isOpen: () => open,
2142
2201
  destroy() {
2143
- if (debounce) clearTimeout(debounce);
2202
+ help.destroy();
2144
2203
  panel.removeEventListener("keydown", onKey);
2145
2204
  root.remove();
2146
2205
  }