@quincarter/create-lit-app 1.0.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.
@@ -0,0 +1,1741 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # ==============================================================================
4
+ # Lit Element App Shell Architecture Generator
5
+ # Scaffolds modular Lit Element applications with optional Routing, Signals,
6
+ # Contexts, MFE Loader, Theme Switcher, and Component Showcase Suites.
7
+ # ==============================================================================
8
+
9
+ set -e
10
+
11
+ # ANSI Color Tokens
12
+ BOLD="\033[1m"
13
+ GREEN="\033[32m"
14
+ BLUE="\033[34m"
15
+ CYAN="\033[36m"
16
+ YELLOW="\033[33m"
17
+ RED="\033[31m"
18
+ RESET="\033[0m"
19
+
20
+ log_title() {
21
+ echo -e "\n${BOLD}${CYAN}====================================================${RESET}"
22
+ echo -e "${BOLD}${CYAN} $1${RESET}"
23
+ echo -e "${BOLD}${CYAN}====================================================${RESET}\n"
24
+ }
25
+
26
+ log_info() {
27
+ echo -e "${BLUE}ℹ${RESET} $1"
28
+ }
29
+
30
+ log_success() {
31
+ echo -e "${GREEN}✔${RESET} $1"
32
+ }
33
+
34
+ log_warn() {
35
+ echo -e "${YELLOW}⚠${RESET} $1"
36
+ }
37
+
38
+ log_error() {
39
+ echo -e "${RED}✖${RESET} $1"
40
+ }
41
+
42
+ # Default Configuration Variables
43
+ APP_NAME=""
44
+ TEMPLATE="full" # full | blank | custom
45
+ ENABLE_ROUTER=true
46
+ ENABLE_CONTEXT=true
47
+ ENABLE_SIGNALS=true
48
+ ENABLE_HEADER=true
49
+ ENABLE_THEME_SWITCHER=true
50
+ ENABLE_MFE_LOADER=true
51
+ ENABLE_TODOS=true
52
+ ENABLE_CARDS=true
53
+ ENABLE_CHARTS=true
54
+ PACKAGE_MANAGER="yarn"
55
+ INIT_GIT=true
56
+ RUN_INSTALL=false
57
+ NON_INTERACTIVE=false
58
+
59
+ # Helper for CLI Flag Parsing
60
+ parse_args() {
61
+ for arg in "$@"; do
62
+ case $arg in
63
+ --name=*)
64
+ APP_NAME="${arg#*=}"
65
+ ;;
66
+ --template=*)
67
+ TEMPLATE="${arg#*=}"
68
+ ;;
69
+ --router) ENABLE_ROUTER=true ;;
70
+ --no-router) ENABLE_ROUTER=false ;;
71
+ --context) ENABLE_CONTEXT=true ;;
72
+ --no-context) ENABLE_CONTEXT=false ;;
73
+ --signals) ENABLE_SIGNALS=true ;;
74
+ --no-signals) ENABLE_SIGNALS=false ;;
75
+ --header) ENABLE_HEADER=true ;;
76
+ --no-header) ENABLE_HEADER=false ;;
77
+ --theme-switcher) ENABLE_THEME_SWITCHER=true ;;
78
+ --no-theme-switcher) ENABLE_THEME_SWITCHER=false ;;
79
+ --mfe-loader) ENABLE_MFE_LOADER=true ;;
80
+ --no-mfe-loader) ENABLE_MFE_LOADER=false ;;
81
+ --todos) ENABLE_TODOS=true; ENABLE_SIGNALS=true ;;
82
+ --no-todos) ENABLE_TODOS=false ;;
83
+ --cards) ENABLE_CARDS=true ;;
84
+ --no-cards) ENABLE_CARDS=false ;;
85
+ --charts) ENABLE_CHARTS=true ;;
86
+ --no-charts) ENABLE_CHARTS=false ;;
87
+ --pm=*) PACKAGE_MANAGER="${arg#*=}" ;;
88
+ --git) INIT_GIT=true ;;
89
+ --no-git) INIT_GIT=false ;;
90
+ --install) RUN_INSTALL=true ;;
91
+ --no-install) RUN_INSTALL=false ;;
92
+ -v|--version)
93
+ echo "@quincarter/create-lit-app v1.0.0"
94
+ exit 0
95
+ ;;
96
+ --help|-h)
97
+ echo -e "\n${BOLD}${CYAN}⚡ Lit Element App Shell Architecture Generator ⚡${RESET}"
98
+ echo -e "${BLUE}Scaffold modular Lit Element applications with signals, context, routing, and UI suites.${RESET}\n"
99
+
100
+ echo -e "${BOLD}${YELLOW}USAGE:${RESET}"
101
+ echo -e " ${GREEN}npx @quincarter/create-lit-app${RESET} [options]\n"
102
+
103
+ echo -e "${BOLD}${YELLOW}PROJECT CONFIGURATION:${RESET}"
104
+ printf " ${CYAN}%-32s${RESET} %s\n" "--name=<name>" "Project directory / package name"
105
+ printf " ${CYAN}%-32s${RESET} %s\n" "--template=<type>" "Starter template: full | blank | custom (default: full)"
106
+ printf " ${CYAN}%-32s${RESET} %s\n" "--pm=<yarn|npm>" "Package manager to use (default: yarn)"
107
+ echo ""
108
+
109
+ echo -e "${BOLD}${YELLOW}CORE ARCHITECTURE & STATE:${RESET}"
110
+ printf " ${CYAN}%-32s${RESET} %s\n" "--router | --no-router" "Enable/disable @lit-labs/router (auto-enables @lit/context)"
111
+ printf " ${CYAN}%-32s${RESET} %s\n" "--context | --no-context" "Enable/disable @lit/context dependency injection"
112
+ printf " ${CYAN}%-32s${RESET} %s\n" "--signals | --no-signals" "Enable/disable Preact Signals & IndexedDB store"
113
+ echo ""
114
+
115
+ echo -e "${BOLD}${YELLOW}UI COMPONENTS & SHOWCASES:${RESET}"
116
+ printf " ${CYAN}%-32s${RESET} %s\n" "--header | --no-header" "Enable/disable AppShellHeader component"
117
+ printf " ${CYAN}%-32s${RESET} %s\n" "--theme-switcher | --no-theme-switcher" "Enable/disable Theme Switcher component"
118
+ printf " ${CYAN}%-32s${RESET} %s\n" "--todos | --no-todos" "Enable/disable Signals Todo List component & view"
119
+ printf " ${CYAN}%-32s${RESET} %s\n" "--cards | --no-cards" "Enable/disable Generic Card component & view"
120
+ printf " ${CYAN}%-32s${RESET} %s\n" "--charts | --no-charts" "Enable/disable Chart.js wrapper & view"
121
+ printf " ${CYAN}%-32s${RESET} %s\n" "--mfe-loader | --no-mfe-loader" "Enable/disable Micro-Frontend (MFE) loader utility"
122
+ echo ""
123
+
124
+ echo -e "${BOLD}${YELLOW}WORKFLOW & SETUP:${RESET}"
125
+ printf " ${CYAN}%-32s${RESET} %s\n" "--git | --no-git" "Initialize Git repository (default: true)"
126
+ printf " ${CYAN}%-32s${RESET} %s\n" "--install | --no-install" "Run dependency installation step (default: false)"
127
+ printf " ${CYAN}%-32s${RESET} %s\n" "-y, --yes" "Run non-interactively with current flags"
128
+ printf " ${CYAN}%-32s${RESET} %s\n" "-v, --version" "Display version number"
129
+ printf " ${CYAN}%-32s${RESET} %s\n" "-h, --help" "Display this help menu"
130
+ echo ""
131
+ exit 0
132
+ ;;
133
+ esac
134
+ done
135
+ }
136
+
137
+ prompt_user() {
138
+ log_title "Lit Element App Shell Generator"
139
+
140
+ if [ -z "$APP_NAME" ]; then
141
+ read -rp "Enter app name (default: my-app-shell): " input_name
142
+ APP_NAME=${input_name:-my-app-shell}
143
+ fi
144
+
145
+ echo -e "\nChoose starter template preset:"
146
+ echo " 1) Full App Shell (Header, Navigation, Router, Contexts, Signals, Todos Showcase, Cards, Charts, MFEs)"
147
+ echo " 2) Blank App Shell (Minimal Lit App Host)"
148
+ echo " 3) Custom Selection (Select specific features interactively)"
149
+ read -rp "Selection [1-3] (default: 1): " template_choice
150
+
151
+ case $template_choice in
152
+ 2)
153
+ TEMPLATE="blank"
154
+ ENABLE_ROUTER=false
155
+ ENABLE_CONTEXT=false
156
+ ENABLE_SIGNALS=false
157
+ ENABLE_HEADER=false
158
+ ENABLE_THEME_SWITCHER=false
159
+ ENABLE_MFE_LOADER=false
160
+ ENABLE_TODOS=false
161
+ ENABLE_CARDS=false
162
+ ENABLE_CHARTS=false
163
+ ;;
164
+ 3)
165
+ TEMPLATE="custom"
166
+ read -rp "Enable Routing (@lit-labs/router)? [Y/n]: " ans
167
+ if [[ "$ans" =~ ^[Nn] ]]; then
168
+ ENABLE_ROUTER=false
169
+ else
170
+ ENABLE_ROUTER=true
171
+ ENABLE_CONTEXT=true
172
+ log_info "Lit Context (@lit/context) automatically enabled for Routing."
173
+ fi
174
+
175
+ if [ "$ENABLE_ROUTER" = false ]; then
176
+ read -rp "Enable Lit Context (@lit/context)? [Y/n]: " ans
177
+ [[ "$ans" =~ ^[Nn] ]] && ENABLE_CONTEXT=false || ENABLE_CONTEXT=true
178
+ fi
179
+
180
+ read -rp "Enable Preact Signals & IndexedDB Stores (@lit-labs/preact-signals)? [Y/n]: " ans
181
+ [[ "$ans" =~ ^[Nn] ]] && ENABLE_SIGNALS=false || ENABLE_SIGNALS=true
182
+
183
+ read -rp "Include Signals Showcase (Todo List Component & Store)? [Y/n]: " ans
184
+ if [[ "$ans" =~ ^[Nn] ]]; then
185
+ ENABLE_TODOS=false
186
+ else
187
+ ENABLE_TODOS=true
188
+ ENABLE_SIGNALS=true
189
+ fi
190
+
191
+ read -rp "Include Generic Card Component & Examples Page? [Y/n]: " ans
192
+ [[ "$ans" =~ ^[Nn] ]] && ENABLE_CARDS=false || ENABLE_CARDS=true
193
+
194
+ read -rp "Include Chart.js Wrapper Component & Examples Page? [Y/n]: " ans
195
+ [[ "$ans" =~ ^[Nn] ]] && ENABLE_CHARTS=false || ENABLE_CHARTS=true
196
+
197
+ read -rp "Enable App Shell Header component? [Y/n]: " ans
198
+ [[ "$ans" =~ ^[Nn] ]] && ENABLE_HEADER=false || ENABLE_HEADER=true
199
+
200
+ read -rp "Enable Theme Switcher component? [Y/n]: " ans
201
+ [[ "$ans" =~ ^[Nn] ]] && ENABLE_THEME_SWITCHER=false || ENABLE_THEME_SWITCHER=true
202
+
203
+ read -rp "Enable Micro-Frontend (MFE) Loader utility? [Y/n]: " ans
204
+ [[ "$ans" =~ ^[Nn] ]] && ENABLE_MFE_LOADER=false || ENABLE_MFE_LOADER=true
205
+ ;;
206
+ *)
207
+ TEMPLATE="full"
208
+ ENABLE_ROUTER=true
209
+ ENABLE_CONTEXT=true
210
+ ENABLE_SIGNALS=true
211
+ ENABLE_HEADER=true
212
+ ENABLE_THEME_SWITCHER=true
213
+ ENABLE_MFE_LOADER=true
214
+ ENABLE_TODOS=true
215
+ ENABLE_CARDS=true
216
+ ENABLE_CHARTS=true
217
+ ;;
218
+ esac
219
+
220
+ read -rp "Choose package manager [yarn/npm] (default: yarn): " pm_choice
221
+ PACKAGE_MANAGER=${pm_choice:-yarn}
222
+
223
+ read -rp "Initialize Git repository? [Y/n]: " git_ans
224
+ [[ "$git_ans" =~ ^[Nn] ]] && INIT_GIT=false || INIT_GIT=true
225
+
226
+ read -rp "Run dependency installation now? [y/N]: " install_ans
227
+ [[ "$install_ans" =~ ^[Yy] ]] && RUN_INSTALL=true || RUN_INSTALL=false
228
+ }
229
+
230
+ parse_args "$@"
231
+
232
+ if [ "$NON_INTERACTIVE" = false ] && [ -t 0 ]; then
233
+ prompt_user
234
+ else
235
+ if [ -z "$APP_NAME" ]; then
236
+ APP_NAME="my-app-shell"
237
+ fi
238
+ if [ "$TEMPLATE" = "blank" ]; then
239
+ ENABLE_ROUTER=false
240
+ ENABLE_CONTEXT=false
241
+ ENABLE_SIGNALS=false
242
+ ENABLE_HEADER=false
243
+ ENABLE_THEME_SWITCHER=false
244
+ ENABLE_MFE_LOADER=false
245
+ ENABLE_TODOS=false
246
+ ENABLE_CARDS=false
247
+ ENABLE_CHARTS=false
248
+ fi
249
+ fi
250
+
251
+ # Mandatory Dependency Rules
252
+ if [ "$ENABLE_ROUTER" = true ]; then
253
+ ENABLE_CONTEXT=true
254
+ fi
255
+ if [ "$ENABLE_TODOS" = true ]; then
256
+ ENABLE_SIGNALS=true
257
+ fi
258
+
259
+ TARGET_DIR="$(pwd)/$APP_NAME"
260
+
261
+ if [ -d "$TARGET_DIR" ]; then
262
+ log_error "Target directory '$APP_NAME' already exists."
263
+ exit 1
264
+ fi
265
+
266
+ log_info "Creating project directory: $TARGET_DIR"
267
+ mkdir -p "$TARGET_DIR"
268
+ cd "$TARGET_DIR"
269
+
270
+ # ------------------------------------------------------------------------------
271
+ # Generate package.json
272
+ # ------------------------------------------------------------------------------
273
+ log_info "Generating package.json..."
274
+
275
+ DEPS='"lit": "^3.3.3"'
276
+ if [ "$ENABLE_ROUTER" = true ]; then
277
+ DEPS="$DEPS,
278
+ \"@lit-labs/router\": \"^0.1.4\",
279
+ \"urlpattern-polyfill\": \"^10.1.0\""
280
+ fi
281
+ if [ "$ENABLE_CONTEXT" = true ]; then
282
+ DEPS="$DEPS,
283
+ \"@lit/context\": \"^1.1.6\""
284
+ fi
285
+ if [ "$ENABLE_SIGNALS" = true ]; then
286
+ DEPS="$DEPS,
287
+ \"@lit-labs/preact-signals\": \"^1.0.3\",
288
+ \"idb\": \"^8.0.3\""
289
+ fi
290
+ if [ "$ENABLE_CHARTS" = true ]; then
291
+ DEPS="$DEPS,
292
+ \"chart.js\": \"^4.5.1\",
293
+ \"chartjs-adapter-luxon\": \"^1.3.1\",
294
+ \"luxon\": \"^3.7.2\",
295
+ \"@kurkle/color\": \"^0.4.0\""
296
+ fi
297
+
298
+ cat <<EOF > package.json
299
+ {
300
+ "name": "@quincarter/$APP_NAME",
301
+ "private": true,
302
+ "version": "0.0.0",
303
+ "type": "module",
304
+ "scripts": {
305
+ "start": "vite",
306
+ "build": "tsc && vite build",
307
+ "preview": "yarn build && vite preview",
308
+ "test": "vitest run"
309
+ },
310
+ "dependencies": {
311
+ $DEPS
312
+ },
313
+ "devDependencies": {
314
+ "@biomejs/biome": "2.5.5",
315
+ "@types/luxon": "^3.7.2",
316
+ "@types/mocha": "^10.0.10",
317
+ "typescript": "^5.4.5",
318
+ "vite": "^5.2.11",
319
+ "vite-plugin-pwa": "^0.20.0",
320
+ "vitest": "^1.6.0"
321
+ }
322
+ }
323
+ EOF
324
+
325
+ # ------------------------------------------------------------------------------
326
+ # Generate Configuration Files
327
+ # ------------------------------------------------------------------------------
328
+ log_info "Generating configuration files (tsconfig.json, vite.config.ts, biome.json)..."
329
+
330
+ cat <<'EOF' > tsconfig.json
331
+ {
332
+ "compilerOptions": {
333
+ "target": "ES2022",
334
+ "module": "ESNext",
335
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
336
+ "declaration": true,
337
+ "emitDeclarationOnly": false,
338
+ "outDir": "./dist",
339
+ "rootDir": "./src",
340
+ "strict": true,
341
+ "noImplicitReturns": true,
342
+ "noFallthroughCasesInSwitch": true,
343
+ "moduleResolution": "bundler",
344
+ "allowSyntheticDefaultImports": true,
345
+ "experimentalDecorators": true,
346
+ "useDefineForClassFields": false,
347
+ "skipLibCheck": true
348
+ },
349
+ "include": ["src/**/*.ts"]
350
+ }
351
+ EOF
352
+
353
+ cat <<'EOF' > vite.config.ts
354
+ import { defineConfig } from 'vite';
355
+
356
+ export default defineConfig({
357
+ build: {
358
+ target: 'esnext',
359
+ },
360
+ });
361
+ EOF
362
+
363
+ cat <<'EOF' > biome.json
364
+ {
365
+ "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json",
366
+ "organizeImports": {
367
+ "enabled": true
368
+ },
369
+ "linter": {
370
+ "enabled": true,
371
+ "rules": {
372
+ "recommended": true
373
+ }
374
+ },
375
+ "formatter": {
376
+ "enabled": true,
377
+ "indentStyle": "tab"
378
+ }
379
+ }
380
+ EOF
381
+
382
+ cat <<'EOF' > .gitignore
383
+ node_modules
384
+ dist
385
+ .DS_Store
386
+ *.log
387
+ EOF
388
+
389
+ cat <<EOF > index.html
390
+ <!DOCTYPE html>
391
+ <html lang="en">
392
+ <head>
393
+ <meta charset="UTF-8" />
394
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
395
+ <title>$APP_NAME</title>
396
+ <script src="/detect-color-scheme.js"></script>
397
+ <link rel="stylesheet" href="/src/index.css" />
398
+ </head>
399
+ <body>
400
+ <app-shell></app-shell>
401
+ <script type="module" src="/src/app-shell.ts"></script>
402
+ </body>
403
+ </html>
404
+ EOF
405
+
406
+ mkdir -p public
407
+ cat <<'EOF' > public/detect-color-scheme.js
408
+ (function () {
409
+ const storedTheme = localStorage.getItem("theme");
410
+ if (storedTheme) {
411
+ document.documentElement.setAttribute("data-theme", storedTheme);
412
+ } else if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
413
+ document.documentElement.setAttribute("data-theme", "dark");
414
+ } else {
415
+ document.documentElement.setAttribute("data-theme", "light");
416
+ }
417
+ })();
418
+ EOF
419
+
420
+ mkdir -p src/assets
421
+
422
+ cat <<'EOF' > src/index.css
423
+ :root {
424
+ --primary-text-color: #212529;
425
+ --bg-color: #ffffff;
426
+ --nav-background-color: #f8f9fa;
427
+ --primary-box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
428
+ font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
429
+ line-height: 1.5;
430
+ font-weight: 400;
431
+ }
432
+
433
+ [data-theme="dark"] {
434
+ --primary-text-color: #f8f9fa;
435
+ --bg-color: #121212;
436
+ --nav-background-color: #1e1e1e;
437
+ --primary-box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.5);
438
+ }
439
+
440
+ body {
441
+ margin: 0;
442
+ padding: 0;
443
+ background-color: var(--bg-color);
444
+ color: var(--primary-text-color);
445
+ }
446
+ EOF
447
+
448
+ cat <<'EOF' > src/vite-env.d.ts
449
+ /// <reference types="vite/client" />
450
+ EOF
451
+
452
+ # ------------------------------------------------------------------------------
453
+ # Create Shared Directories & Modules
454
+ # ------------------------------------------------------------------------------
455
+ log_info "Scaffolding shared modules..."
456
+ mkdir -p src/shared/configuration
457
+ mkdir -p src/shared/contexts
458
+ mkdir -p src/shared/interfaces
459
+ mkdir -p src/shared/internal-views
460
+ mkdir -p src/shared/stores
461
+ mkdir -p src/shared/utilities
462
+
463
+ cat <<'EOF' > src/shared/configuration/base-path.ts
464
+ const raw = import.meta.env.BASE_URL ?? "/";
465
+ export const BASE_PATH = raw.endsWith("/") ? raw.slice(0, -1) : raw;
466
+
467
+ export const withBase = (path: string): string => {
468
+ const normalized = path.startsWith("/") ? path : `/${path}`;
469
+ return `${BASE_PATH}${normalized}`;
470
+ };
471
+ EOF
472
+
473
+ cat <<'EOF' > src/shared/interfaces/navigation.interface.ts
474
+ import type { MfeItem } from "../utilities/mfe-loader.utility";
475
+
476
+ export interface NavItem {
477
+ name: string;
478
+ path: string;
479
+ directory: string;
480
+ filePath: string;
481
+ levelOfAccess: string[];
482
+ action?: () => void;
483
+ children?: NavItem[];
484
+ component: string;
485
+ tagName: string;
486
+ icon?: IconType;
487
+ userHasPermission?: boolean;
488
+ mfeComponent?: MfeItem;
489
+ isMfe: boolean;
490
+ }
491
+
492
+ export declare type IconType = "test";
493
+ EOF
494
+
495
+ cat <<'EOF' > src/shared/interfaces/todos.interface.ts
496
+ export interface ITodoItem {
497
+ id: string;
498
+ title: string;
499
+ description?: string;
500
+ checked: boolean;
501
+ }
502
+ EOF
503
+
504
+ cat <<'EOF' > src/shared/utilities/mfe-loader.utility.ts
505
+ export type CrossoriginType = "anonymous" | "use-credentials" | "";
506
+
507
+ export interface MfeItem {
508
+ mfeBundleUrl: string;
509
+ scriptType: string;
510
+ isAsync: boolean;
511
+ defer: boolean;
512
+ crossOrigin: CrossoriginType;
513
+ tagName: string;
514
+ associatedInternalTag?: string;
515
+ }
516
+
517
+ export class MfeLoader {
518
+ public config: MfeItem[];
519
+
520
+ constructor(mfeConfig: MfeItem[]) {
521
+ this.config = mfeConfig;
522
+ }
523
+
524
+ init(): void {
525
+ this.config.forEach(async (configItem) => {
526
+ await this.createLink(configItem);
527
+ await this.createScript(configItem);
528
+ });
529
+ }
530
+
531
+ private async createScript(configItem: MfeItem): Promise<void> {
532
+ if (!document.querySelector(`#mfe-${configItem.tagName}`)) {
533
+ const script = document.createElement("script");
534
+ script.src = configItem.mfeBundleUrl;
535
+ script.type = configItem.scriptType;
536
+ script.async = configItem.isAsync;
537
+ script.defer = configItem.defer;
538
+ script.crossOrigin = configItem.crossOrigin;
539
+ script.id = `mfe-${configItem.tagName}`;
540
+ await document.body.appendChild(script);
541
+ }
542
+ }
543
+
544
+ private async createLink(configItem: MfeItem): Promise<void> {
545
+ if (!document.querySelector(`#link-mfe-${configItem.tagName}`)) {
546
+ const link = document.createElement("link");
547
+ link.id = `link-mfe-${configItem.tagName}`;
548
+ link.href = configItem.mfeBundleUrl;
549
+ link.rel = "prefetch";
550
+ link.as = "script";
551
+ await document.head.appendChild(link);
552
+ }
553
+ }
554
+ }
555
+ EOF
556
+
557
+ cat <<'EOF' > src/shared/configuration/mfes.ts
558
+ import type { MfeItem } from "../utilities/mfe-loader.utility";
559
+
560
+ export const MFE_LOADER_CONFIG: MfeItem[] = [
561
+ {
562
+ mfeBundleUrl: "https://quincarter.github.io/my-coffee-app/assets/coffee-users-PQOfY16o.js",
563
+ scriptType: "module",
564
+ isAsync: false,
565
+ defer: false,
566
+ crossOrigin: "anonymous",
567
+ tagName: "coffee-users",
568
+ associatedInternalTag: "home-page",
569
+ }
570
+ ];
571
+ EOF
572
+
573
+ cat <<'EOF' > src/shared/utilities/app-root.utility.ts
574
+ import type { NavItem } from "../interfaces/navigation.interface";
575
+
576
+ export namespace AppRootUtilities {
577
+ export function getNotAllowedRoutes(
578
+ fullNavList: NavItem[],
579
+ notAlowedRouteList: NavItem[],
580
+ ) {
581
+ const notAllowed: NavItem[] = [];
582
+ const navItems = fullNavList.filter((item) => {
583
+ if (item.userHasPermission) {
584
+ return true;
585
+ }
586
+ notAllowed.push(item);
587
+ return false;
588
+ });
589
+
590
+ return {
591
+ navItems,
592
+ notAllowed: [...notAlowedRouteList, ...notAllowed],
593
+ };
594
+ }
595
+ }
596
+ EOF
597
+
598
+ # Build navigation routes based on enabled features
599
+ if [ "$ENABLE_ROUTER" = true ]; then
600
+ NAV_ITEMS='{
601
+ name: "Home",
602
+ path: "/home",
603
+ directory: "home-page",
604
+ component: "home-page",
605
+ filePath: "../../views/home-page/home-page.ts",
606
+ levelOfAccess: ["public"],
607
+ tagName: "home-page",
608
+ isMfe: false,
609
+ }'
610
+
611
+ if [ "$ENABLE_TODOS" = true ]; then
612
+ NAV_ITEMS="$NAV_ITEMS,
613
+ {
614
+ name: \"Todos (Signals)\",
615
+ path: \"/todos-page\",
616
+ directory: \"todos-page\",
617
+ component: \"todos-page\",
618
+ filePath: \"../../views/todos-page/todos-page.ts\",
619
+ levelOfAccess: [\"public\"],
620
+ tagName: \"todos-page\",
621
+ isMfe: false,
622
+ }"
623
+ fi
624
+
625
+ if [ "$ENABLE_CARDS" = true ]; then
626
+ NAV_ITEMS="$NAV_ITEMS,
627
+ {
628
+ name: \"Card Examples\",
629
+ path: \"/card-examples\",
630
+ directory: \"card-examples\",
631
+ component: \"card-examples\",
632
+ filePath: \"../../views/card-examples/card-examples.ts\",
633
+ levelOfAccess: [\"public\"],
634
+ tagName: \"card-examples\",
635
+ isMfe: false,
636
+ }"
637
+ fi
638
+
639
+ if [ "$ENABLE_CHARTS" = true ]; then
640
+ NAV_ITEMS="$NAV_ITEMS,
641
+ {
642
+ name: \"Charts\",
643
+ path: \"/chart-examples\",
644
+ directory: \"chart-examples\",
645
+ component: \"chart-examples\",
646
+ filePath: \"../../views/chart-examples/chart-examples.ts\",
647
+ levelOfAccess: [\"public\"],
648
+ tagName: \"chart-examples\",
649
+ isMfe: false,
650
+ }"
651
+ fi
652
+
653
+ if [ "$ENABLE_MFE_LOADER" = true ]; then
654
+ NAV_ITEMS="$NAV_ITEMS,
655
+ {
656
+ name: \"Vite MFE\",
657
+ path: \"/vite-mfe\",
658
+ directory: \"vite-mfe\",
659
+ component: \"vite-mfe\",
660
+ filePath: \"../../views/vite-mfe/vite-mfe.ts\",
661
+ levelOfAccess: [\"public\"],
662
+ tagName: \"vite-mfe\",
663
+ isMfe: true,
664
+ }"
665
+ fi
666
+
667
+ cat <<EOF > src/shared/configuration/nav.ts
668
+ import type { NavItem } from "../interfaces/navigation.interface";
669
+
670
+ export const navigationRouting: NavItem[] = [
671
+ $NAV_ITEMS
672
+ ];
673
+
674
+ export const sidePages: NavItem[] = [];
675
+ EOF
676
+
677
+ cat <<'EOF' > src/shared/configuration/routes.ts
678
+ import type { IconType, NavItem } from "../interfaces/navigation.interface";
679
+ import type { MfeItem } from "../utilities/mfe-loader.utility";
680
+ import { MFE_LOADER_CONFIG } from "./mfes";
681
+
682
+ export const getAccessPermissions = (
683
+ item: NavItem,
684
+ accesses: string[],
685
+ ): boolean => item.levelOfAccess.some((access) => accesses.includes(access));
686
+
687
+ export const getMfeComponent = (
688
+ internalTagName: string,
689
+ ): MfeItem | undefined => {
690
+ const mfe = MFE_LOADER_CONFIG.filter(
691
+ (mfe: MfeItem) => mfe.associatedInternalTag === internalTagName,
692
+ );
693
+ return mfe.length > 0 ? mfe[0] : undefined;
694
+ };
695
+
696
+ export const routesBuilt = (
697
+ navItems: NavItem[],
698
+ accesses: string[],
699
+ ): NavItem[] => {
700
+ return navItems.map((navItem: NavItem) => ({
701
+ ...navItem,
702
+ icon: navItem.icon || ("" as IconType),
703
+ mfeComponent: getMfeComponent(navItem.tagName),
704
+ userHasPermission: getAccessPermissions(navItem, accesses),
705
+ }));
706
+ };
707
+ EOF
708
+ fi
709
+
710
+ # Contexts
711
+ if [ "$ENABLE_CONTEXT" = true ]; then
712
+ cat <<'EOF' > src/shared/contexts/accesses.context.ts
713
+ import { createContext } from "@lit/context";
714
+ export const AccessesContext = createContext<string[]>("accessesContext");
715
+ EOF
716
+ cat <<'EOF' > src/shared/contexts/navigation.context.ts
717
+ import { createContext } from "@lit/context";
718
+ import type { NavItem } from "../interfaces/navigation.interface";
719
+ export const NavigationContext = createContext<NavItem[]>("navContext");
720
+ EOF
721
+ cat <<'EOF' > src/shared/contexts/mfe-loader.context.ts
722
+ import { createContext } from "@lit/context";
723
+ import type { MfeLoader } from "../utilities/mfe-loader.utility";
724
+ export const MfeLoaderContext = createContext<MfeLoader>("mfeLoaderContext");
725
+ EOF
726
+ fi
727
+
728
+ # Preact Signals & Persistence Stores
729
+ if [ "$ENABLE_SIGNALS" = true ]; then
730
+ cat <<'EOF' > src/shared/stores/persistent-signal.ts
731
+ import { effect, signal, type Signal } from "@lit-labs/preact-signals";
732
+ import { openDB, type IDBPDatabase } from "idb";
733
+
734
+ const DB_NAME = "app-shell-store";
735
+ const STORE_NAME = "signals";
736
+ const DB_VERSION = 1;
737
+
738
+ let dbPromise: Promise<IDBPDatabase> | null = null;
739
+
740
+ const getDB = () => {
741
+ if (!dbPromise) {
742
+ dbPromise = openDB(DB_NAME, DB_VERSION, {
743
+ upgrade(db) {
744
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
745
+ db.createObjectStore(STORE_NAME);
746
+ }
747
+ },
748
+ });
749
+ }
750
+ return dbPromise;
751
+ };
752
+
753
+ export interface PersistentSignalOptions {
754
+ key?: string;
755
+ }
756
+
757
+ export function persistentSignal<T>(
758
+ defaultValue: T,
759
+ options: PersistentSignalOptions = {},
760
+ ): Signal<T> {
761
+ const { key } = options;
762
+ const s = signal<T>(defaultValue);
763
+
764
+ if (!key) return s;
765
+
766
+ const initialized = signal(false);
767
+
768
+ getDB().then(async (db) => {
769
+ try {
770
+ const storedValue = await db.get(STORE_NAME, key);
771
+ if (storedValue !== undefined) {
772
+ s.value = storedValue;
773
+ }
774
+ } catch (error) {
775
+ console.error(`Failed to load persistent signal for key "${key}":`, error);
776
+ } finally {
777
+ initialized.value = true;
778
+ }
779
+ });
780
+
781
+ effect(() => {
782
+ const value = s.value;
783
+ if (!initialized.value) return;
784
+
785
+ (async () => {
786
+ try {
787
+ const db = await getDB();
788
+ await db.put(STORE_NAME, value, key);
789
+ } catch (error) {
790
+ console.error(`Failed to persist signal for key "${key}":`, error);
791
+ }
792
+ })();
793
+ });
794
+
795
+ return s;
796
+ }
797
+
798
+ export async function clearPersistentSignals(): Promise<void> {
799
+ const db = await getDB();
800
+ await db.clear(STORE_NAME);
801
+ }
802
+ EOF
803
+
804
+ cat <<'EOF' > src/shared/stores/todo.store.ts
805
+ import { persistentSignal } from "./persistent-signal";
806
+ import type { ITodoItem } from "../interfaces/todos.interface";
807
+
808
+ export const todosSignal = persistentSignal<Record<string, ITodoItem>>({}, { key: "todos" });
809
+
810
+ export const addTodo = (title: string, description?: string) => {
811
+ const id = crypto.randomUUID();
812
+ todosSignal.value = {
813
+ ...todosSignal.value,
814
+ [id]: { id, title, description, checked: false },
815
+ };
816
+ return id;
817
+ };
818
+
819
+ export const toggleTodo = (id: string) => {
820
+ const todo = todosSignal.value[id];
821
+ if (todo) {
822
+ todosSignal.value = {
823
+ ...todosSignal.value,
824
+ [id]: { ...todo, checked: !todo.checked },
825
+ };
826
+ }
827
+ };
828
+
829
+ export const removeTodo = (id: string) => {
830
+ const newTodos = { ...todosSignal.value };
831
+ delete newTodos[id];
832
+ todosSignal.value = newTodos;
833
+ };
834
+ EOF
835
+
836
+ cat <<'EOF' > src/shared/stores/todo-list.store.ts
837
+ import { computed } from "@lit-labs/preact-signals";
838
+ import { persistentSignal } from "./persistent-signal";
839
+ import { todosSignal } from "./todo.store";
840
+
841
+ export const todoOrderSignal = persistentSignal<string[]>([], { key: "todo-order" });
842
+ export const sortDirectionSignal = persistentSignal<"asc" | "desc">("asc", { key: "sort-direction" });
843
+
844
+ export const sortedTodosSignal = computed(() => {
845
+ const todos = todoOrderSignal.value
846
+ .map((id) => todosSignal.value[id])
847
+ .filter(Boolean);
848
+
849
+ return sortDirectionSignal.value === "asc"
850
+ ? [...todos].sort((a, b) => a.title.localeCompare(b.title))
851
+ : [...todos].sort((a, b) => b.title.localeCompare(a.title));
852
+ });
853
+
854
+ export const addIdToOrder = (id: string) => {
855
+ todoOrderSignal.value = [...todoOrderSignal.value, id];
856
+ };
857
+
858
+ export const removeIdFromOrder = (id: string) => {
859
+ todoOrderSignal.value = todoOrderSignal.value.filter(
860
+ (todoId) => todoId !== id,
861
+ );
862
+ };
863
+
864
+ export const toggleSortDirection = () => {
865
+ sortDirectionSignal.value =
866
+ sortDirectionSignal.value === "asc" ? "desc" : "asc";
867
+ };
868
+ EOF
869
+ fi
870
+
871
+ # Internal Views
872
+ mkdir -p src/shared/internal-views/404-not-found
873
+ cat <<'EOF' > src/shared/internal-views/404-not-found/PageNotFound.ts
874
+ import { css, html, LitElement } from "lit";
875
+ export class PageNotFound extends LitElement {
876
+ static styles = [css`:host { display: block; padding: 2rem; }`];
877
+ render() { return html`<h2>404 - Page Not Found</h2>`; }
878
+ }
879
+ EOF
880
+ cat <<'EOF' > src/shared/internal-views/404-not-found/page-not-found.ts
881
+ import { PageNotFound } from "./PageNotFound";
882
+ customElements.define("page-not-found", PageNotFound);
883
+ EOF
884
+
885
+ mkdir -p src/shared/internal-views/no-access
886
+ cat <<'EOF' > src/shared/internal-views/no-access/NoAccess.ts
887
+ import { css, html, LitElement } from "lit";
888
+ export class NoAccess extends LitElement {
889
+ static styles = [css`:host { display: block; padding: 2rem; }`];
890
+ render() { return html`<h2>Access Denied</h2>`; }
891
+ }
892
+ EOF
893
+ cat <<'EOF' > src/shared/internal-views/no-access/no-access.ts
894
+ import { NoAccess } from "./NoAccess";
895
+ customElements.define("no-access", NoAccess);
896
+ EOF
897
+
898
+ mkdir -p src/shared/internal-views/under-construction
899
+ cat <<'EOF' > src/shared/internal-views/under-construction/UnderConstruction.ts
900
+ import { css, html, LitElement } from "lit";
901
+ export class UnderConstruction extends LitElement {
902
+ static styles = [css`:host { display: block; padding: 2rem; }`];
903
+ render() { return html`<h2>Under Construction</h2>`; }
904
+ }
905
+ EOF
906
+ cat <<'EOF' > src/shared/internal-views/under-construction/under-construction.ts
907
+ import { UnderConstruction } from "./UnderConstruction";
908
+ customElements.define("under-construction", UnderConstruction);
909
+ EOF
910
+
911
+ # ------------------------------------------------------------------------------
912
+ # Create Components
913
+ # ------------------------------------------------------------------------------
914
+ log_info "Generating UI components..."
915
+ mkdir -p src/components
916
+
917
+ if [ "$ENABLE_THEME_SWITCHER" = true ]; then
918
+ mkdir -p src/components/theme-switcher
919
+ cat <<'EOF' > src/components/theme-switcher/theme-switcher.styles.ts
920
+ import { css } from "lit";
921
+ export const ThemeSwitcherStyles = css`
922
+ :host { height: fit-content; justify-content: right; display: flex; }
923
+ .switch { position: relative; display: inline-block; width: 3rem; height: 1.5rem; }
924
+ .switch input { opacity: 0; width: 0; height: 0; }
925
+ .slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #27173a; transition: 0.4s; border-radius: 5rem; }
926
+ .slider:before { position: absolute; content: ''; height: 1rem; width: 1rem; left: 5px; bottom: 4px; background-color: #ffc207; transition: 0.4s; border-radius: 50%; }
927
+ input:checked + .slider:before { transform: translateX(1.5rem); box-shadow: inset -3px 0 0 2px #ffc207; background-color: #27173a; }
928
+ `;
929
+ EOF
930
+ cat <<'EOF' > src/components/theme-switcher/ThemeSwitcher.ts
931
+ import { type HTMLTemplateResult, html, LitElement, type PropertyValues } from "lit";
932
+ import { query } from "lit/decorators.js";
933
+ import { ThemeSwitcherStyles } from "./theme-switcher.styles";
934
+
935
+ export class ThemeSwitcher extends LitElement {
936
+ @query('#theme-switch input[type="checkbox"]')
937
+ switchBox: HTMLInputElement | undefined;
938
+
939
+ static styles = [ThemeSwitcherStyles];
940
+
941
+ protected firstUpdated(_changedProperties: PropertyValues): void {
942
+ super.firstUpdated(_changedProperties);
943
+ if (document.documentElement.getAttribute("data-theme") === "dark") {
944
+ this.switchBox?.setAttribute("checked", "");
945
+ }
946
+ }
947
+
948
+ switchTheme(e: Event) {
949
+ const target = e.target as HTMLInputElement;
950
+ if (target.checked) {
951
+ localStorage.setItem("theme", "dark");
952
+ document.documentElement.setAttribute("data-theme", "dark");
953
+ this.switchBox?.setAttribute("checked", "");
954
+ } else {
955
+ localStorage.setItem("theme", "light");
956
+ document.documentElement.setAttribute("data-theme", "light");
957
+ this.switchBox?.removeAttribute("checked");
958
+ }
959
+ }
960
+
961
+ render(): HTMLTemplateResult {
962
+ return html`
963
+ <label id="theme-switch" class="switch">
964
+ <input type="checkbox" @click="${this.switchTheme}" />
965
+ <span class="slider round"></span>
966
+ </label>
967
+ `;
968
+ }
969
+ }
970
+ EOF
971
+ cat <<'EOF' > src/components/theme-switcher/theme-switcher.ts
972
+ import { ThemeSwitcher } from "./ThemeSwitcher";
973
+ customElements.define("theme-switcher", ThemeSwitcher);
974
+ EOF
975
+ fi
976
+
977
+ if [ "$ENABLE_HEADER" = true ]; then
978
+ mkdir -p src/components/header
979
+ cat <<'EOF' > src/components/header/app-shell-header.styles.ts
980
+ import { css } from "lit";
981
+ export const AppShellHeaderStyles = css`
982
+ nav { align-items: center; background-color: var(--nav-background-color); box-shadow: var(--primary-box-shadow); display: flex; justify-content: space-between; padding: 0.75rem 1.5rem; }
983
+ ul { display: flex; gap: 1.5rem; list-style: none; margin: 0; padding: 0; }
984
+ a { font-weight: 500; color: var(--primary-text-color); text-decoration: none; }
985
+ a:hover { text-decoration: underline; }
986
+ `;
987
+ EOF
988
+ cat <<'EOF' > src/components/header/AppShellHeader.ts
989
+ import { type HTMLTemplateResult, html, LitElement, nothing } from "lit";
990
+ import { property } from "lit/decorators.js";
991
+ import { withBase } from "../../shared/configuration/base-path";
992
+ import type { NavItem } from "../../shared/interfaces/navigation.interface";
993
+ import { AppShellHeaderStyles } from "./app-shell-header.styles";
994
+ import "../theme-switcher/theme-switcher";
995
+
996
+ export class AppShellHeader extends LitElement {
997
+ @property({ attribute: "routes", type: Array })
998
+ routes: NavItem[] = [] as NavItem[];
999
+
1000
+ @property({ type: Boolean, attribute: "enable-theme-switcher" })
1001
+ enableThemeSwitcher = false;
1002
+
1003
+ static styles = [AppShellHeaderStyles];
1004
+
1005
+ render(): HTMLTemplateResult {
1006
+ return html`
1007
+ <nav>
1008
+ <div class="brand">
1009
+ <a href="${withBase("/")}"><strong>App Shell</strong></a>
1010
+ </div>
1011
+ ${
1012
+ this.routes.length > 0
1013
+ ? html`<ul>
1014
+ ${this.routes.map(
1015
+ (route) =>
1016
+ html`<li><a href="${withBase(route.path)}">${route.name}</a></li>`,
1017
+ )}
1018
+ </ul>`
1019
+ : nothing
1020
+ }
1021
+ ${
1022
+ this.enableThemeSwitcher
1023
+ ? html`<theme-switcher></theme-switcher>`
1024
+ : nothing
1025
+ }
1026
+ </nav>
1027
+ <slot></slot>
1028
+ `;
1029
+ }
1030
+ }
1031
+ EOF
1032
+ cat <<'EOF' > src/components/header/app-shell-header.ts
1033
+ import { AppShellHeader } from "./AppShellHeader";
1034
+ customElements.define("app-shell-header", AppShellHeader);
1035
+ EOF
1036
+ fi
1037
+
1038
+ if [ "$ENABLE_CARDS" = true ]; then
1039
+ mkdir -p src/components/card
1040
+ cat <<'EOF' > src/components/card/generic-card.styles.ts
1041
+ import { css } from "lit";
1042
+ export const GenericCardStyles = css`
1043
+ :host { display: block; }
1044
+ .card-wrapper {
1045
+ border: 1px solid rgba(0, 0, 0, 0.125);
1046
+ border-radius: 0.5rem;
1047
+ padding: 1.25rem;
1048
+ background-color: var(--bg-color);
1049
+ }
1050
+ `;
1051
+ EOF
1052
+ cat <<'EOF' > src/components/card/GenericCard.ts
1053
+ import { html, LitElement } from "lit";
1054
+ import { GenericCardStyles } from "./generic-card.styles";
1055
+
1056
+ export class GenericCard extends LitElement {
1057
+ static styles = [GenericCardStyles];
1058
+ render() {
1059
+ return html`<div class="card-wrapper"><slot></slot></div>`;
1060
+ }
1061
+ }
1062
+ EOF
1063
+ cat <<'EOF' > src/components/card/generic-card.ts
1064
+ import { GenericCard } from "./GenericCard";
1065
+ customElements.define("generic-card", GenericCard);
1066
+ EOF
1067
+ cat <<'EOF' > src/components/card/vite-logo.svg.ts
1068
+ import type { SVGTemplateResult } from "lit";
1069
+ import { svg } from "lit";
1070
+
1071
+ export const ViteLogo: SVGTemplateResult = svg`<svg xmlns="http://www.w3.org/2000/svg" width="31.88" height="32" viewBox="0 0 256 257"><path fill="#41D1FF" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path></svg>`;
1072
+ EOF
1073
+ fi
1074
+
1075
+ if [ "$ENABLE_CHARTS" = true ]; then
1076
+ mkdir -p src/components/chart-js
1077
+ cat <<'EOF' > src/components/chart-js/chart-js.utility.ts
1078
+ import colorLib, { type Color, type RGBA } from "@kurkle/color";
1079
+ import { DateTime } from "luxon";
1080
+ import "chartjs-adapter-luxon";
1081
+
1082
+ var _seed = Date.now();
1083
+ export function srand(seed: number) { _seed = seed; }
1084
+ export function valueOrDefault<T>(value: T | undefined, defaultValue: T) {
1085
+ return typeof value === "undefined" ? defaultValue : value;
1086
+ }
1087
+ export function rand(min?: number, max?: number) {
1088
+ min = valueOrDefault(min, 0); max = valueOrDefault(max, 0);
1089
+ _seed = (_seed * 9301 + 49297) % 233280;
1090
+ return min + (_seed / 233280) * (max - min);
1091
+ }
1092
+ export function months(config: Record<string, unknown>) {
1093
+ const cfg = config || {}; const count = (cfg.count as number) || 12;
1094
+ const MONTHS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
1095
+ const values = [];
1096
+ for (let i = 0; i < count; ++i) { values.push(MONTHS[Math.ceil(i) % 12]); }
1097
+ return values;
1098
+ }
1099
+ EOF
1100
+
1101
+ cat <<'EOF' > src/components/chart-js/chart-types.interface.ts
1102
+ import type { ChartConfiguration } from "chart.js/auto";
1103
+ import * as Utils from "./chart-js.utility";
1104
+
1105
+ const barLabels = Utils.months({ count: 7 });
1106
+ export const SAMPLE_BAR_DATA: ChartConfiguration = {
1107
+ type: "bar",
1108
+ data: {
1109
+ labels: barLabels,
1110
+ datasets: [{ label: "Sample Dataset", data: [65, 59, 80, 81, 56, 55, 40], backgroundColor: "rgba(54, 162, 235, 0.5)" }]
1111
+ }
1112
+ };
1113
+ export const SAMPLE_PIE_DATA: ChartConfiguration = {
1114
+ type: "pie",
1115
+ data: { labels: ["Red", "Blue", "Yellow"], datasets: [{ data: [300, 50, 100], backgroundColor: ["rgb(255, 99, 132)", "rgb(54, 162, 235)", "rgb(255, 205, 86)"] }] }
1116
+ };
1117
+ export const SAMPLE_DOUGHNUT_DATA: ChartConfiguration = {
1118
+ type: "doughnut",
1119
+ data: { labels: ["Red", "Blue", "Yellow"], datasets: [{ data: [300, 50, 100], backgroundColor: ["rgb(255, 99, 132)", "rgb(54, 162, 235)", "rgb(255, 205, 86)"] }] }
1120
+ };
1121
+ EOF
1122
+
1123
+ cat <<'EOF' > src/components/chart-js/ChartJs.ts
1124
+ import { Chart, type ChartConfiguration, type ChartItem, type ChartType } from "chart.js/auto";
1125
+ import { type HTMLTemplateResult, html, LitElement, type PropertyValues } from "lit";
1126
+ import { property, query, state } from "lit/decorators.js";
1127
+ import { SAMPLE_BAR_DATA, SAMPLE_DOUGHNUT_DATA, SAMPLE_PIE_DATA } from "./chart-types.interface";
1128
+
1129
+ export class ChartJsComponent extends LitElement {
1130
+ @property({ type: String, attribute: "chart-type" }) chartType: ChartType | undefined;
1131
+ @property({ type: Object, attribute: "chart-config" }) config: ChartConfiguration | undefined;
1132
+ @state() chart: Chart | undefined;
1133
+ @query("canvas") canvasElement: HTMLCanvasElement | undefined;
1134
+
1135
+ protected firstUpdated(_changedProperties: PropertyValues): void {
1136
+ const ctx = this.canvasElement?.getContext("2d") as ChartItem;
1137
+ let data = this.config;
1138
+ if (!data) {
1139
+ switch (this.chartType) {
1140
+ case "bar": data = { ...SAMPLE_BAR_DATA }; break;
1141
+ case "pie": data = { ...SAMPLE_PIE_DATA }; break;
1142
+ case "doughnut": data = { ...SAMPLE_DOUGHNUT_DATA }; break;
1143
+ default: break;
1144
+ }
1145
+ }
1146
+ this.chart = new Chart(ctx, { ...data } as ChartConfiguration);
1147
+ }
1148
+
1149
+ render(): HTMLTemplateResult { return html`<canvas></canvas>`; }
1150
+ }
1151
+ EOF
1152
+
1153
+ cat <<'EOF' > src/components/chart-js/chart-js.ts
1154
+ import { ChartJsComponent } from "./ChartJs";
1155
+ if (!customElements.get("base-chart")) {
1156
+ customElements.define("base-chart", ChartJsComponent);
1157
+ }
1158
+ EOF
1159
+ fi
1160
+
1161
+ if [ "$ENABLE_TODOS" = true ]; then
1162
+ mkdir -p src/components/todos/todo-item
1163
+ cat <<'EOF' > src/components/todos/todo-item/todo-item.styles.ts
1164
+ import { css } from "lit";
1165
+ export const TodoItemStyles = css`
1166
+ :host { display: block; padding: 0.5rem; border-bottom: 1px solid #eee; }
1167
+ .todo-item { display: flex; align-items: center; gap: 1rem; }
1168
+ .todo-item.checked .title { text-decoration: line-through; color: #888; }
1169
+ .content { flex: 1; display: flex; flex-direction: column; }
1170
+ .title { font-weight: bold; }
1171
+ .description { font-size: 0.85rem; color: #666; }
1172
+ button { background-color: #ff4d4d; color: white; border: none; padding: 0.25rem 0.5rem; border-radius: 4px; cursor: pointer; }
1173
+ button:hover { background-color: #cc0000; }
1174
+ `;
1175
+ EOF
1176
+
1177
+ cat <<'EOF' > src/components/todos/todo-item/TodoItem.ts
1178
+ import { html, LitElement } from "lit";
1179
+ import { property } from "lit/decorators.js";
1180
+ import { removeTodo, toggleTodo } from "../../../shared/stores/todo.store";
1181
+ import { removeIdFromOrder } from "../../../shared/stores/todo-list.store";
1182
+ import { TodoItemStyles } from "./todo-item.styles";
1183
+
1184
+ export class TodoItem extends LitElement {
1185
+ @property({ type: String }) id = "";
1186
+ @property({ type: String, attribute: "item-title" }) title = "";
1187
+ @property({ type: String, attribute: "item-description" }) description = "";
1188
+ @property({ type: Boolean, attribute: "is-checked" }) isChecked = false;
1189
+
1190
+ static styles = [TodoItemStyles];
1191
+
1192
+ onChecked() { toggleTodo(this.id); }
1193
+ onDelete() { removeTodo(this.id); removeIdFromOrder(this.id); }
1194
+
1195
+ render() {
1196
+ return html`
1197
+ <div class="todo-item ${this.isChecked ? "checked" : ""}">
1198
+ <input type="checkbox" .checked="${this.isChecked}" @change="${this.onChecked}" />
1199
+ <div class="content">
1200
+ <span class="title">${this.title}</span>
1201
+ ${this.description ? html`<span class="description">${this.description}</span>` : ""}
1202
+ </div>
1203
+ <button @click="${this.onDelete}">Delete</button>
1204
+ </div>
1205
+ `;
1206
+ }
1207
+ }
1208
+ EOF
1209
+
1210
+ cat <<'EOF' > src/components/todos/todo-item/todo-item.ts
1211
+ import { TodoItem } from "./TodoItem";
1212
+ customElements.define("todo-item", TodoItem);
1213
+ EOF
1214
+
1215
+ mkdir -p src/components/todos/todo-list
1216
+ cat <<'EOF' > src/components/todos/todo-list/todo-list.styles.ts
1217
+ import { css } from "lit";
1218
+ export const TodoListStyles = css`
1219
+ :host { display: block; font-family: sans-serif; }
1220
+ .todo-list-container { max-width: 500px; margin: 0 auto; background: var(--bg-color); border-radius: 8px; box-shadow: var(--primary-box-shadow); padding: 1.5rem; }
1221
+ header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem; }
1222
+ h2 { margin: 0; color: var(--primary-text-color); }
1223
+ .sort-btn { background: #6c757d; color: white; border: none; padding: 0.4rem 0.8rem; border-radius: 4px; cursor: pointer; }
1224
+ .input-area { display: flex; gap: 0.5rem; margin-bottom: 1.5rem; }
1225
+ input { flex: 1; padding: 0.6rem; border: 1px solid #ced4da; border-radius: 4px; }
1226
+ button { padding: 0.6rem 1.2rem; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; }
1227
+ .list { border-top: 1px solid #dee2e6; }
1228
+ `;
1229
+ EOF
1230
+
1231
+ cat <<'EOF' > src/components/todos/todo-list/TodoList.ts
1232
+ import { SignalWatcher } from "@lit-labs/preact-signals";
1233
+ import { type CSSResultOrNative, html, LitElement } from "lit";
1234
+ import { query } from "lit/decorators.js";
1235
+ import { repeat } from "lit/directives/repeat.js";
1236
+ import { addTodo } from "../../../shared/stores/todo.store";
1237
+ import { addIdToOrder, sortDirectionSignal, sortedTodosSignal, toggleSortDirection } from "../../../shared/stores/todo-list.store";
1238
+ import "../todo-item/todo-item";
1239
+ import { TodoListStyles } from "./todo-list.styles";
1240
+
1241
+ export class TodoList extends SignalWatcher(LitElement) {
1242
+ static styles: CSSResultOrNative[] = [TodoListStyles];
1243
+
1244
+ @query("#todo-input") inputElement!: HTMLInputElement;
1245
+
1246
+ private _onAddTodo() {
1247
+ const title = this.inputElement.value.trim();
1248
+ if (title) {
1249
+ const id = addTodo(title);
1250
+ addIdToOrder(id);
1251
+ this.inputElement.value = "";
1252
+ }
1253
+ }
1254
+
1255
+ private _toggleSort() { toggleSortDirection(); }
1256
+
1257
+ render() {
1258
+ return html`
1259
+ <div class="todo-list-container">
1260
+ <header>
1261
+ <h2>Todos (Signals & IndexedDB)</h2>
1262
+ <button class="sort-btn" @click="${this._toggleSort}">
1263
+ Sort: ${sortDirectionSignal.value.toUpperCase()}
1264
+ </button>
1265
+ </header>
1266
+
1267
+ <div class="input-area">
1268
+ <input id="todo-input" type="text" placeholder="Add a new todo..." />
1269
+ <button @click="${this._onAddTodo}">Add</button>
1270
+ </div>
1271
+
1272
+ <div class="list">
1273
+ ${repeat(
1274
+ sortedTodosSignal.value,
1275
+ (item) => item.id,
1276
+ (item) => html`
1277
+ <todo-item
1278
+ .id="${item.id}"
1279
+ .title="${item.title}"
1280
+ .description="${item.description || ""}"
1281
+ .isChecked="${item.checked}"
1282
+ ></todo-item>
1283
+ `,
1284
+ )}
1285
+ </div>
1286
+ </div>
1287
+ `;
1288
+ }
1289
+ }
1290
+ EOF
1291
+
1292
+ cat <<'EOF' > src/components/todos/todo-list/todo-list.ts
1293
+ import { TodoList } from "./TodoList";
1294
+ customElements.define("todo-list", TodoList);
1295
+ EOF
1296
+ fi
1297
+
1298
+ # ------------------------------------------------------------------------------
1299
+ # Create Views
1300
+ # ------------------------------------------------------------------------------
1301
+ log_info "Generating Views & ViewMixin..."
1302
+ mkdir -p src/views
1303
+
1304
+ if [ "$ENABLE_ROUTER" = true ]; then
1305
+ cat <<'EOF' > src/views/view.mixin.ts
1306
+ import { consume } from "@lit/context";
1307
+ import { type HTMLTemplateResult, html, type LitElement } from "lit";
1308
+ import { property, state } from "lit/decorators.js";
1309
+ import { AccessesContext } from "../shared/contexts/accesses.context";
1310
+ import { MfeLoaderContext } from "../shared/contexts/mfe-loader.context";
1311
+ import { NavigationContext } from "../shared/contexts/navigation.context";
1312
+ import type { NavItem } from "../shared/interfaces/navigation.interface";
1313
+ import type { MfeLoader } from "../shared/utilities/mfe-loader.utility";
1314
+
1315
+ type Constructor<T = {}> = new (...args: any[]) => T;
1316
+
1317
+ export declare class ViewMixinInterface {
1318
+ navItems: NavItem[];
1319
+ mfeLoader: MfeLoader | undefined;
1320
+ tagName: string;
1321
+ componentData: NavItem;
1322
+ featureIsEnabled: boolean;
1323
+ isMfe: boolean;
1324
+ renderUnderConstruction(): HTMLTemplateResult;
1325
+ renderMfe(customTemplate?: HTMLTemplateResult): HTMLTemplateResult;
1326
+ }
1327
+
1328
+ export const ViewMixin = <T extends Constructor<LitElement>>(superClass: T) => {
1329
+ class ViewMixinClass extends superClass {
1330
+ @consume({ context: NavigationContext, subscribe: true })
1331
+ @property({ type: Array }) navItems: NavItem[] = [];
1332
+
1333
+ @consume({ context: AccessesContext, subscribe: true })
1334
+ @state() accesses: string[] = [];
1335
+
1336
+ @consume({ context: MfeLoaderContext, subscribe: true })
1337
+ @state() mfeLoader: MfeLoader | undefined;
1338
+
1339
+ @state() tagName = "";
1340
+ @state() featureIsEnabled = false;
1341
+ isMfe = false;
1342
+
1343
+ get componentData(): NavItem {
1344
+ if (this.isMfe) {
1345
+ return (
1346
+ this.navItems.find(
1347
+ (item: NavItem) => item.mfeComponent?.tagName === this.tagName,
1348
+ ) ?? ({} as NavItem)
1349
+ );
1350
+ }
1351
+ return (
1352
+ this.navItems.find((item: NavItem) => item.tagName === this.tagName) ??
1353
+ ({} as NavItem)
1354
+ );
1355
+ }
1356
+
1357
+ connectedCallback(): void {
1358
+ super.connectedCallback();
1359
+ this.featureIsEnabled = true;
1360
+ this.mfeLoader?.init();
1361
+ }
1362
+
1363
+ renderUnderConstruction(): HTMLTemplateResult {
1364
+ const hasPermission = this.componentData?.userHasPermission ?? true;
1365
+ return !hasPermission
1366
+ ? html`<no-access></no-access>`
1367
+ : html`<under-construction></under-construction>`;
1368
+ }
1369
+
1370
+ renderMfe(customTemplate?: HTMLTemplateResult): HTMLTemplateResult {
1371
+ const hasPermission = this.componentData?.userHasPermission ?? true;
1372
+ if (!hasPermission) {
1373
+ return this.renderUnderConstruction();
1374
+ }
1375
+
1376
+ if (customTemplate) return html`${customTemplate}`;
1377
+
1378
+ return html`${
1379
+ this.featureIsEnabled && this.isMfe
1380
+ ? html`<div id="mfe-container-${this.tagName}"></div>`
1381
+ : this.renderUnderConstruction()
1382
+ }`;
1383
+ }
1384
+
1385
+ protected render(): HTMLTemplateResult { return html`${this.renderMfe()}`; }
1386
+ }
1387
+ return ViewMixinClass as unknown as Constructor<ViewMixinInterface> & T;
1388
+ };
1389
+ EOF
1390
+
1391
+ mkdir -p src/views/home-page
1392
+ cat <<'EOF' > src/views/home-page/home-page.ts
1393
+ import { type HTMLTemplateResult, html, LitElement } from "lit";
1394
+ import { customElement } from "lit/decorators.js";
1395
+ import { ViewMixin } from "../view.mixin";
1396
+
1397
+ @customElement("home-page")
1398
+ export class HomePage extends ViewMixin(LitElement) {
1399
+ tagName = "home-page";
1400
+
1401
+ render(): HTMLTemplateResult {
1402
+ return html`
1403
+ <div style="padding: 2rem;">
1404
+ <h1>Welcome to your Lit App Shell!</h1>
1405
+ <p>Your modular application structure is initialized and ready for development.</p>
1406
+ </div>
1407
+ `;
1408
+ }
1409
+ }
1410
+ EOF
1411
+
1412
+ if [ "$ENABLE_TODOS" = true ]; then
1413
+ mkdir -p src/views/todos-page
1414
+ cat <<'EOF' > src/views/todos-page/todos-page.styles.ts
1415
+ import { css } from "lit";
1416
+ export const TodosPageStyles = css``;
1417
+ EOF
1418
+
1419
+ cat <<'EOF' > src/views/todos-page/todos-page.ts
1420
+ import { type HTMLTemplateResult, html, LitElement } from "lit";
1421
+ import { customElement } from "lit/decorators.js";
1422
+ import { ViewMixin } from "../view.mixin";
1423
+ import { TodosPageStyles } from "./todos-page.styles";
1424
+ import "../../components/todos/todo-list/todo-list";
1425
+
1426
+ @customElement("todos-page")
1427
+ export class TodosPage extends ViewMixin(LitElement) {
1428
+ featureIsEnabled = true;
1429
+ isMfe = false;
1430
+ static styles = [TodosPageStyles];
1431
+
1432
+ render(): HTMLTemplateResult {
1433
+ return this.renderMfe(html`<todo-list></todo-list>`);
1434
+ }
1435
+ }
1436
+ EOF
1437
+ fi
1438
+
1439
+ if [ "$ENABLE_CARDS" = true ]; then
1440
+ mkdir -p src/views/card-examples
1441
+ cat <<'EOF' > src/views/card-examples/card-page.styles.ts
1442
+ import { css } from "lit";
1443
+ export const CardPageStyles = css`
1444
+ .card-container { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem; padding: 1rem; }
1445
+ `;
1446
+ EOF
1447
+ cat <<'EOF' > src/views/card-examples/card-examples.ts
1448
+ import { type HTMLTemplateResult, html, LitElement } from "lit";
1449
+ import { customElement } from "lit/decorators.js";
1450
+ import { ViewMixin } from "../view.mixin";
1451
+ import { CardPageStyles } from "./card-page.styles";
1452
+ import "../../components/card/generic-card";
1453
+
1454
+ @customElement("card-examples")
1455
+ export class CardExamples extends ViewMixin(LitElement) {
1456
+ featureIsEnabled = true;
1457
+ isMfe = false;
1458
+ static styles = [CardPageStyles];
1459
+
1460
+ render(): HTMLTemplateResult {
1461
+ return this.renderMfe(
1462
+ html`<div class="card-container">
1463
+ <generic-card>
1464
+ <div slot="card-title"><h2>Generic Card</h2></div>
1465
+ This is a sample card component.
1466
+ </generic-card>
1467
+ </div>`,
1468
+ );
1469
+ }
1470
+ }
1471
+ EOF
1472
+ fi
1473
+
1474
+ if [ "$ENABLE_CHARTS" = true ]; then
1475
+ mkdir -p src/views/chart-examples
1476
+ cat <<'EOF' > src/views/chart-examples/chart-examples.ts
1477
+ import { css, type HTMLTemplateResult, html, LitElement } from "lit";
1478
+ import { customElement } from "lit/decorators.js";
1479
+ import { ViewMixin } from "../view.mixin";
1480
+ import "../../components/chart-js/chart-js";
1481
+
1482
+ @customElement("chart-examples")
1483
+ export class ChartExamples extends ViewMixin(LitElement) {
1484
+ featureIsEnabled = true;
1485
+ isMfe = false;
1486
+
1487
+ static styles = [
1488
+ css`.pie-container { display: flex; gap: 2rem; flex-wrap: wrap; }`,
1489
+ ];
1490
+
1491
+ render(): HTMLTemplateResult {
1492
+ return this.renderMfe(
1493
+ html`
1494
+ <h2>Basic Bar Chart</h2>
1495
+ <base-chart chart-type="bar"></base-chart>
1496
+ <h2>Pie & Doughnut Charts</h2>
1497
+ <div class="pie-container">
1498
+ <base-chart chart-type="pie"></base-chart>
1499
+ <base-chart chart-type="doughnut"></base-chart>
1500
+ </div>`,
1501
+ );
1502
+ }
1503
+ }
1504
+ EOF
1505
+ fi
1506
+
1507
+ if [ "$ENABLE_MFE_LOADER" = true ]; then
1508
+ mkdir -p src/views/vite-mfe
1509
+ cat <<'EOF' > src/views/vite-mfe/vite-mfe.ts
1510
+ import { type HTMLTemplateResult, html, LitElement } from "lit";
1511
+ import { customElement } from "lit/decorators.js";
1512
+ import { ViewMixin } from "../view.mixin";
1513
+
1514
+ @customElement("vite-mfe")
1515
+ export class ViteMfe extends ViewMixin(LitElement) {
1516
+ tagName = "coffee-users";
1517
+ featureIsEnabled = true;
1518
+ isMfe = true;
1519
+
1520
+ render(): HTMLTemplateResult {
1521
+ return html`${this.renderMfe()}`;
1522
+ }
1523
+ }
1524
+ EOF
1525
+ fi
1526
+
1527
+ fi
1528
+
1529
+ # ------------------------------------------------------------------------------
1530
+ # Create src/app-shell.ts & src/app-shell.styles.ts
1531
+ # ------------------------------------------------------------------------------
1532
+ log_info "Generating App Shell root component..."
1533
+
1534
+ cat <<'EOF' > src/app-shell.styles.ts
1535
+ import { css } from "lit";
1536
+ export const AppShellStyles = css`
1537
+ :host { display: block; min-height: 100vh; }
1538
+ main { padding: 1rem; }
1539
+ `;
1540
+ EOF
1541
+
1542
+ IMPORTS="import { type HTMLTemplateResult, html, LitElement } from 'lit';
1543
+ import { customElement, property, state } from 'lit/decorators.js';
1544
+ import { AppShellStyles } from './app-shell.styles';
1545
+ import type { NavItem } from './shared/interfaces/navigation.interface';"
1546
+
1547
+ if [ "$ENABLE_CONTEXT" = true ]; then
1548
+ IMPORTS="$IMPORTS
1549
+ import { provide } from '@lit/context';
1550
+ import { AccessesContext } from './shared/contexts/accesses.context';
1551
+ import { NavigationContext } from './shared/contexts/navigation.context';
1552
+ import { MfeLoaderContext } from './shared/contexts/mfe-loader.context';"
1553
+ fi
1554
+
1555
+ if [ "$ENABLE_ROUTER" = true ]; then
1556
+ IMPORTS="$IMPORTS
1557
+ import { Router } from '@lit-labs/router';
1558
+ import { html as staticHtml, unsafeStatic } from 'lit/static-html.js';
1559
+ import 'urlpattern-polyfill';
1560
+ import { navigationRouting } from './shared/configuration/nav';
1561
+ import { routesBuilt } from './shared/configuration/routes';
1562
+ import { withBase } from './shared/configuration/base-path';"
1563
+ fi
1564
+
1565
+ if [ "$ENABLE_HEADER" = true ]; then
1566
+ IMPORTS="$IMPORTS
1567
+ import './components/header/app-shell-header';"
1568
+ fi
1569
+
1570
+ if [ "$ENABLE_MFE_LOADER" = true ]; then
1571
+ IMPORTS="$IMPORTS
1572
+ import { MFE_LOADER_CONFIG } from './shared/configuration/mfes';
1573
+ import { MfeLoader } from './shared/utilities/mfe-loader.utility';"
1574
+ fi
1575
+
1576
+ cat <<EOF > src/app-shell.ts
1577
+ $IMPORTS
1578
+
1579
+ @customElement("app-shell")
1580
+ export class AppShell extends LitElement {
1581
+ EOF
1582
+
1583
+ if [ "$ENABLE_CONTEXT" = true ]; then
1584
+ cat <<EOF >> src/app-shell.ts
1585
+ @provide({ context: NavigationContext })
1586
+ @property({ type: Array })
1587
+ routing: NavItem[] = [];
1588
+
1589
+ @provide({ context: AccessesContext })
1590
+ @property({ type: Array })
1591
+ accesses: string[] = ["public"];
1592
+ EOF
1593
+ if [ "$ENABLE_MFE_LOADER" = true ]; then
1594
+ cat <<EOF >> src/app-shell.ts
1595
+ @provide({ context: MfeLoaderContext })
1596
+ @state()
1597
+ mfeLoader = new MfeLoader(MFE_LOADER_CONFIG);
1598
+ EOF
1599
+ fi
1600
+ fi
1601
+
1602
+ if [ "$ENABLE_ROUTER" = true ]; then
1603
+ cat <<EOF >> src/app-shell.ts
1604
+ private _router = new Router(this, []);
1605
+ EOF
1606
+ fi
1607
+
1608
+ cat <<EOF >> src/app-shell.ts
1609
+
1610
+ static styles = [AppShellStyles];
1611
+
1612
+ async connectedCallback(): Promise<void> {
1613
+ super.connectedCallback();
1614
+ EOF
1615
+
1616
+ if [ "$ENABLE_ROUTER" = true ]; then
1617
+ cat <<EOF >> src/app-shell.ts
1618
+ await this._setupRoutes();
1619
+ EOF
1620
+ fi
1621
+
1622
+ cat <<EOF >> src/app-shell.ts
1623
+ }
1624
+ EOF
1625
+
1626
+ if [ "$ENABLE_ROUTER" = true ]; then
1627
+ cat <<EOF >> src/app-shell.ts
1628
+
1629
+ private async _setupRoutes(): Promise<void> {
1630
+ this.routing = routesBuilt(navigationRouting, ["public"]);
1631
+ const routeConfigs = this.routing.map((navItem) => ({
1632
+ path: withBase(navItem.path),
1633
+ enter: async () => {
1634
+ await import(\`./views/\${navItem.directory}/\${navItem.component}.ts\`);
1635
+ return true;
1636
+ },
1637
+ render: () => {
1638
+ const tag = unsafeStatic(navItem.tagName);
1639
+ return staticHtml\`<\${tag}></\${tag}>\`;
1640
+ },
1641
+ }));
1642
+
1643
+ const firstPath = navigationRouting[0]?.path || "/home";
1644
+ const firstItem = navigationRouting[0];
1645
+
1646
+ this._router.routes = [
1647
+ {
1648
+ path: withBase("/"),
1649
+ enter: async () => {
1650
+ window.history.replaceState({}, "", withBase(firstPath));
1651
+ if (firstItem) {
1652
+ await import(\`./views/\${firstItem.directory}/\${firstItem.component}.ts\`);
1653
+ }
1654
+ return true;
1655
+ },
1656
+ render: () => {
1657
+ const tag = unsafeStatic(firstItem?.tagName || "home-page");
1658
+ return staticHtml\`<\${tag}></\${tag}>\`;
1659
+ },
1660
+ },
1661
+ ...routeConfigs,
1662
+ ];
1663
+
1664
+ const currentPath = window.location.pathname + window.location.search + window.location.hash;
1665
+ await this._router.goto(currentPath);
1666
+ }
1667
+ EOF
1668
+ fi
1669
+
1670
+ cat <<EOF >> src/app-shell.ts
1671
+
1672
+ render(): HTMLTemplateResult {
1673
+ return html\`
1674
+ EOF
1675
+
1676
+ if [ "$ENABLE_HEADER" = true ]; then
1677
+ cat <<EOF >> src/app-shell.ts
1678
+ <app-shell-header .routes="\${this.routing}" enable-theme-switcher>
1679
+ <main>
1680
+ EOF
1681
+ else
1682
+ cat <<EOF >> src/app-shell.ts
1683
+ <main>
1684
+ EOF
1685
+ fi
1686
+
1687
+ if [ "$ENABLE_ROUTER" = true ]; then
1688
+ cat <<EOF >> src/app-shell.ts
1689
+ \${this._router.outlet()}
1690
+ EOF
1691
+ else
1692
+ cat <<EOF >> src/app-shell.ts
1693
+ <h2>Welcome to $APP_NAME</h2>
1694
+ EOF
1695
+ fi
1696
+
1697
+ cat <<EOF >> src/app-shell.ts
1698
+ </main>
1699
+ EOF
1700
+
1701
+ if [ "$ENABLE_HEADER" = true ]; then
1702
+ cat <<EOF >> src/app-shell.ts
1703
+ </app-shell-header>
1704
+ EOF
1705
+ fi
1706
+
1707
+ cat <<EOF >> src/app-shell.ts
1708
+ \`;
1709
+ }
1710
+ }
1711
+
1712
+ declare global {
1713
+ interface HTMLElementTagNameMap {
1714
+ "app-shell": AppShell;
1715
+ }
1716
+ }
1717
+ EOF
1718
+
1719
+ # ------------------------------------------------------------------------------
1720
+ # Post-Generation Steps
1721
+ # ------------------------------------------------------------------------------
1722
+ if [ "$INIT_GIT" = true ]; then
1723
+ log_info "Initializing git repository..."
1724
+ git init -q
1725
+ log_success "Git repository initialized."
1726
+ fi
1727
+
1728
+ if [ "$RUN_INSTALL" = true ]; then
1729
+ log_info "Installing dependencies with $PACKAGE_MANAGER..."
1730
+ $PACKAGE_MANAGER install
1731
+ log_success "Dependencies installed."
1732
+ fi
1733
+
1734
+ log_title "App Shell Scaffolding Complete!"
1735
+ echo -e "${GREEN}Project successfully created at:${RESET} ${BOLD}$TARGET_DIR${RESET}\n"
1736
+ echo -e "Next steps:"
1737
+ echo -e " cd ${BOLD}$APP_NAME${RESET}"
1738
+ if [ "$RUN_INSTALL" = false ]; then
1739
+ echo -e " ${BOLD}$PACKAGE_MANAGER install${RESET}"
1740
+ fi
1741
+ echo -e " ${BOLD}$PACKAGE_MANAGER start${RESET}\n"