@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.
- package/dist/NavWidget.d.ts +7 -7
- package/dist/NavWidget.d.ts.map +1 -1
- package/dist/NavWidget.js +336 -86
- package/dist/cdn.d.ts +30 -1
- package/dist/cdn.d.ts.map +1 -1
- package/dist/cdn.js +39 -7
- package/dist/editor.d.ts +12 -2
- package/dist/editor.d.ts.map +1 -1
- package/dist/editor.js +255 -236
- package/dist/runtime.d.ts +3 -3
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +10 -10
- package/dist/schema.d.ts +526 -75
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +42 -17
- package/dist/summarize.d.ts +14 -0
- package/dist/summarize.d.ts.map +1 -0
- package/dist/summarize.js +51 -0
- package/dist/types.d.ts +79 -17
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +13 -2
- package/package.json +16 -4
package/dist/NavWidget.d.ts
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Adaptive Nav - NavWidget Component
|
|
3
3
|
*
|
|
4
|
-
* React component that renders a navigation
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
*/
|
package/dist/NavWidget.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NavWidget.d.ts","sourceRoot":"","sources":["../src/NavWidget.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;
|
|
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
|
|
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:
|
|
9
|
+
* (nav:tip) serve as configuration data for the parent widget.
|
|
10
10
|
*/
|
|
11
|
-
import
|
|
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, '<')
|
|
21
|
+
.replace(/>/g, '>')
|
|
22
|
+
.replace(/"/g, '"')
|
|
23
|
+
.replace(/'/g, ''');
|
|
24
|
+
}
|
|
12
25
|
// ============================================================================
|
|
13
26
|
// Styles
|
|
14
27
|
// ============================================================================
|
|
15
28
|
const baseStyles = {
|
|
16
|
-
|
|
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
|
-
|
|
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: '
|
|
26
|
-
|
|
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: '
|
|
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
|
-
|
|
91
|
+
transition: 'background-color 0.15s ease',
|
|
35
92
|
},
|
|
36
|
-
|
|
37
|
-
fontSize: '
|
|
93
|
+
categoryHeader: {
|
|
94
|
+
fontSize: '11px',
|
|
95
|
+
fontWeight: 600,
|
|
96
|
+
textTransform: 'uppercase',
|
|
97
|
+
letterSpacing: '0.05em',
|
|
98
|
+
padding: '12px 4px 4px',
|
|
38
99
|
},
|
|
39
|
-
|
|
40
|
-
fontSize: '
|
|
41
|
-
|
|
100
|
+
emptyState: {
|
|
101
|
+
fontSize: '13px',
|
|
102
|
+
padding: '16px',
|
|
103
|
+
textAlign: 'center',
|
|
42
104
|
},
|
|
43
105
|
};
|
|
44
106
|
const themeStyles = {
|
|
45
107
|
light: {
|
|
46
|
-
|
|
47
|
-
backgroundColor:
|
|
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
|
-
|
|
50
|
-
|
|
119
|
+
header: {
|
|
120
|
+
backgroundColor: 'transparent',
|
|
121
|
+
color: slateGrey[1],
|
|
51
122
|
},
|
|
52
|
-
|
|
53
|
-
backgroundColor:
|
|
54
|
-
|
|
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
|
-
|
|
59
|
-
backgroundColor:
|
|
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
|
-
|
|
62
|
-
|
|
156
|
+
headerHover: {
|
|
157
|
+
backgroundColor: slateGrey[5],
|
|
63
158
|
},
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
71
|
-
const [isHovered, setIsHovered] =
|
|
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
|
|
75
|
-
|
|
76
|
-
...
|
|
77
|
-
...
|
|
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
|
|
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
|
-
|
|
200
|
+
e.stopPropagation();
|
|
201
|
+
if (href) {
|
|
202
|
+
onNavigate(href, external ?? false);
|
|
203
|
+
}
|
|
82
204
|
};
|
|
83
|
-
return (_jsxs("
|
|
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
|
|
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
|
|
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
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
//
|
|
129
|
-
|
|
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
|
|
144
|
-
const
|
|
145
|
-
...baseStyles.
|
|
146
|
-
...themeStyles[resolvedTheme].
|
|
147
|
-
|
|
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 (
|
|
151
|
-
return
|
|
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("
|
|
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
|
-
|
|
164
|
-
|
|
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
|
-
//
|
|
171
|
-
if (
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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 →</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:
|
|
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;
|
|
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"}
|