@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.
- package/README.md +73 -0
- package/create-app-shell.sh +1741 -0
- package/man/create-lit-app.1 +74 -0
- package/package.json +38 -0
|
@@ -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"
|