@syntrologie/adapt-nav 2.1.0-canary.8 → 2.1.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.
@@ -1,23 +1,23 @@
1
1
  /**
2
2
  * Adaptive Nav - NavWidget Component
3
3
  *
4
- * React component that renders a navigation link list with per-item
5
- * conditional visibility based on showWhen decision strategies.
4
+ * React component that renders a collapsible navigation tips accordion
5
+ * with per-item conditional visibility based on showWhen decision strategies.
6
6
  *
7
7
  * Demonstrates the compositional action pattern where child actions
8
- * (nav:link) serve as configuration data for the parent widget.
8
+ * (nav:tip) serve as configuration data for the parent widget.
9
9
  */
10
10
  import type { NavWidgetProps, NavConfig, NavWidgetRuntime } from './types';
11
11
  /**
12
- * NavWidget - Renders a navigation link list with per-item activation.
12
+ * NavWidget - Renders a collapsible navigation tips accordion.
13
13
  *
14
14
  * This component demonstrates the compositional action pattern:
15
15
  * - Parent (NavWidget) receives `config.actions` array
16
16
  * - Each action has optional `showWhen` for per-item visibility
17
- * - Parent evaluates showWhen and filters visible links
18
- * - Parent manages re-rendering on context changes
17
+ * - Parent evaluates showWhen and filters visible tips
18
+ * - Parent manages expand state and re-rendering on context changes
19
19
  */
20
- export declare function NavWidget({ config, runtime, instanceId }: NavWidgetProps): import("react/jsx-runtime").JSX.Element | null;
20
+ export declare function NavWidget({ config, runtime, instanceId }: NavWidgetProps): import("react/jsx-runtime").JSX.Element;
21
21
  /**
22
22
  * Mountable widget interface for the runtime's WidgetRegistry.
23
23
  */
