@herdingbits/trailhead-cloudscape 0.0.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/README.md ADDED
@@ -0,0 +1,37 @@
1
+ # @herdingbits/trailhead-cloudscape
2
+
3
+ CloudScape design system adapter for the Trailhead micro-frontend framework.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @herdingbits/trailhead-core @herdingbits/trailhead-cloudscape @cloudscape-design/components @cloudscape-design/global-styles react react-dom
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```typescript
14
+ import React from 'react';
15
+ import { createRoot } from 'react-dom/client';
16
+ import { Trailhead } from '@herdingbits/trailhead-core';
17
+ import { CloudScapeAdapter, ShellApp } from '@herdingbits/trailhead-cloudscape';
18
+ import '@cloudscape-design/global-styles/index.css';
19
+ import '@herdingbits/trailhead-cloudscape/shell.css';
20
+
21
+ const shell = new Trailhead({
22
+ adapter: new CloudScapeAdapter(),
23
+ basePath: '/app',
24
+ apiUrl: 'https://api.example.com'
25
+ });
26
+
27
+ const root = createRoot(document.getElementById('app')!);
28
+ root.render(<ShellApp shell={shell} />);
29
+ ```
30
+
31
+ ## Documentation
32
+
33
+ See the [main Trailhead documentation](https://github.com/herdingbits/trailhead) for more information.
34
+
35
+ ## License
36
+
37
+ MIT
@@ -0,0 +1,13 @@
1
+ /**
2
+ * CloudScape Design System Adapter
3
+ * Uses CloudScape Flashbar, Modal, and Spinner components
4
+ */
5
+ import type { DesignSystemAdapter, FeedbackAdapter } from '@herdingbits/trailhead-types/adapters';
6
+ export declare class CloudScapeAdapter implements DesignSystemAdapter {
7
+ name: string;
8
+ version: string;
9
+ feedback: FeedbackAdapter;
10
+ constructor();
11
+ init(basePath: string): Promise<void>;
12
+ }
13
+ //# sourceMappingURL=adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,mBAAmB,EAAE,eAAe,EAA4C,MAAM,uCAAuC,CAAC;AAyE5I,qBAAa,iBAAkB,YAAW,mBAAmB;IAC3D,IAAI,SAAgB;IACpB,OAAO,SAAW;IAClB,QAAQ,EAAE,eAAe,CAAC;;IAMpB,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAG5C"}
@@ -0,0 +1,63 @@
1
+ class CloudScapeFeedbackAdapter {
2
+ constructor() {
3
+ this.flashMessages = [];
4
+ this.busyMessage = '';
5
+ }
6
+ setFlashChangeHandler(handler) {
7
+ this.onFlashChange = handler;
8
+ }
9
+ setBusyChangeHandler(handler) {
10
+ this.onBusyChange = handler;
11
+ }
12
+ showBusy(message) {
13
+ this.busyMessage = message;
14
+ this.onBusyChange?.(message);
15
+ }
16
+ clearBusy() {
17
+ this.busyMessage = '';
18
+ this.onBusyChange?.('');
19
+ }
20
+ showToast(message, variant, duration = 3000) {
21
+ const id = `flash-${Date.now()}`;
22
+ const flash = {
23
+ id,
24
+ type: variant,
25
+ content: message,
26
+ dismissible: true,
27
+ };
28
+ this.flashMessages.push(flash);
29
+ this.onFlashChange?.([...this.flashMessages]);
30
+ if (duration > 0) {
31
+ setTimeout(() => {
32
+ this.flashMessages = this.flashMessages.filter(f => f.id !== id);
33
+ this.onFlashChange?.([...this.flashMessages]);
34
+ }, duration);
35
+ }
36
+ }
37
+ dismissFlash(id) {
38
+ this.flashMessages = this.flashMessages.filter(f => f.id !== id);
39
+ this.onFlashChange?.([...this.flashMessages]);
40
+ }
41
+ async showDialog(config) {
42
+ // Modal dialogs handled by React component
43
+ return new Promise((resolve) => {
44
+ const event = new CustomEvent('cloudscape-dialog', {
45
+ detail: { config, resolve }
46
+ });
47
+ window.dispatchEvent(event);
48
+ });
49
+ }
50
+ async initAdapter() {
51
+ // No initialization needed
52
+ }
53
+ }
54
+ export class CloudScapeAdapter {
55
+ constructor() {
56
+ this.name = 'cloudscape';
57
+ this.version = '3.0.0';
58
+ this.feedback = new CloudScapeFeedbackAdapter();
59
+ }
60
+ async init(basePath) {
61
+ // CloudScape styles loaded via global-styles package
62
+ }
63
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @herdingbits/trailhead-cloudscape
3
+ * CloudScape design system adapter for Trailhead
4
+ */
5
+ export { CloudScapeAdapter } from './adapter.js';
6
+ export { ShellApp } from './shell-app.js';
7
+ export { ShellLayout } from './shell-layout.js';
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @herdingbits/trailhead-cloudscape
3
+ * CloudScape design system adapter for Trailhead
4
+ */
5
+ export { CloudScapeAdapter } from './adapter.js';
6
+ export { ShellApp } from './shell-app.js';
7
+ export { ShellLayout } from './shell-layout.js';
@@ -0,0 +1,7 @@
1
+ import type { Trailhead } from '@herdingbits/trailhead-core';
2
+ interface ShellAppProps {
3
+ shell: Trailhead;
4
+ }
5
+ export declare function ShellApp({ shell }: ShellAppProps): import("react/jsx-runtime").JSX.Element;
6
+ export {};
7
+ //# sourceMappingURL=shell-app.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shell-app.d.ts","sourceRoot":"","sources":["../src/shell-app.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAE7D,UAAU,aAAa;IACrB,KAAK,EAAE,SAAS,CAAC;CAClB;AAiBD,wBAAgB,QAAQ,CAAC,EAAE,KAAK,EAAE,EAAE,aAAa,2CAyMhD"}
@@ -0,0 +1,142 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useState, useEffect, useRef } from 'react';
3
+ import Flashbar from '@cloudscape-design/components/flashbar';
4
+ import Spinner from '@cloudscape-design/components/spinner';
5
+ import Modal from '@cloudscape-design/components/modal';
6
+ import Box from '@cloudscape-design/components/box';
7
+ import SpaceBetween from '@cloudscape-design/components/space-between';
8
+ import Button from '@cloudscape-design/components/button';
9
+ import { ShellLayout } from './shell-layout.js';
10
+ export function ShellApp({ shell }) {
11
+ const [navigation, setNavigation] = useState([]);
12
+ const [flashMessages, setFlashMessages] = useState([]);
13
+ const [busyMessage, setBusyMessage] = useState('');
14
+ const [dialogState, setDialogState] = useState({
15
+ visible: false,
16
+ message: '',
17
+ buttons: []
18
+ });
19
+ const contentRef = useRef(null);
20
+ // Get current path without basePath
21
+ const getCurrentPath = () => {
22
+ let path = window.location.pathname;
23
+ if (shell.basePath && path.startsWith(shell.basePath)) {
24
+ path = path.substring(shell.basePath.length) || '/';
25
+ }
26
+ return path;
27
+ };
28
+ const [currentPath, setCurrentPath] = useState(getCurrentPath());
29
+ useEffect(() => {
30
+ // Connect adapter to React state
31
+ const adapter = shell.adapter.feedback;
32
+ adapter.setFlashChangeHandler(setFlashMessages);
33
+ adapter.setBusyChangeHandler(setBusyMessage);
34
+ // Listen for dialog events
35
+ const handleDialogEvent = (event) => {
36
+ const { config, resolve } = event.detail;
37
+ setDialogState({
38
+ visible: true,
39
+ title: config.title,
40
+ message: config.message,
41
+ buttons: config.buttons,
42
+ resolve
43
+ });
44
+ };
45
+ window.addEventListener('cloudscape-dialog', handleDialogEvent);
46
+ // Load navigation
47
+ fetch(`${shell.basePath}/navigation.json`)
48
+ .then(res => res.json())
49
+ .then(data => {
50
+ setNavigation(data);
51
+ })
52
+ .catch(err => console.error('Failed to load navigation:', err));
53
+ return () => {
54
+ window.removeEventListener('cloudscape-dialog', handleDialogEvent);
55
+ };
56
+ }, []);
57
+ useEffect(() => {
58
+ // Handle route when navigation is loaded
59
+ if (navigation.length > 0) {
60
+ handleRoute(currentPath);
61
+ }
62
+ }, [navigation, currentPath]);
63
+ const handleRoute = (path) => {
64
+ // Normalize path by removing trailing slash for matching
65
+ const normalizedPath = path.endsWith('/') && path !== '/' ? path.slice(0, -1) : path;
66
+ const route = navigation.find(item => item.path === normalizedPath);
67
+ if (route && contentRef.current) {
68
+ loadApp(route.app, route.path);
69
+ }
70
+ };
71
+ const loadApp = async (appName, appPath) => {
72
+ if (!contentRef.current)
73
+ return;
74
+ contentRef.current.innerHTML = '<div>Loading...</div>';
75
+ try {
76
+ const pluginUrl = `${shell.basePath}${appPath}/app.js`;
77
+ const pluginCss = `${shell.basePath}${appPath}/${appName}.css`;
78
+ // Load CSS
79
+ const link = document.createElement("link");
80
+ link.rel = "stylesheet";
81
+ link.href = pluginCss;
82
+ document.head.appendChild(link);
83
+ // Load JS
84
+ const script = document.createElement("script");
85
+ script.src = pluginUrl;
86
+ script.type = "module";
87
+ script.onload = () => {
88
+ contentRef.current.innerHTML = "";
89
+ if (window.AppMount) {
90
+ window.AppMount(contentRef.current);
91
+ }
92
+ };
93
+ script.onerror = () => {
94
+ contentRef.current.innerHTML = `<div>Failed to load application: ${appName}</div>`;
95
+ };
96
+ document.body.appendChild(script);
97
+ }
98
+ catch (error) {
99
+ console.error("Failed to load plugin:", error);
100
+ contentRef.current.innerHTML = '<div>Failed to load application</div>';
101
+ }
102
+ };
103
+ const handleNavigate = (path) => {
104
+ // Use full page reload for navigation (no server rewrites needed)
105
+ window.location.href = path;
106
+ };
107
+ const handleDialogButton = (value) => {
108
+ if (dialogState.resolve) {
109
+ dialogState.resolve({ value });
110
+ }
111
+ setDialogState({ ...dialogState, visible: false });
112
+ };
113
+ const handleDialogDismiss = () => {
114
+ if (dialogState.resolve) {
115
+ dialogState.resolve({ value: null });
116
+ }
117
+ setDialogState({ ...dialogState, visible: false });
118
+ };
119
+ return (_jsxs(_Fragment, { children: [flashMessages.length > 0 && (_jsx("div", { style: { position: 'fixed', top: 0, left: 0, right: 0, zIndex: 9999 }, children: _jsx(Flashbar, { items: flashMessages.map(msg => ({
120
+ type: msg.type,
121
+ content: msg.content,
122
+ dismissible: msg.dismissible,
123
+ onDismiss: () => shell.adapter.feedback.dismissFlash(msg.id),
124
+ id: msg.id,
125
+ })) }) })), busyMessage && (_jsx("div", { style: {
126
+ position: 'fixed',
127
+ top: 0,
128
+ left: 0,
129
+ right: 0,
130
+ bottom: 0,
131
+ background: 'rgba(0, 0, 0, 0.5)',
132
+ display: 'flex',
133
+ alignItems: 'center',
134
+ justifyContent: 'center',
135
+ zIndex: 10000,
136
+ }, children: _jsxs("div", { style: {
137
+ background: 'white',
138
+ padding: '24px',
139
+ borderRadius: '8px',
140
+ textAlign: 'center',
141
+ }, children: [_jsx(Spinner, { size: "large" }), _jsx("div", { style: { marginTop: '16px' }, children: busyMessage })] }) })), _jsx(Modal, { visible: dialogState.visible, onDismiss: handleDialogDismiss, header: dialogState.title, footer: _jsx(Box, { float: "right", children: _jsx(SpaceBetween, { direction: "horizontal", size: "xs", children: dialogState.buttons.map((btn, idx) => (_jsx(Button, { variant: btn.variant === 'primary' ? 'primary' : 'normal', onClick: () => handleDialogButton(btn.value), children: btn.label }, idx))) }) }), children: dialogState.message }), _jsx(ShellLayout, { navigation: navigation, currentPath: currentPath, basePath: shell.basePath, onNavigate: handleNavigate, children: _jsx("div", { ref: contentRef }) })] }));
142
+ }
@@ -0,0 +1,17 @@
1
+ import React from 'react';
2
+ interface NavigationItem {
3
+ id: string;
4
+ label: string;
5
+ path: string;
6
+ icon?: string;
7
+ }
8
+ interface ShellLayoutProps {
9
+ navigation: NavigationItem[];
10
+ currentPath: string;
11
+ basePath: string;
12
+ onNavigate: (path: string) => void;
13
+ children: React.ReactNode;
14
+ }
15
+ export declare function ShellLayout({ navigation, currentPath, basePath, onNavigate, children }: ShellLayoutProps): import("react/jsx-runtime").JSX.Element;
16
+ export {};
17
+ //# sourceMappingURL=shell-layout.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shell-layout.d.ts","sourceRoot":"","sources":["../src/shell-layout.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA8B,MAAM,OAAO,CAAC;AAMnD,UAAU,cAAc;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,UAAU,gBAAgB;IACxB,UAAU,EAAE,cAAc,EAAE,CAAC;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B;AAED,wBAAgB,WAAW,CAAC,EAAE,UAAU,EAAE,WAAW,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,EAAE,gBAAgB,2CA2BxG"}
@@ -0,0 +1,16 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import AppLayout from '@cloudscape-design/components/app-layout';
4
+ import SideNavigation from '@cloudscape-design/components/side-navigation';
5
+ export function ShellLayout({ navigation, currentPath, basePath, onNavigate, children }) {
6
+ const [navigationOpen, setNavigationOpen] = useState(true);
7
+ const navItems = navigation.map(item => ({
8
+ type: 'link',
9
+ text: item.label,
10
+ href: basePath + item.path + '/', // Add trailing slash for directory routing
11
+ }));
12
+ return (_jsx(AppLayout, { navigationOpen: navigationOpen, onNavigationChange: ({ detail }) => setNavigationOpen(detail.open), navigation: _jsx(SideNavigation, { activeHref: currentPath, items: navItems, onFollow: (event) => {
13
+ event.preventDefault();
14
+ onNavigate(event.detail.href);
15
+ } }), content: children, toolsHide: true }));
16
+ }
package/dist/shell.css ADDED
@@ -0,0 +1,154 @@
1
+ /**
2
+ * CloudScape Shell Styles
3
+ */
4
+
5
+ * {
6
+ box-sizing: border-box;
7
+ }
8
+
9
+ body {
10
+ margin: 0;
11
+ font-family: "Amazon Ember", "Helvetica Neue", Roboto, Arial, sans-serif;
12
+ background: #f2f3f3;
13
+ color: #000716;
14
+ }
15
+
16
+ /* Shell Layout */
17
+ #shell-sidebar {
18
+ position: fixed;
19
+ left: 0;
20
+ top: 0;
21
+ bottom: 0;
22
+ width: 280px;
23
+ background: #232f3e;
24
+ color: white;
25
+ display: flex;
26
+ flex-direction: column;
27
+ z-index: 100;
28
+ box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
29
+ }
30
+
31
+ #shell-header {
32
+ padding: 20px;
33
+ border-bottom: 1px solid #414d5c;
34
+ display: flex;
35
+ align-items: center;
36
+ justify-content: space-between;
37
+ }
38
+
39
+ #shell-title {
40
+ margin: 0;
41
+ font-size: 18px;
42
+ font-weight: 700;
43
+ color: white;
44
+ }
45
+
46
+ #shell-collapse {
47
+ background: transparent;
48
+ border: none;
49
+ color: white;
50
+ cursor: pointer;
51
+ padding: 8px;
52
+ border-radius: 4px;
53
+ }
54
+
55
+ #shell-collapse:hover {
56
+ background: rgba(255, 255, 255, 0.1);
57
+ }
58
+
59
+ /* Navigation */
60
+ #shell-navigation {
61
+ flex: 1;
62
+ overflow-y: auto;
63
+ padding: 8px 0;
64
+ }
65
+
66
+ .shell-nav-item {
67
+ display: flex;
68
+ align-items: center;
69
+ padding: 12px 20px;
70
+ color: #aab7b8;
71
+ text-decoration: none;
72
+ transition: all 0.2s;
73
+ border-left: 4px solid transparent;
74
+ }
75
+
76
+ .shell-nav-item:hover {
77
+ background: rgba(255, 255, 255, 0.05);
78
+ color: white;
79
+ }
80
+
81
+ .shell-nav-item-active {
82
+ background: rgba(255, 255, 255, 0.1);
83
+ color: white;
84
+ border-left-color: #0972d3;
85
+ }
86
+
87
+ .shell-icon {
88
+ margin-right: 12px;
89
+ font-size: 18px;
90
+ }
91
+
92
+ .shell-nav-label {
93
+ font-size: 14px;
94
+ font-weight: 500;
95
+ }
96
+
97
+ /* Main Content */
98
+ #shell-main {
99
+ margin-left: 280px;
100
+ min-height: 100vh;
101
+ background: #f2f3f3;
102
+ }
103
+
104
+ #shell-content {
105
+ padding: 24px;
106
+ max-width: 1600px;
107
+ margin: 0 auto;
108
+ }
109
+
110
+ /* Loading State */
111
+ .shell-loading {
112
+ display: flex;
113
+ align-items: center;
114
+ justify-content: center;
115
+ min-height: 400px;
116
+ color: #545b64;
117
+ font-size: 16px;
118
+ }
119
+
120
+ /* Error State */
121
+ .shell-error {
122
+ background: white;
123
+ border: 1px solid #e9ebed;
124
+ border-radius: 8px;
125
+ padding: 24px;
126
+ margin: 24px;
127
+ color: #d13212;
128
+ font-size: 14px;
129
+ }
130
+
131
+ /* Responsive */
132
+ @media (max-width: 768px) {
133
+ #shell-sidebar {
134
+ width: 60px;
135
+ }
136
+
137
+ #shell-title,
138
+ .shell-nav-label {
139
+ display: none;
140
+ }
141
+
142
+ #shell-main {
143
+ margin-left: 60px;
144
+ }
145
+
146
+ .shell-nav-item {
147
+ justify-content: center;
148
+ padding: 12px;
149
+ }
150
+
151
+ .shell-icon {
152
+ margin-right: 0;
153
+ }
154
+ }
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@herdingbits/trailhead-cloudscape",
3
+ "version": "0.0.1",
4
+ "description": "CloudScape design system adapter for Trailhead micro-frontend framework",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./shell.css": "./dist/shell.css"
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md"
18
+ ],
19
+ "scripts": {
20
+ "clean": "rm -rf dist",
21
+ "build": "npm run clean && tsc && cp src/shell.css dist/"
22
+ },
23
+ "peerDependencies": {
24
+ "@herdingbits/trailhead-core": "^0.0.3",
25
+ "@cloudscape-design/components": "^3.0.0",
26
+ "@cloudscape-design/global-styles": "^1.0.0",
27
+ "react": "^18.0.0",
28
+ "react-dom": "^18.0.0"
29
+ },
30
+ "devDependencies": {
31
+ "@herdingbits/trailhead-types": "^0.0.3",
32
+ "@cloudscape-design/components": "^3.0.1194",
33
+ "@cloudscape-design/global-styles": "^1.0.50",
34
+ "@types/react": "^19.2.13",
35
+ "@types/react-dom": "^19.2.3",
36
+ "react": "^19.2.4",
37
+ "react-dom": "^19.2.4",
38
+ "typescript": "^5.9.3"
39
+ },
40
+ "keywords": [
41
+ "micro-frontend",
42
+ "cloudscape",
43
+ "adapter",
44
+ "trailhead",
45
+ "react"
46
+ ],
47
+ "author": "HerdingBits",
48
+ "license": "MIT",
49
+ "publishConfig": {
50
+ "access": "public"
51
+ },
52
+ "repository": {
53
+ "type": "git",
54
+ "url": "https://github.com/herdingbits/trailhead.git",
55
+ "directory": "packages/cloudscape"
56
+ }
57
+ }