athena-portal-app-kit 1.0.0 → 1.0.2

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.
@@ -6,7 +6,7 @@
6
6
  "use strict";
7
7
 
8
8
  var CONFIG_PATH = "./athena-app.config.json";
9
- var DEFAULT_KIT = "https://cdn.jsdelivr.net/npm/athena-portal-app-kit@1.0.0";
9
+ var DEFAULT_KIT = "https://cdn.jsdelivr.net/npm/athena-portal-app-kit@1";
10
10
  var DEFAULT_PORTAL = "https://backoffice.athenacare.health";
11
11
 
12
12
  function loadScript(src) {
@@ -76,6 +76,10 @@
76
76
  .then(function (config) {
77
77
  if (!config.portalOrigin) config.portalOrigin = DEFAULT_PORTAL;
78
78
  if (!config.kitOrigin) config.kitOrigin = DEFAULT_KIT;
79
+ globalThis.__ATHENA_KIT_ORIGIN__ = (config.kitOrigin || DEFAULT_KIT).replace(
80
+ /\/$/,
81
+ "",
82
+ );
79
83
  global.__ATHENA_APP_CONFIG__ = config;
80
84
 
81
85
  var urls = deriveAssetUrls(config);
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Registers Web Awesome default icon library → kit sharp-solid SVGs.
3
+ * Load as type="module" after webawesome.loader.js. Set window.__ATHENA_KIT_ORIGIN__
4
+ * before this script (athena-bootstrap and preview templates do).
5
+ */
6
+ const DEFAULT_KIT = "https://cdn.jsdelivr.net/npm/athena-portal-app-kit@1";
7
+ const kit = String(globalThis.__ATHENA_KIT_ORIGIN__ || DEFAULT_KIT).replace(
8
+ /\/$/,
9
+ "",
10
+ );
11
+
12
+ const { registerIconLibrary } = await import(
13
+ kit + "/assets/webawesome/dist-cdn/webawesome.loader.js"
14
+ );
15
+
16
+ registerIconLibrary("default", {
17
+ resolver: (name) =>
18
+ kit +
19
+ "/assets/fontawesome/svgs/sharp-solid/" +
20
+ encodeURIComponent(String(name || "link").trim() || "link") +
21
+ ".svg",
22
+ mutator: (svg) => {
23
+ svg.setAttribute("fill", "currentColor");
24
+ },
25
+ });
@@ -86,6 +86,9 @@
86
86
  }
87
87
 
88
88
  function fetchMe() {
89
+ if (global.__ATHENA_VIEWER__) {
90
+ return Promise.resolve(global.__ATHENA_VIEWER__);
91
+ }
89
92
  if (inflight) return inflight;
90
93
  var config = getConfig();
91
94
 
@@ -0,0 +1,313 @@
1
+ /**
2
+ * Single-file Back Office preview — Claude artifact / preview.html only.
3
+ * Renders wa-page + drawer around an existing <main class="portal-app">.
4
+ * No iframe, no sibling files, no portal API calls.
5
+ */
6
+ (function () {
7
+ "use strict";
8
+
9
+ var DEFAULT_KIT = "https://cdn.jsdelivr.net/npm/athena-portal-app-kit@1";
10
+ var ROLES = [
11
+ "staff",
12
+ "providers",
13
+ "managers",
14
+ "executive",
15
+ "board",
16
+ "admin",
17
+ ];
18
+ var ROLE_RANK = {
19
+ admin: 5,
20
+ board: 4,
21
+ executive: 3,
22
+ managers: 2,
23
+ providers: 1,
24
+ staff: 0,
25
+ };
26
+
27
+ var activeKit = DEFAULT_KIT;
28
+
29
+ function el(tag, className) {
30
+ var node = document.createElement(tag);
31
+ if (className) node.className = className;
32
+ return node;
33
+ }
34
+
35
+ function injectStylesheet(href) {
36
+ var link = document.createElement("link");
37
+ link.rel = "stylesheet";
38
+ link.href = href;
39
+ document.head.appendChild(link);
40
+ }
41
+
42
+ function faKitSolidIconSrc(icon) {
43
+ var name = (icon || "link").trim() || "link";
44
+ return (
45
+ activeKit +
46
+ "/assets/fontawesome/svgs/sharp-solid/" +
47
+ encodeURIComponent(name) +
48
+ ".svg"
49
+ );
50
+ }
51
+
52
+ function defaultRole(config) {
53
+ return (
54
+ (config.dev && config.dev.role) ||
55
+ config.minRole ||
56
+ "staff"
57
+ );
58
+ }
59
+
60
+ function buildViewer(role) {
61
+ if (ROLE_RANK[role] === undefined) role = "staff";
62
+ return {
63
+ userId: "00000000-0000-0000-0000-000000000001",
64
+ email: "preview@local.test",
65
+ firstName: "Preview",
66
+ lastName: "User",
67
+ role: role,
68
+ status: "active",
69
+ roleRank: ROLE_RANK[role] ?? 0,
70
+ fixture: true,
71
+ };
72
+ }
73
+
74
+ function setPreviewViewer(role) {
75
+ globalThis.__ATHENA_VIEWER__ = buildViewer(role);
76
+ if (globalThis.AthenaMe && typeof globalThis.AthenaMe.refresh === "function") {
77
+ globalThis.AthenaMe.refresh();
78
+ }
79
+ }
80
+
81
+ function syntheticNav(config) {
82
+ var href = config.appKey ? "/apps/" + config.appKey : "#";
83
+ var category = config.navCategory || "applications";
84
+ if (
85
+ category !== "applications" &&
86
+ category !== "dashboards" &&
87
+ category !== "administration"
88
+ ) {
89
+ category = "applications";
90
+ }
91
+ var entry = {
92
+ key: config.appKey || "preview-app",
93
+ href: href,
94
+ label: config.label || config.appKey || "Your app",
95
+ icon: config.icon || "link",
96
+ };
97
+ var sections = {
98
+ applications: [],
99
+ dashboards: [],
100
+ administration: [],
101
+ };
102
+ sections[category] = [entry];
103
+ return sections;
104
+ }
105
+
106
+ function sortLinks(links) {
107
+ return links.slice().sort(function (a, b) {
108
+ return String(a.label).localeCompare(String(b.label));
109
+ });
110
+ }
111
+
112
+ function navIcon(entry) {
113
+ var wa = document.createElement("wa-icon");
114
+ wa.setAttribute("src", faKitSolidIconSrc(entry.icon || "link"));
115
+ wa.setAttribute("label", "");
116
+ return wa;
117
+ }
118
+
119
+ function sidebarLink(entry, activeHref) {
120
+ var a = el("a", "shell-sidebar-link");
121
+ a.href = entry.href;
122
+ a.setAttribute("data-shell-link", entry.key);
123
+ if (activeHref && entry.href === activeHref) {
124
+ a.setAttribute("aria-current", "page");
125
+ }
126
+ a.addEventListener("click", function (e) {
127
+ e.preventDefault();
128
+ });
129
+ a.appendChild(navIcon(entry));
130
+ a.appendChild(document.createTextNode(entry.label));
131
+ return a;
132
+ }
133
+
134
+ function fillNavGroup(container, links, activeHref) {
135
+ if (!container) return;
136
+ container.replaceChildren();
137
+ sortLinks(links).forEach(function (link) {
138
+ container.appendChild(sidebarLink(link, activeHref));
139
+ });
140
+ }
141
+
142
+ function buildEmbedShell(config, role, sections) {
143
+ var activeHref = config.appKey ? "/apps/" + config.appKey : "";
144
+ var waPage = document.createElement("wa-page");
145
+ waPage.className = "shell athena-preview-shell";
146
+ waPage.setAttribute("mobile-breakpoint", "48rem");
147
+
148
+ var headerSlot = el("div", "shell-header-bar wa-cluster wa-gap-m");
149
+ headerSlot.setAttribute("slot", "header");
150
+
151
+ var menuBtn = document.createElement("wa-button");
152
+ menuBtn.setAttribute("type", "button");
153
+ menuBtn.setAttribute("appearance", "outlined");
154
+ menuBtn.setAttribute("variant", "neutral");
155
+ menuBtn.setAttribute("size", "s");
156
+ menuBtn.setAttribute("aria-label", "Open sidebar");
157
+ menuBtn.setAttribute("data-drawer", "open nav-drawer");
158
+
159
+ var markIcon = document.createElement("i");
160
+ markIcon.className = "fa-kit fa-athenacare-mark";
161
+ markIcon.setAttribute("aria-hidden", "true");
162
+ menuBtn.appendChild(markIcon);
163
+ headerSlot.appendChild(menuBtn);
164
+
165
+ var spacer = el("span", "shell-header-spacer");
166
+ spacer.setAttribute("aria-hidden", "true");
167
+ headerSlot.appendChild(spacer);
168
+
169
+ var drawer = document.createElement("wa-drawer");
170
+ drawer.id = "nav-drawer";
171
+ drawer.setAttribute("without-header", "");
172
+ drawer.setAttribute("aria-label", "Athena Care");
173
+ drawer.setAttribute("placement", "start");
174
+ drawer.setAttribute("light-dismiss", "");
175
+
176
+ var wordmarkOuter = el("div", "shell-drawer-wordmark");
177
+ var wordmarkLink = el(
178
+ "a",
179
+ "shell-drawer-wordmark-link shell-drawer-wordmark-brand",
180
+ );
181
+ wordmarkLink.href = "#";
182
+ wordmarkLink.setAttribute("aria-label", "Athena Care home");
183
+ wordmarkLink.addEventListener("click", function (e) {
184
+ e.preventDefault();
185
+ });
186
+ var wmMark = document.createElement("i");
187
+ wmMark.className = "fa-kit fa-athenacare-mark";
188
+ wmMark.setAttribute("aria-hidden", "true");
189
+ var wmText = el("span", "shell-drawer-wordmark-text");
190
+ wmText.textContent = "Athena Care";
191
+ wordmarkLink.appendChild(wmMark);
192
+ wordmarkLink.appendChild(wmText);
193
+ wordmarkOuter.appendChild(wordmarkLink);
194
+ drawer.appendChild(wordmarkOuter);
195
+
196
+ var nav = el("nav", "wa-stack wa-gap-m");
197
+ nav.setAttribute("aria-label", "Sidebar navigation");
198
+
199
+ var hApps = el("div", "shell-nav-heading");
200
+ hApps.textContent = "Applications";
201
+ nav.appendChild(hApps);
202
+ var appsEl = el("div", "wa-stack wa-gap-s shell-nav-applications");
203
+ fillNavGroup(appsEl, sections.applications, activeHref);
204
+ nav.appendChild(appsEl);
205
+
206
+ var dashRegion = el("div", "shell-nav-admin-region");
207
+ dashRegion.setAttribute("data-shell-dashboards-region", "");
208
+ if (sections.dashboards.length === 0) dashRegion.hidden = true;
209
+ var hDash = el("div", "shell-nav-heading shell-nav-heading--group");
210
+ hDash.textContent = "DASHBOARDS";
211
+ var dashEl = el("div", "wa-stack wa-gap-s shell-nav-dashboards");
212
+ fillNavGroup(dashEl, sections.dashboards, activeHref);
213
+ dashRegion.appendChild(hDash);
214
+ dashRegion.appendChild(dashEl);
215
+ nav.appendChild(dashRegion);
216
+
217
+ var adminRegion = el("div", "shell-nav-admin-region");
218
+ adminRegion.setAttribute("data-shell-admin-region", "");
219
+ if (sections.administration.length === 0) adminRegion.hidden = true;
220
+ var hAdmin = el("div", "shell-nav-heading shell-nav-heading--group");
221
+ hAdmin.textContent = "ADMINISTRATION";
222
+ var adminEl = el("div", "wa-stack wa-gap-s shell-nav-administration");
223
+ fillNavGroup(adminEl, sections.administration, activeHref);
224
+ adminRegion.appendChild(hAdmin);
225
+ adminRegion.appendChild(adminEl);
226
+ nav.appendChild(adminRegion);
227
+
228
+ var hAcct = el("div", "shell-nav-heading shell-nav-heading--group");
229
+ hAcct.textContent = "Preview";
230
+ nav.appendChild(hAcct);
231
+
232
+ var roleField = el("div", "athena-preview-role-field");
233
+ var roleLabel = document.createElement("label");
234
+ roleLabel.setAttribute("for", "athena-preview-role");
235
+ roleLabel.textContent = "Preview as";
236
+ roleField.appendChild(roleLabel);
237
+
238
+ var roleSelect = document.createElement("select");
239
+ roleSelect.id = "athena-preview-role";
240
+ roleSelect.setAttribute("aria-label", "Preview as role");
241
+ ROLES.forEach(function (r) {
242
+ var opt = document.createElement("option");
243
+ opt.value = r;
244
+ opt.textContent = r.charAt(0).toUpperCase() + r.slice(1);
245
+ if (r === role) opt.selected = true;
246
+ roleSelect.appendChild(opt);
247
+ });
248
+ roleField.appendChild(roleSelect);
249
+ nav.appendChild(roleField);
250
+
251
+ drawer.appendChild(nav);
252
+
253
+ var main = el("main", "shell-main");
254
+ var appWrap = el("div", "athena-preview-embed-app");
255
+ main.appendChild(appWrap);
256
+
257
+ roleSelect.addEventListener("change", function () {
258
+ setPreviewViewer(roleSelect.value);
259
+ });
260
+
261
+ waPage.appendChild(headerSlot);
262
+ waPage.appendChild(drawer);
263
+ waPage.appendChild(main);
264
+
265
+ return { shell: waPage, appSlot: appWrap };
266
+ }
267
+
268
+ function showError(message) {
269
+ var p = el("p", "shell-subtle");
270
+ p.style.padding = "1rem";
271
+ p.textContent = message;
272
+ document.body.appendChild(p);
273
+ }
274
+
275
+ function mount() {
276
+ var config = globalThis.__ATHENA_APP_CONFIG__;
277
+ if (!config || typeof config !== "object") {
278
+ showError("Missing preview config.");
279
+ return;
280
+ }
281
+
282
+ var appMain = document.querySelector("main.portal-app");
283
+ if (!appMain) {
284
+ showError("Missing <main class=\"portal-app\"> for preview.");
285
+ return;
286
+ }
287
+
288
+ var kit = (config.kitOrigin || DEFAULT_KIT).replace(/\/$/, "");
289
+ activeKit = kit;
290
+ globalThis.__ATHENA_KIT_ORIGIN__ = kit;
291
+
292
+ injectStylesheet(kit + "/assets/athena-app.css");
293
+ injectStylesheet(kit + "/assets/shell-base.css");
294
+ injectStylesheet(kit + "/assets/preview-shell.css");
295
+
296
+ document.documentElement.classList.add("embedded");
297
+ document.title = (config.label || "App") + " — preview";
298
+
299
+ var role = defaultRole(config);
300
+ setPreviewViewer(role);
301
+
302
+ var sections = syntheticNav(config);
303
+ var built = buildEmbedShell(config, role, sections);
304
+ built.appSlot.appendChild(appMain);
305
+ document.body.replaceChildren(built.shell);
306
+ }
307
+
308
+ if (document.readyState === "loading") {
309
+ document.addEventListener("DOMContentLoaded", mount);
310
+ } else {
311
+ mount();
312
+ }
313
+ })();
@@ -19,6 +19,19 @@ wa-page.shell.athena-preview-shell > main.shell-main > .athena-preview-app {
19
19
  min-height: 24rem;
20
20
  }
21
21
 
22
+ .athena-preview-embed-app {
23
+ display: block;
24
+ min-height: 0;
25
+ overflow: auto;
26
+ height: calc(100dvh - var(--athena-preview-chrome-offset, 5rem));
27
+ min-height: 24rem;
28
+ }
29
+
30
+ wa-page.shell.athena-preview-shell > main.shell-main > .athena-preview-embed-app {
31
+ display: block;
32
+ min-height: 0;
33
+ }
34
+
22
35
  .athena-preview-role-field {
23
36
  padding: 0 0.25rem;
24
37
  }
@@ -5,7 +5,7 @@
5
5
  (function () {
6
6
  "use strict";
7
7
 
8
- var DEFAULT_KIT = "https://cdn.jsdelivr.net/npm/athena-portal-app-kit@1.0.0";
8
+ var DEFAULT_KIT = "https://cdn.jsdelivr.net/npm/athena-portal-app-kit@1";
9
9
  var DEFAULT_PORTAL = "https://backoffice.athenacare.health";
10
10
  var ROLES = [
11
11
  "staff",
@@ -83,6 +83,31 @@
83
83
  };
84
84
  }
85
85
 
86
+ function syntheticNav(config) {
87
+ var href = config.appKey ? "/apps/" + config.appKey : "#";
88
+ var category = config.navCategory || "applications";
89
+ if (
90
+ category !== "applications" &&
91
+ category !== "dashboards" &&
92
+ category !== "administration"
93
+ ) {
94
+ category = "applications";
95
+ }
96
+ var entry = {
97
+ key: config.appKey || "preview-app",
98
+ href: href,
99
+ label: config.label || config.appKey || "Your app",
100
+ icon: config.icon || "link",
101
+ };
102
+ var sections = {
103
+ applications: [],
104
+ dashboards: [],
105
+ administration: [],
106
+ };
107
+ sections[category] = [entry];
108
+ return sections;
109
+ }
110
+
86
111
  function ensureAppInNav(sections, config) {
87
112
  if (!config.appKey) return sections;
88
113
  var href = "/apps/" + config.appKey;
@@ -171,7 +196,7 @@
171
196
  menuBtn.setAttribute("type", "button");
172
197
  menuBtn.setAttribute("appearance", "outlined");
173
198
  menuBtn.setAttribute("variant", "neutral");
174
- menuBtn.setAttribute("size", "small");
199
+ menuBtn.setAttribute("size", "s");
175
200
  menuBtn.setAttribute("aria-label", "Open sidebar");
176
201
  menuBtn.setAttribute("data-drawer", "open nav-drawer");
177
202
 
@@ -323,12 +348,18 @@
323
348
  document.title = (config.label || "App") + " — preview";
324
349
 
325
350
  var role = defaultRole(config);
326
- return fetchNav(portal, role).then(function (payload) {
327
- var sections = ensureAppInNav(parseNavSections(payload), config);
328
- document.body.replaceChildren(
329
- buildPreviewShell(config, role, sections),
330
- );
331
- });
351
+ return fetchNav(portal, role)
352
+ .then(function (payload) {
353
+ return ensureAppInNav(parseNavSections(payload), config);
354
+ })
355
+ .catch(function () {
356
+ return ensureAppInNav(syntheticNav(config), config);
357
+ })
358
+ .then(function (sections) {
359
+ document.body.replaceChildren(
360
+ buildPreviewShell(config, role, sections),
361
+ );
362
+ });
332
363
  })
333
364
  .catch(function (err) {
334
365
  console.error("[preview-shell]", err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "athena-portal-app-kit",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Athena Care static app kit — CDN assets for back-office HTML apps (Claude sandbox compatible via jsDelivr)",
5
5
  "license": "UNLICENSED",
6
6
  "files": [