@@ -1 +1 @@
1
- {"version":3,"file":"NavWidget.d.ts","sourceRoot":"","sources":["../src/NavWidget.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAiB,SAAS,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AA8G1F;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,EAAE,cAAc,kDAkFxE;AAMD;;GAEG;AACH,eAAO,MAAM,kBAAkB;qBAEhB,WAAW,WACb,SAAS,GAAG;QAAE,OAAO,CAAC,EAAE,gBAAgB,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE;CAsC3E,CAAC;AAEF,eAAe,SAAS,CAAC"}
1
+ {"version":3,"file":"NavWidget.d.ts","sourceRoot":"","sources":["../src/NavWidget.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH,OAAO,KAAK,EAAE,cAAc,EAAgB,SAAS,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AA2PzF;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,EAAE,cAAc,2CAuOxE;AAMD;;GAEG;AACH,eAAO,MAAM,kBAAkB;qBAEhB,WAAW,WACb,SAAS,GAAG;QAAE,OAAO,CAAC,EAAE,gBAAgB,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE;CAiD3E,CAAC;AAEF,eAAe,SAAS,CAAC"}
package/dist/NavWidget.js CHANGED
@@ -2,101 +2,225 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /**
3
3
  * Adaptive Nav - NavWidget Component
4
4
  *
5
- * React component that renders a navigation link list with per-item
6
- * conditional visibility based on showWhen decision strategies.
5
+ * React component that renders a collapsible navigation tips accordion
6
+ * with per-item conditional visibility based on showWhen decision strategies.
7
7
  *
8
8
  * Demonstrates the compositional action pattern where child actions
9
- * (nav:link) serve as configuration data for the parent widget.
9
+ * (nav:tip) serve as configuration data for the parent widget.
10
10
  */
11
- import React, { useEffect, useReducer, useMemo, useCallback } from 'react';
11
+ import { base, slateGrey, purple } from '@syntro/design-system/tokens';
12
+ import React, { useEffect, useReducer, useMemo, useCallback, useState } from 'react';
13
+ import { createRoot } from 'react-dom/client';
14
+ // ============================================================================
15
+ // Sanitization
16
+ // ============================================================================
17
+ function escapeHtml(str) {
18
+ return str
19
+ .replace(/&/g, '&')
20
+ .replace(/</g, '&lt;')
21
+ .replace(/>/g, '&gt;')
22
+ .replace(/"/g, '&quot;')
23
+ .replace(/'/g, '&#39;');
24
+ }
12
25
  // ============================================================================
13
26
  // Styles
14
27
  // ============================================================================
15
28
  const baseStyles = {
16
- nav: {
29
+ container: {
30
+ fontFamily: 'system-ui, -apple-system, sans-serif',
31
+ padding: '8px',
32
+ maxWidth: '100%',
33
+ overflow: 'hidden',
34
+ },
35
+ accordion: {
17
36
  display: 'flex',
37
+ flexDirection: 'column',
18
38
  gap: '4px',
19
- padding: '8px',
20
- fontFamily: 'system-ui, -apple-system, sans-serif',
21
39
  },
22
- link: {
40
+ item: {
41
+ borderRadius: '8px',
42
+ overflow: 'hidden',
43
+ transition: 'box-shadow 0.2s ease',
44
+ },
45
+ header: {
23
46
  display: 'flex',
24
47
  alignItems: 'center',
25
- gap: '6px',
26
- padding: '8px 12px',
48
+ gap: '8px',
49
+ width: '100%',
50
+ padding: '12px 16px',
51
+ border: 'none',
52
+ cursor: 'pointer',
53
+ fontSize: '14px',
54
+ fontWeight: 500,
55
+ fontFamily: 'inherit',
56
+ textAlign: 'left',
57
+ transition: 'background-color 0.15s ease',
58
+ },
59
+ chevron: {
60
+ fontSize: '10px',
61
+ transition: 'transform 0.2s ease',
62
+ marginLeft: 'auto',
63
+ flexShrink: 0,
64
+ },
65
+ icon: {
66
+ fontSize: '16px',
67
+ flexShrink: 0,
68
+ },
69
+ body: {
70
+ overflow: 'hidden',
71
+ transition: 'max-height 0.25s ease, padding-bottom 0.25s ease',
72
+ padding: '0 16px',
73
+ },
74
+ description: {
75
+ fontSize: '13px',
76
+ lineHeight: '1.5',
77
+ margin: 0,
78
+ },
79
+ linkButton: {
80
+ display: 'inline-flex',
81
+ alignItems: 'center',
82
+ gap: '4px',
83
+ marginTop: '10px',
84
+ padding: '6px 12px',
27
85
  borderRadius: '6px',
28
86
  textDecoration: 'none',
29
- fontSize: '14px',
87
+ fontSize: '13px',
30
88
  fontWeight: 500,
31
- transition: 'background-color 0.15s ease, color 0.15s ease',
32
89
  cursor: 'pointer',
33
90
  border: 'none',
34
- background: 'transparent',
91
+ transition: 'background-color 0.15s ease',
35
92
  },
36
- icon: {
37
- fontSize: '16px',
93
+ categoryHeader: {
94
+ fontSize: '11px',
95
+ fontWeight: 600,
96
+ textTransform: 'uppercase',
97
+ letterSpacing: '0.05em',
98
+ padding: '12px 4px 4px',
38
99
  },
39
- externalIcon: {
40
- fontSize: '12px',
41
- opacity: 0.6,
100
+ emptyState: {
101
+ fontSize: '13px',
102
+ padding: '16px',
103
+ textAlign: 'center',
42
104
  },
43
105
  };
44
106
  const themeStyles = {
45
107
  light: {
46
- nav: {
47
- backgroundColor: '#ffffff',
108
+ container: {
109
+ backgroundColor: base.white,
110
+ color: slateGrey[1],
111
+ },
112
+ item: {
113
+ backgroundColor: slateGrey[12],
114
+ border: `1px solid ${slateGrey[11]}`,
115
+ },
116
+ itemExpanded: {
117
+ boxShadow: '0 4px 12px rgba(0, 0, 0, 0.08)',
48
118
  },
49
- link: {
50
- color: '#374151',
119
+ header: {
120
+ backgroundColor: 'transparent',
121
+ color: slateGrey[1],
51
122
  },
52
- linkHover: {
53
- backgroundColor: '#f3f4f6',
54
- color: '#111827',
123
+ headerHover: {
124
+ backgroundColor: slateGrey[12],
125
+ },
126
+ body: {
127
+ color: slateGrey[6],
128
+ },
129
+ linkButton: {
130
+ backgroundColor: purple[8],
131
+ color: purple[2],
132
+ },
133
+ categoryHeader: {
134
+ color: slateGrey[7],
135
+ },
136
+ emptyState: {
137
+ color: slateGrey[8],
55
138
  },
56
139
  },
57
140
  dark: {
58
- nav: {
59
- backgroundColor: '#1f2937',
141
+ container: {
142
+ backgroundColor: slateGrey[1],
143
+ color: slateGrey[12],
144
+ },
145
+ item: {
146
+ backgroundColor: slateGrey[3],
147
+ border: `1px solid ${slateGrey[5]}`,
148
+ },
149
+ itemExpanded: {
150
+ boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)',
151
+ },
152
+ header: {
153
+ backgroundColor: 'transparent',
154
+ color: slateGrey[12],
60
155
  },
61
- link: {
62
- color: '#d1d5db',
156
+ headerHover: {
157
+ backgroundColor: slateGrey[5],
63
158
  },
64
- linkHover: {
65
- backgroundColor: '#374151',
66
- color: '#f9fafb',
159
+ body: {
160
+ color: slateGrey[8],
161
+ },
162
+ linkButton: {
163
+ backgroundColor: purple[0],
164
+ color: purple[6],
165
+ },
166
+ categoryHeader: {
167
+ color: slateGrey[8],
168
+ },
169
+ emptyState: {
170
+ color: slateGrey[7],
67
171
  },
68
172
  },
69
173
  };
70
- function NavLinkComponent({ link, theme, onNavigate }) {
71
- const [isHovered, setIsHovered] = React.useState(false);
72
- const { label, href, icon, external } = link.config;
174
+ function NavTipItem({ item, isExpanded, onToggle, onNavigate, theme }) {
175
+ const [isHovered, setIsHovered] = useState(false);
73
176
  const colors = themeStyles[theme];
74
- const style = {
75
- ...baseStyles.link,
76
- ...colors.link,
77
- ...(isHovered ? colors.linkHover : {}),
177
+ const { title, description, href, icon, external } = item.config;
178
+ const itemStyle = {
179
+ ...baseStyles.item,
180
+ ...colors.item,
181
+ ...(isExpanded ? colors.itemExpanded : {}),
182
+ };
183
+ const headerStyle = {
184
+ ...baseStyles.header,
185
+ ...colors.header,
186
+ ...(isHovered ? colors.headerHover : {}),
78
187
  };
79
- const handleClick = (e) => {
188
+ const chevronStyle = {
189
+ ...baseStyles.chevron,
190
+ transform: isExpanded ? 'rotate(180deg)' : 'rotate(0deg)',
191
+ };
192
+ const bodyStyle = {
193
+ ...baseStyles.body,
194
+ ...colors.body,
195
+ maxHeight: isExpanded ? '500px' : '0',
196
+ paddingBottom: isExpanded ? '16px' : '0',
197
+ };
198
+ const handleLinkClick = (e) => {
80
199
  e.preventDefault();
81
- onNavigate(href, external ?? false);
200
+ e.stopPropagation();
201
+ if (href) {
202
+ onNavigate(href, external ?? false);
203
+ }
82
204
  };
83
- return (_jsxs("a", { href: href, onClick: handleClick, style: style, onMouseEnter: () => setIsHovered(true), onMouseLeave: () => setIsHovered(false), target: external ? '_blank' : undefined, rel: external ? 'noopener noreferrer' : undefined, children: [icon && _jsx("span", { style: baseStyles.icon, children: icon }), _jsx("span", { children: label }), external && _jsx("span", { style: baseStyles.externalIcon, children: "\u2197" })] }));
205
+ return (_jsxs("div", { style: itemStyle, "data-nav-tip-id": item.config.id, children: [_jsxs("button", { style: headerStyle, onClick: onToggle, onMouseEnter: () => setIsHovered(true), onMouseLeave: () => setIsHovered(false), "aria-expanded": isExpanded, children: [icon && _jsx("span", { style: baseStyles.icon, children: icon }), _jsx("span", { children: title }), _jsx("span", { style: chevronStyle, children: '\u25BC' })] }), _jsxs("div", { style: bodyStyle, "aria-hidden": !isExpanded, children: [_jsx("p", { style: baseStyles.description, children: description }), href && (_jsxs("a", { href: href, onClick: handleLinkClick, style: { ...baseStyles.linkButton, ...colors.linkButton }, target: external ? '_blank' : undefined, rel: external ? 'noopener noreferrer' : undefined, children: ["Go ", external ? '\u2197' : '\u2192'] }))] })] }));
84
206
  }
85
207
  // ============================================================================
86
208
  // NavWidget Component
87
209
  // ============================================================================
88
210
  /**
89
- * NavWidget - Renders a navigation link list with per-item activation.
211
+ * NavWidget - Renders a collapsible navigation tips accordion.
90
212
  *
91
213
  * This component demonstrates the compositional action pattern:
92
214
  * - Parent (NavWidget) receives `config.actions` array
93
215
  * - Each action has optional `showWhen` for per-item visibility
94
- * - Parent evaluates showWhen and filters visible links
95
- * - Parent manages re-rendering on context changes
216
+ * - Parent evaluates showWhen and filters visible tips
217
+ * - Parent manages expand state and re-rendering on context changes
96
218
  */
97
219
  export function NavWidget({ config, runtime, instanceId }) {
98
- // Force re-render when context changes
99
- const [, forceUpdate] = useReducer((x) => x + 1, 0);
220
+ // Force re-render when context/accumulator changes.
221
+ const [renderTick, forceUpdate] = useReducer((x) => x + 1, 0);
222
+ // Track expanded tip IDs
223
+ const [expandedIds, setExpandedIds] = useState(new Set());
100
224
  // Subscribe to context changes for reactive updates
101
225
  useEffect(() => {
102
226
  const unsubscribe = runtime.context.subscribe(() => {
@@ -104,35 +228,141 @@ export function NavWidget({ config, runtime, instanceId }) {
104
228
  });
105
229
  return unsubscribe;
106
230
  }, [runtime.context]);
107
- // Filter visible links based on per-item showWhen
108
- const visibleLinks = useMemo(() => config.actions.filter((link) => {
109
- // No showWhen = always visible
110
- if (!link.showWhen)
231
+ // Subscribe to accumulator changes for event_count-based showWhen
232
+ useEffect(() => {
233
+ if (!runtime.accumulator?.subscribe)
234
+ return;
235
+ return runtime.accumulator.subscribe(() => {
236
+ forceUpdate();
237
+ });
238
+ }, [runtime.accumulator]);
239
+ // Register accumulator predicates from scope config
240
+ useEffect(() => {
241
+ if (!config.scope || !runtime.accumulator?.register)
242
+ return;
243
+ const { events: eventNames, urlContains, props: propFilters } = config.scope;
244
+ // Scan showWhen conditions for event_count keys
245
+ const keys = new Set();
246
+ for (const action of config.actions) {
247
+ if (action.showWhen?.type === 'rules') {
248
+ for (const rule of action.showWhen.rules) {
249
+ for (const cond of rule.conditions) {
250
+ if (cond.type === 'event_count' && cond.key) {
251
+ keys.add(cond.key);
252
+ }
253
+ }
254
+ }
255
+ }
256
+ }
257
+ for (const key of keys) {
258
+ runtime.accumulator.register(key, (event) => {
259
+ if (!eventNames.includes(event.name))
260
+ return false;
261
+ if (urlContains) {
262
+ const pathname = String(event.props?.pathname ?? '');
263
+ if (!pathname.includes(urlContains))
264
+ return false;
265
+ }
266
+ if (propFilters) {
267
+ for (const [k, v] of Object.entries(propFilters)) {
268
+ if (event.props?.[k] !== v)
269
+ return false;
270
+ }
271
+ }
272
+ return true;
273
+ });
274
+ }
275
+ }, [config.scope, config.actions, runtime.accumulator]);
276
+ // Filter visible tips based on per-item showWhen
277
+ const visibleTips = useMemo(() => config.actions.filter((tip) => {
278
+ if (!tip.showWhen)
111
279
  return true;
112
- // Evaluate the decision strategy
113
- const result = runtime.evaluateSync(link.showWhen);
114
- return result.value;
115
- }), [config.actions, runtime]);
280
+ try {
281
+ const result = runtime.evaluateSync(tip.showWhen);
282
+ return result.value;
283
+ }
284
+ catch {
285
+ // If strategy evaluation fails, hide the tip (fail-closed)
286
+ return false;
287
+ }
288
+ }),
289
+ // eslint-disable-next-line react-hooks/exhaustive-deps
290
+ [config.actions, runtime, renderTick]);
291
+ // Group by category
292
+ const categoryGroups = useMemo(() => {
293
+ const groups = new Map();
294
+ for (const tip of visibleTips) {
295
+ const cat = tip.config.category;
296
+ if (!groups.has(cat)) {
297
+ groups.set(cat, []);
298
+ }
299
+ groups.get(cat).push(tip);
300
+ }
301
+ return groups;
302
+ }, [visibleTips]);
303
+ // Check if any items have categories
304
+ const hasCategories = useMemo(() => visibleTips.some((t) => t.config.category), [visibleTips]);
116
305
  // Resolve theme (auto → detect system preference)
117
306
  const resolvedTheme = useMemo(() => {
118
307
  if (config.theme !== 'auto')
119
308
  return config.theme;
120
- // Check system preference (SSR-safe)
121
309
  if (typeof window !== 'undefined') {
122
310
  return window.matchMedia?.('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
123
311
  }
124
312
  return 'light';
125
313
  }, [config.theme]);
314
+ // Handle tip toggle
315
+ const handleToggle = useCallback((id) => {
316
+ setExpandedIds((prev) => {
317
+ const wasExpanded = prev.has(id);
318
+ let next;
319
+ if (config.expandBehavior === 'single') {
320
+ // In single mode, emit collapse events for any previously-expanded tips
321
+ for (const prevId of prev) {
322
+ if (prevId !== id) {
323
+ runtime.events.publish('nav:toggled', {
324
+ instanceId,
325
+ tipId: prevId,
326
+ expanded: false,
327
+ timestamp: Date.now(),
328
+ });
329
+ }
330
+ }
331
+ next = wasExpanded ? new Set() : new Set([id]);
332
+ }
333
+ else {
334
+ next = new Set(prev);
335
+ if (wasExpanded) {
336
+ next.delete(id);
337
+ }
338
+ else {
339
+ next.add(id);
340
+ }
341
+ }
342
+ // Emit event for the clicked tip using fresh state from the updater
343
+ runtime.events.publish('nav:toggled', {
344
+ instanceId,
345
+ tipId: id,
346
+ expanded: !wasExpanded,
347
+ timestamp: Date.now(),
348
+ });
349
+ return next;
350
+ });
351
+ }, [config.expandBehavior, runtime.events, instanceId]);
126
352
  // Handle navigation with event publishing
127
353
  const handleNavigate = useCallback((href, external) => {
128
- // Publish navigation event for analytics
129
- runtime.events.publish('nav:click', {
354
+ // Reject dangerous URIs to prevent XSS
355
+ const normalizedHref = href.trim().toLowerCase();
356
+ if (normalizedHref.startsWith('javascript:') ||
357
+ normalizedHref.startsWith('data:')) {
358
+ return;
359
+ }
360
+ runtime.events.publish('nav:tip_clicked', {
130
361
  instanceId,
131
362
  href,
132
363
  external,
133
364
  timestamp: Date.now(),
134
365
  });
135
- // Perform navigation
136
366
  if (external) {
137
367
  window.open(href, '_blank', 'noopener,noreferrer');
138
368
  }
@@ -140,17 +370,28 @@ export function NavWidget({ config, runtime, instanceId }) {
140
370
  window.location.href = href;
141
371
  }
142
372
  }, [runtime.events, instanceId]);
143
- // Compute nav styles
144
- const navStyle = {
145
- ...baseStyles.nav,
146
- ...themeStyles[resolvedTheme].nav,
147
- flexDirection: config.layout === 'vertical' ? 'column' : 'row',
373
+ // Compute container styles
374
+ const containerStyle = {
375
+ ...baseStyles.container,
376
+ ...themeStyles[resolvedTheme].container,
377
+ };
378
+ const categoryHeaderStyle = {
379
+ ...baseStyles.categoryHeader,
380
+ ...themeStyles[resolvedTheme].categoryHeader,
381
+ };
382
+ const emptyStateStyle = {
383
+ ...baseStyles.emptyState,
384
+ ...themeStyles[resolvedTheme].emptyState,
148
385
  };
386
+ // Render a list of nav tip items
387
+ const renderItems = (items) => items.map((tip) => (_jsx(NavTipItem, { item: tip, isExpanded: expandedIds.has(tip.config.id), onToggle: () => handleToggle(tip.config.id), onNavigate: handleNavigate, theme: resolvedTheme }, tip.config.id)));
149
388
  // Empty state
150
- if (visibleLinks.length === 0) {
151
- return null;
389
+ if (visibleTips.length === 0) {
390
+ return (_jsx("div", { style: containerStyle, "data-adaptive-id": instanceId, "data-adaptive-type": "adaptive-nav", children: _jsx("div", { style: emptyStateStyle, children: "No navigation tips available." }) }));
152
391
  }
153
- return (_jsx("nav", { style: navStyle, "data-adaptive-id": instanceId, "data-adaptive-type": "adaptive-nav", children: visibleLinks.map((link, index) => (_jsx(NavLinkComponent, { link: link, theme: resolvedTheme, onNavigate: handleNavigate }, link.config.href + index))) }));
392
+ return (_jsx("div", { style: containerStyle, "data-adaptive-id": instanceId, "data-adaptive-type": "adaptive-nav", children: _jsx("div", { style: baseStyles.accordion, children: hasCategories
393
+ ? Array.from(categoryGroups.entries()).map(([category, items]) => (_jsxs(React.Fragment, { children: [category && (_jsx("div", { style: categoryHeaderStyle, "data-category-header": category, children: category })), renderItems(items)] }, category ?? '__ungrouped')))
394
+ : renderItems(visibleTips) }) }));
154
395
  }
155
396
  // ============================================================================
156
397
  // Mountable Widget Interface
@@ -160,29 +401,38 @@ export function NavWidget({ config, runtime, instanceId }) {
160
401
  */
161
402
  export const NavMountableWidget = {
162
403
  mount(container, config) {
163
- // This is a simplified mount for non-React environments
164
- // In practice, the runtime handles React rendering
165
- const { runtime, instanceId: _instanceId = 'nav-widget', ...navConfig } = config || {
166
- layout: 'horizontal',
404
+ const { runtime, instanceId = 'nav-widget', ...navConfig } = config || {
405
+ expandBehavior: 'single',
167
406
  theme: 'auto',
168
407
  actions: [],
169
408
  };
170
- // Create simple HTML fallback if no runtime
171
- if (!runtime) {
172
- const links = navConfig.actions || [];
173
- container.innerHTML = `
174
- <nav style="display: flex; gap: 8px; padding: 8px; font-family: system-ui;">
175
- ${links
176
- .map((link) => `
177
- <a href="${link.config.href}" style="padding: 8px 12px; text-decoration: none; color: #374151;">
178
- ${link.config.icon ? `<span>${link.config.icon}</span>` : ''}
179
- ${link.config.label}
180
- </a>
181
- `)
182
- .join('')}
183
- </nav>
184
- `;
409
+ // React rendering when runtime + ReactDOM are available
410
+ if (runtime && typeof createRoot === 'function') {
411
+ const root = createRoot(container);
412
+ root.render(React.createElement(NavWidget, {
413
+ config: navConfig,
414
+ runtime: runtime,
415
+ instanceId,
416
+ }));
417
+ return () => {
418
+ root.unmount();
419
+ };
185
420
  }
421
+ // HTML fallback for non-React environments
422
+ const tips = navConfig.actions || [];
423
+ container.innerHTML = `
424
+ <div style="font-family: system-ui; max-width: 100%;">
425
+ ${tips
426
+ .map((tip) => `
427
+ <div style="margin-bottom: 4px; padding: 12px 16px; background: ${slateGrey[12]}; border-radius: 8px;">
428
+ ${tip.config.icon ? `<span>${escapeHtml(tip.config.icon)}</span> ` : ''}<strong>${escapeHtml(tip.config.title)}</strong>
429
+ <p style="margin-top: 8px; color: ${slateGrey[6]}; font-size: 13px;">${escapeHtml(tip.config.description)}</p>
430
+ ${tip.config.href ? `<a href="${escapeHtml(tip.config.href)}" style="color: ${purple[2]}; font-size: 13px;">Go &rarr;</a>` : ''}
431
+ </div>
432
+ `)
433
+ .join('')}
434
+ </div>
435
+ `;
186
436
  return () => {
187
437
  container.innerHTML = '';
188
438
  };
package/dist/cdn.d.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  * This module is bundled for CDN delivery and self-registers with the global
5
5
  * SynOS app registry when loaded dynamically via the AppLoader.
6
6
  */
7
+ import NavEditor from './editor';
7
8
  /**
8
9
  * App manifest for registry registration.
9
10
  * Follows the AppManifest interface expected by AppLoader/AppRegistry.
@@ -14,7 +15,10 @@ export declare const manifest: {
14
15
  name: string;
15
16
  description: string;
16
17
  runtime: {
17
- actions: never[];
18
+ actions: {
19
+ kind: "navigation:scrollTo" | "navigation:navigate";
20
+ executor: import("./types").ActionExecutor<import("./types").ScrollToAction> | import("./types").ActionExecutor<import("./types").NavigateAction>;
21
+ }[];
18
22
  widgets: {
19
23
  id: string;
20
24
  component: {
@@ -29,6 +33,31 @@ export declare const manifest: {
29
33
  icon: string;
30
34
  };
31
35
  }[];
36
+ /**
37
+ * Extract notify watcher entries from tile config props.
38
+ * The runtime evaluates these continuously (even with drawer closed)
39
+ * and publishes nav:tip_revealed when showWhen transitions false → true.
40
+ */
41
+ notifyWatchers(props: Record<string, unknown>): {
42
+ id: string;
43
+ strategy: import("./types").DecisionStrategy<boolean>;
44
+ eventName: string;
45
+ eventProps: {
46
+ tipId: string;
47
+ title: string | undefined;
48
+ body: string | undefined;
49
+ icon: string | undefined;
50
+ };
51
+ }[];
52
+ };
53
+ editor: {
54
+ component: typeof NavEditor;
55
+ panel: {
56
+ title: string;
57
+ icon: string;
58
+ description: string;
59
+ };
60
+ getActionLabel(action: Record<string, unknown>): string;
32
61
  };
33
62
  metadata: {
34
63
  isBuiltIn: boolean;
package/dist/cdn.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cdn.d.ts","sourceRoot":"","sources":["../src/cdn.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH;;;GAGG;AACH,eAAO,MAAM,QAAQ;;;;;;;;;;;2BA2B4jK,CAAC;8BAA8B,CAAC;;;;;;;;;;;;;CAdhnK,CAAC;AAaF,eAAe,QAAQ,CAAC"}
1
+ {"version":3,"file":"cdn.d.ts","sourceRoot":"","sources":["../src/cdn.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,SAA0B,MAAM,UAAU,CAAC;AAIlD;;;GAGG;AACH,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;2BA0DukY,CAAC;8BAA8B,CAAC;;;;;;;;;QA/CxnY;;;;WAIG;8BACmB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;;;;;;;;;;;;;;;;;;+BAoBtB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;;;;CAQjD,CAAC;AAaF,eAAe,QAAQ,CAAC"}