@faclon-labs/app-sdk 0.3.0 → 0.4.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.
@@ -5,6 +5,7 @@ import { configure, getConfig } from './config.js';
5
5
  import { publishSession } from './request.js';
6
6
  import { SAMPLE_NOTICE, SessionContext, useIOSense } from './session.js';
7
7
  import { SignIn } from './SignIn.js';
8
+ import { startHostLook } from './host-look.js';
8
9
  /**
9
10
  * The root of an IOSense app: the session, and the sign-in screen when there is none.
10
11
  *
@@ -18,6 +19,8 @@ export function IOSenseApp({ name, auth = 'required', sample, apiBaseUrl, signIn
18
19
  const api = getConfig().apiBaseUrl;
19
20
  // Once, before the first child renders, so the first paint already has the session.
20
21
  const [phase, setPhase] = useState(() => {
22
+ // Framed in I/O Sense, the app may wear its look (chosen in the sidebar): asked for at once.
23
+ startHostLook();
21
24
  if (auth === 'none')
22
25
  return { kind: 'ready', token: null };
23
26
  const how = adoptHostSession();
@@ -0,0 +1,29 @@
1
+ /**
2
+ * I/O Sense's look, worn over the app's own design system when the person chose it for this app in
3
+ * their sidebar. The host works out the variables for the app's own library (shadcn's `--primary`,
4
+ * MUI's `--mui-palette-primary-main`, a company library's own token names) and sends them; this puts
5
+ * them on the page over the library's, and follows each change the host sends, with no reload.
6
+ * Outside I/O Sense, or with the app's own look chosen, it does nothing.
7
+ */
8
+ export interface HostLook {
9
+ mode: 'light' | 'dark';
10
+ vars: Record<string, string>;
11
+ /** More selectors that hold the library's variables themselves (Ant Design's `.css-var-*`). */
12
+ scope?: string[];
13
+ /** The `data-theme` the library's own light/dark switch reads. */
14
+ theme?: string;
15
+ /** A Google Fonts stylesheet. */
16
+ fonts?: string;
17
+ }
18
+ /** Read and clean a look from the host; null for "its own look", undefined for nonsense. */
19
+ export declare function hostLookOf(raw: unknown): HostLook | null | undefined;
20
+ /** The stylesheet a look makes: its variables over the library's, whatever the library's selectors. */
21
+ export declare function hostLookCss(look: HostLook): string;
22
+ /** Put a look on the page, or take it off (null). */
23
+ export declare function wearHostLook(look: HostLook | null): void;
24
+ /**
25
+ * Ask the host for its look and follow what it sends. Once per page; `<IOSenseApp>` calls it before
26
+ * its first render. The question carries nothing, so it may go to any parent; only a trusted host's
27
+ * answer is worn.
28
+ */
29
+ export declare function startHostLook(): void;
@@ -0,0 +1,115 @@
1
+ import { isEmbedded, isTrustedHostOrigin } from './auth.js';
2
+ const STYLE_ID = 'iosense-host-look';
3
+ const FONTS_ID = 'iosense-host-look-fonts';
4
+ const NAME = /^--[\w-]+$/;
5
+ /** A value can say a colour, a length or a font stack; never end the rule or open another. */
6
+ const VALUE = /^[^;{}<>\\]*$/;
7
+ const SELECTOR = /^[\w\s\-[\]="*^$.:#>+~(),]+$/;
8
+ let started = false;
9
+ let worn = null;
10
+ /** What the page had before the look, put back when it is taken off. */
11
+ let before = null;
12
+ let guard = null;
13
+ /** Read and clean a look from the host; null for "its own look", undefined for nonsense. */
14
+ export function hostLookOf(raw) {
15
+ if (raw === null)
16
+ return null;
17
+ if (!raw || typeof raw !== 'object')
18
+ return undefined;
19
+ const look = raw;
20
+ if (look.mode !== 'light' && look.mode !== 'dark')
21
+ return undefined;
22
+ const vars = {};
23
+ for (const [name, value] of Object.entries(look.vars ?? {}))
24
+ if (NAME.test(name) && typeof value === 'string' && VALUE.test(value))
25
+ vars[name] = value;
26
+ const scope = Array.isArray(look.scope) ? look.scope.filter((s) => typeof s === 'string' && SELECTOR.test(s)) : [];
27
+ const theme = typeof look.theme === 'string' && /^[\w-]+$/.test(look.theme) ? look.theme : undefined;
28
+ const fonts = typeof look.fonts === 'string' && look.fonts.startsWith('https://fonts.googleapis.com/') ? look.fonts : undefined;
29
+ return { mode: look.mode, vars, ...(scope.length ? { scope } : {}), ...(theme ? { theme } : {}), ...(fonts ? { fonts } : {}) };
30
+ }
31
+ /** The stylesheet a look makes: its variables over the library's, whatever the library's selectors. */
32
+ export function hostLookCss(look) {
33
+ const body = Object.entries(look.vars)
34
+ .map(([name, value]) => ` ${name}: ${value} !important;`)
35
+ .join('\n');
36
+ return body ? `${[':root', ...(look.scope ?? [])].join(',\n')} {\n${body}\n}\n` : '';
37
+ }
38
+ function setMode(look) {
39
+ const root = document.documentElement;
40
+ root.classList.toggle('dark', look.mode === 'dark');
41
+ root.classList.toggle('light', look.mode === 'light');
42
+ root.style.colorScheme = look.mode;
43
+ if (look.theme && root.getAttribute('data-theme') !== look.theme)
44
+ root.setAttribute('data-theme', look.theme);
45
+ }
46
+ /** Put a look on the page, or take it off (null). */
47
+ export function wearHostLook(look) {
48
+ const root = document.documentElement;
49
+ if (look) {
50
+ if (!before)
51
+ before = { theme: root.getAttribute('data-theme'), dark: root.classList.contains('dark'), light: root.classList.contains('light'), scheme: root.style.colorScheme };
52
+ let style = document.getElementById(STYLE_ID);
53
+ if (!style) {
54
+ style = document.createElement('style');
55
+ style.id = STYLE_ID;
56
+ }
57
+ style.textContent = hostLookCss(look);
58
+ // Last in <head>, after the library's own sheets (a dev server adds them later; they still lose to !important).
59
+ document.head.appendChild(style);
60
+ const fonts = document.getElementById(FONTS_ID);
61
+ if (look.fonts && fonts?.href !== look.fonts) {
62
+ const link = fonts ?? document.createElement('link');
63
+ link.id = FONTS_ID;
64
+ link.rel = 'stylesheet';
65
+ link.href = look.fonts;
66
+ document.head.appendChild(link);
67
+ }
68
+ else if (!look.fonts)
69
+ fonts?.remove();
70
+ worn = look;
71
+ setMode(look);
72
+ // The app's own switch (main.tsx follows the computer's light or dark) must not undo the host's.
73
+ if (!guard && typeof MutationObserver !== 'undefined') {
74
+ guard = new MutationObserver(() => worn && setMode(worn));
75
+ guard.observe(root, { attributes: true, attributeFilter: ['class', 'data-theme'] });
76
+ }
77
+ return;
78
+ }
79
+ guard?.disconnect();
80
+ guard = null;
81
+ worn = null;
82
+ document.getElementById(STYLE_ID)?.remove();
83
+ document.getElementById(FONTS_ID)?.remove();
84
+ if (before) {
85
+ if (before.theme === null)
86
+ root.removeAttribute('data-theme');
87
+ else
88
+ root.setAttribute('data-theme', before.theme);
89
+ root.classList.toggle('dark', before.dark);
90
+ root.classList.toggle('light', before.light);
91
+ root.style.colorScheme = before.scheme;
92
+ before = null;
93
+ }
94
+ }
95
+ /**
96
+ * Ask the host for its look and follow what it sends. Once per page; `<IOSenseApp>` calls it before
97
+ * its first render. The question carries nothing, so it may go to any parent; only a trusted host's
98
+ * answer is worn.
99
+ */
100
+ export function startHostLook() {
101
+ if (started || typeof window === 'undefined' || !isEmbedded())
102
+ return;
103
+ started = true;
104
+ window.addEventListener('message', (event) => {
105
+ if (event.source !== window.parent || !isTrustedHostOrigin(event.origin))
106
+ return;
107
+ const data = event.data;
108
+ if (!data || data.source !== 'iolens-studio' || data.type !== 'look')
109
+ return;
110
+ const look = hostLookOf(data.look);
111
+ if (look !== undefined)
112
+ wearHostLook(look);
113
+ });
114
+ window.parent.postMessage({ source: 'iosense-app', type: 'look-request', v: 1 }, '*');
115
+ }
package/dist/index.d.ts CHANGED
@@ -14,3 +14,4 @@ export { fetchLatest, fetchSeries, isSampleTopic, type SeriesPoint } from './dat
14
14
  export { request, useRequest, RequestError, isIOSenseHost, type RequestOptions, type UseRequestOptions, type UseRequestResult } from './request.js';
15
15
  export { configure, getConfig, type IOSenseConfig } from './config.js';
16
16
  export { adoptHostSession, exchangeSsoToken, getToken, setToken, clearToken, isSsoToken, isEmbedded, requestHostSession, isTrustedHostOrigin } from './auth.js';
17
+ export { startHostLook, wearHostLook, type HostLook } from './host-look.js';
package/dist/index.js CHANGED
@@ -14,3 +14,4 @@ export { fetchLatest, fetchSeries, isSampleTopic } from './data.js';
14
14
  export { request, useRequest, RequestError, isIOSenseHost } from './request.js';
15
15
  export { configure, getConfig } from './config.js';
16
16
  export { adoptHostSession, exchangeSsoToken, getToken, setToken, clearToken, isSsoToken, isEmbedded, requestHostSession, isTrustedHostOrigin } from './auth.js';
17
+ export { startHostLook, wearHostLook } from './host-look.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@faclon-labs/app-sdk",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "IOSense sign-in, plant data and maps for React apps: the launchpad SSO session, UNS latest values and series, the signed-in user, requests to IOSense's APIs, and the Map Engine on UNS (./map).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/vendor/SOURCE.txt CHANGED
@@ -1 +1 @@
1
- Built from MapEngine c5b1f59 — do not edit; run npm run vendor.
1
+ Built from MapEngine c24b05d — do not edit; run npm run vendor.