Compare commits
1 Commits
Features/F
...
3041fa4d0f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3041fa4d0f |
@@ -1,13 +0,0 @@
|
|||||||
.DS_Store
|
|
||||||
.git
|
|
||||||
.gitignore
|
|
||||||
|
|
||||||
**/.pnpm-store
|
|
||||||
**/.output
|
|
||||||
**/dist
|
|
||||||
**/node_modules
|
|
||||||
|
|
||||||
Commands
|
|
||||||
Docker
|
|
||||||
Documentation
|
|
||||||
Env
|
|
||||||
@@ -1,32 +1,11 @@
|
|||||||
project_root := justfile_directory()
|
project_root := justfile_directory()
|
||||||
proxy_bake := project_root + "/Proxy/docker-bake.hcl"
|
frontend_dir := project_root + "/Frontend"
|
||||||
local_compose := project_root + "/Docker/docker-compose.local.prod.yaml"
|
frontend_bake := project_root + "/Frontend/docker-bake.hcl"
|
||||||
|
|
||||||
# Build the local production proxy image locally.
|
# Build the Frontend production image locally.
|
||||||
build:
|
build:
|
||||||
cd '{{project_root}}' && docker buildx bake -f '{{proxy_bake}}' prod
|
cd '{{frontend_dir}}' && docker buildx bake -f '{{frontend_bake}}' prod
|
||||||
|
|
||||||
# Start the local production stack in the background using the current image.
|
# Rebuild the Frontend production image locally.
|
||||||
up:
|
|
||||||
docker compose -f '{{local_compose}}' up -d --remove-orphans --force-recreate
|
|
||||||
|
|
||||||
# Build first, then start the local production stack in the background.
|
|
||||||
start: build
|
|
||||||
docker compose -f '{{local_compose}}' up -d --remove-orphans --force-recreate
|
|
||||||
|
|
||||||
# Rebuild the local production proxy image locally.
|
|
||||||
rebuild:
|
rebuild:
|
||||||
cd '{{project_root}}' && docker buildx bake -f '{{proxy_bake}}' --set '*.no-cache=true' prod
|
cd '{{frontend_dir}}' && docker buildx bake -f '{{frontend_bake}}' --set '*.no-cache=true' prod
|
||||||
docker compose -f '{{local_compose}}' up -d --remove-orphans --force-recreate
|
|
||||||
|
|
||||||
# Stop and remove the local production stack.
|
|
||||||
down:
|
|
||||||
docker compose -f '{{local_compose}}' down --remove-orphans --volumes
|
|
||||||
|
|
||||||
# Follow logs for the local production stack.
|
|
||||||
logs:
|
|
||||||
docker compose -f '{{local_compose}}' logs -f
|
|
||||||
|
|
||||||
# Restart the local production stack.
|
|
||||||
restart:
|
|
||||||
docker compose -f '{{local_compose}}' restart
|
|
||||||
|
|||||||
@@ -1,7 +1 @@
|
|||||||
services:
|
# Reserved for a future local production compose stack.
|
||||||
proxy:
|
|
||||||
image: moku/work-proxy:local-prod
|
|
||||||
container_name: moku-work-proxy-local
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "8080:80"
|
|
||||||
|
|||||||
@@ -11,19 +11,7 @@
|
|||||||
- [ ] Project-Structure
|
- [ ] Project-Structure
|
||||||
- [ ] Stack-Decisions
|
- [ ] Stack-Decisions
|
||||||
- [ ] Proxy
|
- [ ] Proxy
|
||||||
- [ ] Local-Prod-NGINX-Proxy
|
|
||||||
- [ ] Static-Frontend-Serving
|
|
||||||
- [ ] First-Request-Web-Loader
|
|
||||||
- [ ] Bootstrap-Document
|
|
||||||
- [ ] Route-Intent-Handoff
|
|
||||||
- [ ] Tiny-First-Paint-Budget
|
|
||||||
- [ ] Dev-and-Prod-Builds
|
- [ ] Dev-and-Prod-Builds
|
||||||
- [x] Local-Dev-Just-Commands
|
|
||||||
- [x] Local-Dev-Docker-Compose
|
|
||||||
- [ ] Local-Prod-Just-Commands
|
|
||||||
- [ ] Local-Prod-Docker-Compose
|
|
||||||
- [ ] Frontend-Production-Dockerfile
|
|
||||||
- [ ] Frontend-docker-bake
|
|
||||||
|
|
||||||
#### Backend
|
#### Backend
|
||||||
|
|
||||||
@@ -49,12 +37,12 @@
|
|||||||
- [x] Foundation
|
- [x] Foundation
|
||||||
- [x] Typography
|
- [x] Typography
|
||||||
- [x] Icons
|
- [x] Icons
|
||||||
- [ ] App Shell
|
|
||||||
- [ ] Primitives
|
- [ ] Primitives
|
||||||
- [ ] Button
|
- [ ] Button
|
||||||
- [ ] IconButton
|
- [ ] IconButton
|
||||||
- [ ] Input
|
- [ ] Input
|
||||||
- [ ] Surface
|
- [ ] Surface
|
||||||
|
- [ ] App-Shell
|
||||||
- [ ] Nav-Bar
|
- [ ] Nav-Bar
|
||||||
- [ ] Workspace-Switching
|
- [ ] Workspace-Switching
|
||||||
- [ ] Workspace-Home
|
- [ ] Workspace-Home
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
# syntax=docker/dockerfile:1.7
|
# syntax=docker/dockerfile:1.7
|
||||||
|
|
||||||
# Frontend development image only.
|
|
||||||
# Production static serving is owned by Proxy/Local/Dockerfile.
|
|
||||||
|
|
||||||
FROM node:22-alpine AS base
|
FROM node:22-alpine AS base
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@@ -19,11 +16,27 @@ FROM dependencies AS development
|
|||||||
ENV NODE_ENV=development
|
ENV NODE_ENV=development
|
||||||
ENV PORT=3333
|
ENV PORT=3333
|
||||||
|
|
||||||
ARG ALLOWED_HOSTS
|
|
||||||
ENV ALLOWED_HOSTS=${ALLOWED_HOSTS}
|
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
EXPOSE 3333
|
EXPOSE 3333
|
||||||
|
|
||||||
CMD ["pnpm", "dev", "--host", "0.0.0.0", "--port", "3333"]
|
CMD ["pnpm", "dev", "--host", "0.0.0.0", "--port", "3333"]
|
||||||
|
|
||||||
|
FROM dependencies AS build
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN pnpm build
|
||||||
|
|
||||||
|
FROM base AS production
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV PORT=3333
|
||||||
|
|
||||||
|
COPY --from=build /app /app
|
||||||
|
|
||||||
|
EXPOSE 3333
|
||||||
|
|
||||||
|
CMD ["pnpm", "start", "--host", "0.0.0.0", "--port", "3333"]
|
||||||
|
|||||||
@@ -17,18 +17,30 @@ target "dev" {
|
|||||||
tags = ["moku/work-frontend:dev"]
|
tags = ["moku/work-frontend:dev"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
target "prod" {
|
||||||
|
inherits = ["_app"]
|
||||||
|
target = "production"
|
||||||
|
tags = ["moku/work-frontend:prod"]
|
||||||
|
}
|
||||||
|
|
||||||
target "dev-image" {
|
target "dev-image" {
|
||||||
inherits = ["_app"]
|
inherits = ["_app"]
|
||||||
target = "development"
|
target = "development"
|
||||||
tags = ["${REGISTRY}/moku/work-frontend:dev-${TAG}"]
|
tags = ["${REGISTRY}/moku/work-frontend:dev-${TAG}"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
target "prod-image" {
|
||||||
|
inherits = ["_app"]
|
||||||
|
target = "production"
|
||||||
|
tags = ["${REGISTRY}/moku/work-frontend:prod-${TAG}"]
|
||||||
|
}
|
||||||
|
|
||||||
group "local" {
|
group "local" {
|
||||||
targets = ["dev"]
|
targets = ["dev", "prod"]
|
||||||
}
|
}
|
||||||
|
|
||||||
group "registry" {
|
group "registry" {
|
||||||
targets = ["dev-image"]
|
targets = ["dev-image", "prod-image"]
|
||||||
}
|
}
|
||||||
|
|
||||||
group "default" {
|
group "default" {
|
||||||
|
|||||||
@@ -11,22 +11,19 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"build:static": "pnpm build && node ./scripts/render-static-index.mjs",
|
"start": "vite start",
|
||||||
"typecheck": "tsc --noEmit",
|
|
||||||
"start": "vite preview",
|
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"lucide-solid": "^0.542.0",
|
||||||
"@solidjs/start": "2.0.0-alpha.2",
|
"@solidjs/start": "2.0.0-alpha.2",
|
||||||
"@solidjs/vite-plugin-nitro-2": "^0.1.0",
|
"@solidjs/vite-plugin-nitro-2": "^0.1.0",
|
||||||
"lucide-solid": "^0.542.0",
|
|
||||||
"postcss": "^8.5.15",
|
"postcss": "^8.5.15",
|
||||||
"sass": "^1.101.0",
|
"sass": "^1.101.0",
|
||||||
"solid-js": "^1.9.5",
|
"solid-js": "^1.9.5",
|
||||||
"vite": "^7.0.0"
|
"vite": "^7.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^25.9.3",
|
|
||||||
"autoprefixer": "^10.5.0",
|
"autoprefixer": "^10.5.0",
|
||||||
"cssnano": "^8.0.2",
|
"cssnano": "^8.0.2",
|
||||||
"postcss-preset-env": "^11.3.0",
|
"postcss-preset-env": "^11.3.0",
|
||||||
|
|||||||
54
Frontend/pnpm-lock.yaml
generated
54
Frontend/pnpm-lock.yaml
generated
@@ -10,10 +10,10 @@ importers:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@solidjs/start':
|
'@solidjs/start':
|
||||||
specifier: 2.0.0-alpha.2
|
specifier: 2.0.0-alpha.2
|
||||||
version: 2.0.0-alpha.2(crossws@0.4.4(srvx@0.11.8))(vite@7.3.1(@types/node@25.9.3)(jiti@2.6.1)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.46.0))
|
version: 2.0.0-alpha.2(crossws@0.4.4(srvx@0.11.8))(vite@7.3.1(jiti@2.6.1)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.46.0))
|
||||||
'@solidjs/vite-plugin-nitro-2':
|
'@solidjs/vite-plugin-nitro-2':
|
||||||
specifier: ^0.1.0
|
specifier: ^0.1.0
|
||||||
version: 0.1.0(vite@7.3.1(@types/node@25.9.3)(jiti@2.6.1)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.46.0))
|
version: 0.1.0(vite@7.3.1(jiti@2.6.1)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.46.0))
|
||||||
lucide-solid:
|
lucide-solid:
|
||||||
specifier: ^0.542.0
|
specifier: ^0.542.0
|
||||||
version: 0.542.0(solid-js@1.9.11)
|
version: 0.542.0(solid-js@1.9.11)
|
||||||
@@ -28,11 +28,8 @@ importers:
|
|||||||
version: 1.9.11
|
version: 1.9.11
|
||||||
vite:
|
vite:
|
||||||
specifier: ^7.0.0
|
specifier: ^7.0.0
|
||||||
version: 7.3.1(@types/node@25.9.3)(jiti@2.6.1)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.46.0)
|
version: 7.3.1(jiti@2.6.1)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.46.0)
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@types/node':
|
|
||||||
specifier: ^25.9.3
|
|
||||||
version: 25.9.3
|
|
||||||
autoprefixer:
|
autoprefixer:
|
||||||
specifier: ^10.5.0
|
specifier: ^10.5.0
|
||||||
version: 10.5.0(postcss@8.5.15)
|
version: 10.5.0(postcss@8.5.15)
|
||||||
@@ -1265,9 +1262,6 @@ packages:
|
|||||||
'@types/micromatch@4.0.10':
|
'@types/micromatch@4.0.10':
|
||||||
resolution: {integrity: sha512-5jOhFDElqr4DKTrTEbnW8DZ4Hz5LRUEmyrGpCMrD/NphYv3nUnaF08xmSLx1rGGnyEs/kFnhiw6dCgcDqMr5PQ==}
|
resolution: {integrity: sha512-5jOhFDElqr4DKTrTEbnW8DZ4Hz5LRUEmyrGpCMrD/NphYv3nUnaF08xmSLx1rGGnyEs/kFnhiw6dCgcDqMr5PQ==}
|
||||||
|
|
||||||
'@types/node@25.9.3':
|
|
||||||
resolution: {integrity: sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==}
|
|
||||||
|
|
||||||
'@types/resolve@1.20.2':
|
'@types/resolve@1.20.2':
|
||||||
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
|
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
|
||||||
|
|
||||||
@@ -3223,9 +3217,6 @@ packages:
|
|||||||
unctx@2.5.0:
|
unctx@2.5.0:
|
||||||
resolution: {integrity: sha512-p+Rz9x0R7X+CYDkT+Xg8/GhpcShTlU8n+cf9OtOEf7zEQsNcCZO1dPKNRDqvUTaq+P32PMMkxWHwfrxkqfqAYg==}
|
resolution: {integrity: sha512-p+Rz9x0R7X+CYDkT+Xg8/GhpcShTlU8n+cf9OtOEf7zEQsNcCZO1dPKNRDqvUTaq+P32PMMkxWHwfrxkqfqAYg==}
|
||||||
|
|
||||||
undici-types@7.24.6:
|
|
||||||
resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==}
|
|
||||||
|
|
||||||
unenv@2.0.0-rc.24:
|
unenv@2.0.0-rc.24:
|
||||||
resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==}
|
resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==}
|
||||||
|
|
||||||
@@ -4485,13 +4476,13 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
solid-js: 1.9.11
|
solid-js: 1.9.11
|
||||||
|
|
||||||
'@solidjs/start@2.0.0-alpha.2(crossws@0.4.4(srvx@0.11.8))(vite@7.3.1(@types/node@25.9.3)(jiti@2.6.1)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.46.0))':
|
'@solidjs/start@2.0.0-alpha.2(crossws@0.4.4(srvx@0.11.8))(vite@7.3.1(jiti@2.6.1)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.46.0))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.0
|
'@babel/core': 7.29.0
|
||||||
'@babel/traverse': 7.29.0
|
'@babel/traverse': 7.29.0
|
||||||
'@babel/types': 7.29.0
|
'@babel/types': 7.29.0
|
||||||
'@solidjs/meta': 0.29.4(solid-js@1.9.11)
|
'@solidjs/meta': 0.29.4(solid-js@1.9.11)
|
||||||
'@tanstack/server-functions-plugin': 1.134.5(vite@7.3.1(@types/node@25.9.3)(jiti@2.6.1)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.46.0))
|
'@tanstack/server-functions-plugin': 1.134.5(vite@7.3.1(jiti@2.6.1)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.46.0))
|
||||||
'@types/babel__traverse': 7.28.0
|
'@types/babel__traverse': 7.28.0
|
||||||
'@types/micromatch': 4.0.10
|
'@types/micromatch': 4.0.10
|
||||||
cookie-es: 2.0.0
|
cookie-es: 2.0.0
|
||||||
@@ -4513,17 +4504,17 @@ snapshots:
|
|||||||
source-map-js: 1.2.1
|
source-map-js: 1.2.1
|
||||||
srvx: 0.9.8
|
srvx: 0.9.8
|
||||||
terracotta: 1.1.0(solid-js@1.9.11)
|
terracotta: 1.1.0(solid-js@1.9.11)
|
||||||
vite: 7.3.1(@types/node@25.9.3)(jiti@2.6.1)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.46.0)
|
vite: 7.3.1(jiti@2.6.1)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.46.0)
|
||||||
vite-plugin-solid: 2.11.10(solid-js@1.9.11)(vite@7.3.1(@types/node@25.9.3)(jiti@2.6.1)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.46.0))
|
vite-plugin-solid: 2.11.10(solid-js@1.9.11)(vite@7.3.1(jiti@2.6.1)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.46.0))
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@testing-library/jest-dom'
|
- '@testing-library/jest-dom'
|
||||||
- crossws
|
- crossws
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@solidjs/vite-plugin-nitro-2@0.1.0(vite@7.3.1(@types/node@25.9.3)(jiti@2.6.1)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.46.0))':
|
'@solidjs/vite-plugin-nitro-2@0.1.0(vite@7.3.1(jiti@2.6.1)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.46.0))':
|
||||||
dependencies:
|
dependencies:
|
||||||
nitropack: 2.13.1
|
nitropack: 2.13.1
|
||||||
vite: 7.3.1(@types/node@25.9.3)(jiti@2.6.1)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.46.0)
|
vite: 7.3.1(jiti@2.6.1)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.46.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@azure/app-configuration'
|
- '@azure/app-configuration'
|
||||||
- '@azure/cosmos'
|
- '@azure/cosmos'
|
||||||
@@ -4558,7 +4549,7 @@ snapshots:
|
|||||||
|
|
||||||
'@speed-highlight/core@1.2.14': {}
|
'@speed-highlight/core@1.2.14': {}
|
||||||
|
|
||||||
'@tanstack/directive-functions-plugin@1.134.5(vite@7.3.1(@types/node@25.9.3)(jiti@2.6.1)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.46.0))':
|
'@tanstack/directive-functions-plugin@1.134.5(vite@7.3.1(jiti@2.6.1)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.46.0))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/code-frame': 7.27.1
|
'@babel/code-frame': 7.27.1
|
||||||
'@babel/core': 7.29.0
|
'@babel/core': 7.29.0
|
||||||
@@ -4568,7 +4559,7 @@ snapshots:
|
|||||||
babel-dead-code-elimination: 1.0.12
|
babel-dead-code-elimination: 1.0.12
|
||||||
pathe: 2.0.3
|
pathe: 2.0.3
|
||||||
tiny-invariant: 1.3.3
|
tiny-invariant: 1.3.3
|
||||||
vite: 7.3.1(@types/node@25.9.3)(jiti@2.6.1)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.46.0)
|
vite: 7.3.1(jiti@2.6.1)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.46.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -4585,7 +4576,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@tanstack/server-functions-plugin@1.134.5(vite@7.3.1(@types/node@25.9.3)(jiti@2.6.1)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.46.0))':
|
'@tanstack/server-functions-plugin@1.134.5(vite@7.3.1(jiti@2.6.1)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.46.0))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/code-frame': 7.27.1
|
'@babel/code-frame': 7.27.1
|
||||||
'@babel/core': 7.29.0
|
'@babel/core': 7.29.0
|
||||||
@@ -4594,7 +4585,7 @@ snapshots:
|
|||||||
'@babel/template': 7.28.6
|
'@babel/template': 7.28.6
|
||||||
'@babel/traverse': 7.29.0
|
'@babel/traverse': 7.29.0
|
||||||
'@babel/types': 7.29.0
|
'@babel/types': 7.29.0
|
||||||
'@tanstack/directive-functions-plugin': 1.134.5(vite@7.3.1(@types/node@25.9.3)(jiti@2.6.1)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.46.0))
|
'@tanstack/directive-functions-plugin': 1.134.5(vite@7.3.1(jiti@2.6.1)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.46.0))
|
||||||
babel-dead-code-elimination: 1.0.12
|
babel-dead-code-elimination: 1.0.12
|
||||||
tiny-invariant: 1.3.3
|
tiny-invariant: 1.3.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -4638,10 +4629,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/braces': 3.0.5
|
'@types/braces': 3.0.5
|
||||||
|
|
||||||
'@types/node@25.9.3':
|
|
||||||
dependencies:
|
|
||||||
undici-types: 7.24.6
|
|
||||||
|
|
||||||
'@types/resolve@1.20.2': {}
|
'@types/resolve@1.20.2': {}
|
||||||
|
|
||||||
'@types/unist@3.0.3': {}
|
'@types/unist@3.0.3': {}
|
||||||
@@ -6766,8 +6753,6 @@ snapshots:
|
|||||||
magic-string: 0.30.21
|
magic-string: 0.30.21
|
||||||
unplugin: 2.3.11
|
unplugin: 2.3.11
|
||||||
|
|
||||||
undici-types@7.24.6: {}
|
|
||||||
|
|
||||||
unenv@2.0.0-rc.24:
|
unenv@2.0.0-rc.24:
|
||||||
dependencies:
|
dependencies:
|
||||||
pathe: 2.0.3
|
pathe: 2.0.3
|
||||||
@@ -6891,7 +6876,7 @@ snapshots:
|
|||||||
'@types/unist': 3.0.3
|
'@types/unist': 3.0.3
|
||||||
vfile-message: 4.0.3
|
vfile-message: 4.0.3
|
||||||
|
|
||||||
vite-plugin-solid@2.11.10(solid-js@1.9.11)(vite@7.3.1(@types/node@25.9.3)(jiti@2.6.1)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.46.0)):
|
vite-plugin-solid@2.11.10(solid-js@1.9.11)(vite@7.3.1(jiti@2.6.1)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.46.0)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.0
|
'@babel/core': 7.29.0
|
||||||
'@types/babel__core': 7.20.5
|
'@types/babel__core': 7.20.5
|
||||||
@@ -6899,12 +6884,12 @@ snapshots:
|
|||||||
merge-anything: 5.1.7
|
merge-anything: 5.1.7
|
||||||
solid-js: 1.9.11
|
solid-js: 1.9.11
|
||||||
solid-refresh: 0.6.3(solid-js@1.9.11)
|
solid-refresh: 0.6.3(solid-js@1.9.11)
|
||||||
vite: 7.3.1(@types/node@25.9.3)(jiti@2.6.1)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.46.0)
|
vite: 7.3.1(jiti@2.6.1)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.46.0)
|
||||||
vitefu: 1.1.2(vite@7.3.1(@types/node@25.9.3)(jiti@2.6.1)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.46.0))
|
vitefu: 1.1.2(vite@7.3.1(jiti@2.6.1)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.46.0))
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
vite@7.3.1(@types/node@25.9.3)(jiti@2.6.1)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.46.0):
|
vite@7.3.1(jiti@2.6.1)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.46.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.27.3
|
esbuild: 0.27.3
|
||||||
fdir: 6.5.0(picomatch@4.0.3)
|
fdir: 6.5.0(picomatch@4.0.3)
|
||||||
@@ -6913,16 +6898,15 @@ snapshots:
|
|||||||
rollup: 4.59.0
|
rollup: 4.59.0
|
||||||
tinyglobby: 0.2.15
|
tinyglobby: 0.2.15
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 25.9.3
|
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
jiti: 2.6.1
|
jiti: 2.6.1
|
||||||
sass: 1.101.0
|
sass: 1.101.0
|
||||||
sass-embedded: 1.100.0
|
sass-embedded: 1.100.0
|
||||||
terser: 5.46.0
|
terser: 5.46.0
|
||||||
|
|
||||||
vitefu@1.1.2(vite@7.3.1(@types/node@25.9.3)(jiti@2.6.1)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.46.0)):
|
vitefu@1.1.2(vite@7.3.1(jiti@2.6.1)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.46.0)):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
vite: 7.3.1(@types/node@25.9.3)(jiti@2.6.1)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.46.0)
|
vite: 7.3.1(jiti@2.6.1)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.46.0)
|
||||||
|
|
||||||
webidl-conversions@3.0.1: {}
|
webidl-conversions@3.0.1: {}
|
||||||
|
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
||||||
import { dirname, resolve } from "node:path";
|
|
||||||
|
|
||||||
const manifestPath = resolve("dist/client/.vite/manifest.json");
|
|
||||||
const outputPath = resolve("dist/client/index.html");
|
|
||||||
|
|
||||||
const themeBootstrapScript = `
|
|
||||||
(() => {
|
|
||||||
try {
|
|
||||||
const storageKey = "theme";
|
|
||||||
const stored = localStorage.getItem(storageKey);
|
|
||||||
const theme = stored === "light" || stored === "dark"
|
|
||||||
? stored
|
|
||||||
: (window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
|
|
||||||
|
|
||||||
document.documentElement.setAttribute("data-theme", theme);
|
|
||||||
} catch {
|
|
||||||
document.documentElement.setAttribute("data-theme", "light");
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
`.trim();
|
|
||||||
|
|
||||||
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
||||||
const entry = manifest["src/entry-client.tsx"];
|
|
||||||
|
|
||||||
if (!entry?.file) {
|
|
||||||
throw new Error("Could not find src/entry-client.tsx in the client manifest.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const cssLinks = Array.isArray(entry.css) ? entry.css.map((href) => ` <link rel="stylesheet" href="/${href}">`).join("\n") : "";
|
|
||||||
|
|
||||||
const html = `<!doctype html>
|
|
||||||
<html lang="en" data-theme="light">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<link rel="icon" href="/favicon.ico">
|
|
||||||
<script>${themeBootstrapScript}</script>
|
|
||||||
${cssLinks}
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="app"></div>
|
|
||||||
<script type="module" src="/${entry.file}"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`;
|
|
||||||
|
|
||||||
await mkdir(dirname(outputPath), { recursive: true });
|
|
||||||
await writeFile(outputPath, html, "utf8");
|
|
||||||
@@ -1,11 +1,7 @@
|
|||||||
// Path: Frontend/src/app.tsx
|
// Path: Frontend/src/app.tsx
|
||||||
|
|
||||||
import type { JSX } from "solid-js";
|
|
||||||
import { AppShell } from "./components/shell/AppShell/AppShell";
|
|
||||||
import "./styles/main.scss";
|
import "./styles/main.scss";
|
||||||
|
|
||||||
const App = (): JSX.Element => {
|
export default function App() {
|
||||||
return <AppShell />;
|
return <main />;
|
||||||
};
|
}
|
||||||
|
|
||||||
export default App;
|
|
||||||
|
|||||||
@@ -1,135 +0,0 @@
|
|||||||
.shell {
|
|
||||||
height: 100dvh;
|
|
||||||
min-height: 100dvh;
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: auto minmax(0, 1fr);
|
|
||||||
background: var(--color-canvas);
|
|
||||||
color: var(--color-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.body {
|
|
||||||
--rail-width: 4.75rem;
|
|
||||||
--sidebar-width: 16.75rem;
|
|
||||||
--shell-top-left-radius: calc(var(--radius-xl) + var(--space-1));
|
|
||||||
--shell-frame-border: color-mix(in srgb, var(--color-border-strong) 44%, transparent);
|
|
||||||
--shell-divider-border: color-mix(in srgb, var(--color-border-strong) 34%, transparent);
|
|
||||||
--sidebar-panel-surface: color-mix(in srgb, var(--color-surface-muted) 92%, transparent);
|
|
||||||
--workspace-panel-surface: color-mix(in srgb, var(--color-canvas) 94%, var(--color-surface));
|
|
||||||
min-height: 0;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: var(--rail-width) minmax(0, 1fr);
|
|
||||||
overflow: hidden;
|
|
||||||
background: var(--color-surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
.railColumn {
|
|
||||||
min-height: 0;
|
|
||||||
display: flex;
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
background: var(--color-surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
.workspaceRegion {
|
|
||||||
position: relative;
|
|
||||||
min-width: 0;
|
|
||||||
min-height: 0;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: var(--sidebar-width) minmax(0, 1fr);
|
|
||||||
overflow: visible;
|
|
||||||
z-index: 1;
|
|
||||||
isolation: isolate;
|
|
||||||
border-top-left-radius: var(--shell-top-left-radius);
|
|
||||||
border-top-right-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.workspaceRegion::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
background:
|
|
||||||
linear-gradient(
|
|
||||||
to right,
|
|
||||||
var(--sidebar-panel-surface) 0,
|
|
||||||
var(--sidebar-panel-surface) calc(var(--sidebar-width) - 0.5px),
|
|
||||||
var(--workspace-panel-surface) calc(var(--sidebar-width) - 0.5px),
|
|
||||||
var(--workspace-panel-surface) 100%
|
|
||||||
);
|
|
||||||
border: 1px solid var(--shell-frame-border);
|
|
||||||
border-top: 0;
|
|
||||||
border-top-left-radius: var(--shell-top-left-radius);
|
|
||||||
border-top-right-radius: 0;
|
|
||||||
box-shadow: inset 0 1px 0 color-mix(in srgb, white 3%, transparent);
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebarColumn {
|
|
||||||
position: relative;
|
|
||||||
min-width: 0;
|
|
||||||
min-height: 0;
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: minmax(0, 1fr);
|
|
||||||
overflow: visible;
|
|
||||||
z-index: 1;
|
|
||||||
background: var(--sidebar-panel-surface);
|
|
||||||
border-top: 1px solid var(--shell-frame-border);
|
|
||||||
border-left: 1px solid var(--shell-frame-border);
|
|
||||||
border-top-left-radius: var(--shell-top-left-radius);
|
|
||||||
}
|
|
||||||
|
|
||||||
.workspaceMain {
|
|
||||||
min-width: 0;
|
|
||||||
min-height: 0;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
z-index: 1;
|
|
||||||
border-top: 1px solid var(--shell-frame-border);
|
|
||||||
border-left: 1px solid var(--shell-divider-border);
|
|
||||||
background: var(--workspace-panel-surface);
|
|
||||||
border-top-right-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebarDock {
|
|
||||||
position: absolute;
|
|
||||||
right: var(--space-1);
|
|
||||||
bottom: var(--space-3);
|
|
||||||
left: calc(var(--space-1) - (var(--rail-width) * 0.9));
|
|
||||||
z-index: calc(var(--z-modal) + 1);
|
|
||||||
pointer-events: none;
|
|
||||||
|
|
||||||
> * {
|
|
||||||
pointer-events: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@include respond-up(mobile) {
|
|
||||||
.body {
|
|
||||||
--rail-width: 5rem;
|
|
||||||
--sidebar-width: 17.25rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@include respond-down(tablet) {
|
|
||||||
.body {
|
|
||||||
--rail-width: 4.5rem;
|
|
||||||
--sidebar-width: 13.25rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@include respond-down(mobile) {
|
|
||||||
.body {
|
|
||||||
grid-template-columns: 4.5rem minmax(0, 1fr);
|
|
||||||
--rail-width: 4.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.railColumn {
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.workspaceRegion,
|
|
||||||
.sidebarDock {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
// Path: Frontend/src/components/shell/AppShell/AppShell.tsx
|
|
||||||
|
|
||||||
import { createSignal, onMount, type JSX } from "solid-js";
|
|
||||||
import { getDocumentTheme, setTheme, type Theme } from "../../../helpers/theme";
|
|
||||||
import { WorkspaceHome } from "../../workspace-home/WorkspaceHome/WorkspaceHome";
|
|
||||||
import { LeftRail } from "../LeftRail/LeftRail";
|
|
||||||
import { ProfileDock } from "../ProfileDock/ProfileDock";
|
|
||||||
import { TopBar } from "../TopBar/TopBar";
|
|
||||||
import { WorkspaceSidebar } from "../WorkspaceSidebar/WorkspaceSidebar";
|
|
||||||
import styles from "./AppShell.module.scss";
|
|
||||||
|
|
||||||
export const AppShell = (): JSX.Element => {
|
|
||||||
const [themeState, setThemeState] = createSignal<Theme>("light");
|
|
||||||
|
|
||||||
onMount((): void => {
|
|
||||||
setThemeState(getDocumentTheme());
|
|
||||||
});
|
|
||||||
|
|
||||||
const toggleTheme = (): void => {
|
|
||||||
const next: Theme = themeState() === "dark" ? "light" : "dark";
|
|
||||||
|
|
||||||
setTheme(next);
|
|
||||||
setThemeState(next);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class={styles.shell}>
|
|
||||||
<TopBar theme={themeState()} onToggleTheme={toggleTheme} />
|
|
||||||
<div class={styles.body}>
|
|
||||||
<div class={styles.railColumn}>
|
|
||||||
<LeftRail />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class={styles.workspaceRegion}>
|
|
||||||
<div class={styles.sidebarColumn}>
|
|
||||||
<WorkspaceSidebar />
|
|
||||||
|
|
||||||
<div class={styles.sidebarDock}>
|
|
||||||
<ProfileDock />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class={styles.workspaceMain}>
|
|
||||||
<WorkspaceHome />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
.rail {
|
|
||||||
--rail-workspace-size: var(--control-size-lg);
|
|
||||||
--rail-action-size: var(--control-size-md);
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-3);
|
|
||||||
padding: var(--space-3) var(--space-2);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.topCluster,
|
|
||||||
.bottomCluster {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.items {
|
|
||||||
width: 100%;
|
|
||||||
min-height: 0;
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-2);
|
|
||||||
overflow-y: auto;
|
|
||||||
overscroll-behavior: contain;
|
|
||||||
padding-block: var(--space-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
width: var(--rail-workspace-size);
|
|
||||||
height: var(--rail-workspace-size);
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
background: var(--color-accent);
|
|
||||||
color: var(--color-accent-contrast);
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: -0.02em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.workspaceButton {
|
|
||||||
width: var(--rail-workspace-size);
|
|
||||||
height: var(--rail-workspace-size);
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
@include interactive-frame(var(--color-surface-muted), var(--color-border), var(--color-text-muted), var(--radius-lg));
|
|
||||||
@include text-label;
|
|
||||||
@include interactive-frame-hover();
|
|
||||||
}
|
|
||||||
|
|
||||||
.workspaceButtonActive {
|
|
||||||
background: var(--color-accent);
|
|
||||||
border-color: transparent;
|
|
||||||
color: var(--color-accent-contrast);
|
|
||||||
box-shadow: var(--shadow-soft);
|
|
||||||
}
|
|
||||||
|
|
||||||
.addButton {
|
|
||||||
width: var(--rail-action-size);
|
|
||||||
height: var(--rail-action-size);
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border: 1px dashed var(--color-border-strong);
|
|
||||||
border-radius: var(--radius-pill);
|
|
||||||
background: transparent;
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--color-surface-hover);
|
|
||||||
color: var(--color-text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
// Path: Frontend/src/components/shell/LeftRail/LeftRail.tsx
|
|
||||||
|
|
||||||
import { For, type JSX } from "solid-js";
|
|
||||||
import { Plus } from "../../../lib/icons";
|
|
||||||
import { railItems } from "../data/shell.data";
|
|
||||||
import styles from "./LeftRail.module.scss";
|
|
||||||
|
|
||||||
export const LeftRail = (): JSX.Element => {
|
|
||||||
return (
|
|
||||||
<aside class={styles.rail} aria-label="Workspace rail">
|
|
||||||
<div class={styles.topCluster}>
|
|
||||||
<div class={styles.logo} aria-hidden="true">
|
|
||||||
M
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class={styles.items}>
|
|
||||||
<For each={railItems}>
|
|
||||||
{(item): JSX.Element => (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
classList={{
|
|
||||||
[styles.workspaceButton]: true,
|
|
||||||
[styles.workspaceButtonActive]: !!item.active,
|
|
||||||
}}
|
|
||||||
title={item.label}
|
|
||||||
aria-label={item.label}
|
|
||||||
>
|
|
||||||
{item.abbreviation}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class={styles.bottomCluster}>
|
|
||||||
<button type="button" class={styles.addButton} aria-label="Create workspace" title="Create workspace">
|
|
||||||
<Plus size={16} strokeWidth={2} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
.panel {
|
|
||||||
--profile-dock-avatar-size: var(--control-size-md);
|
|
||||||
--profile-dock-action-min-height: var(--space-8);
|
|
||||||
--profile-dock-border: color-mix(in srgb, var(--color-border-strong) 75%, transparent);
|
|
||||||
--profile-dock-surface: color-mix(in srgb, var(--color-surface) 94%, transparent);
|
|
||||||
--profile-dock-status-ring: 0 0 0 3px color-mix(in srgb, var(--color-success) 18%, transparent);
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
width: 100%;
|
|
||||||
display: grid;
|
|
||||||
gap: var(--space-2);
|
|
||||||
padding: var(--space-3) var(--space-3) var(--space-2);
|
|
||||||
border: 1px solid var(--profile-dock-border);
|
|
||||||
border-radius: calc(var(--radius-xl) + var(--space-1));
|
|
||||||
background: var(--profile-dock-surface);
|
|
||||||
box-shadow:
|
|
||||||
0 20px 48px color-mix(in srgb, black 16%, transparent),
|
|
||||||
var(--shadow-strong);
|
|
||||||
backdrop-filter: blur(var(--blur-overlay));
|
|
||||||
}
|
|
||||||
|
|
||||||
.identity {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: auto minmax(0, 1fr);
|
|
||||||
gap: var(--space-3);
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar {
|
|
||||||
width: var(--profile-dock-avatar-size);
|
|
||||||
height: var(--profile-dock-avatar-size);
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--color-accent-soft);
|
|
||||||
color: var(--color-accent-strong);
|
|
||||||
@include text-label;
|
|
||||||
}
|
|
||||||
|
|
||||||
.copy {
|
|
||||||
min-width: 0;
|
|
||||||
display: grid;
|
|
||||||
gap: 0.15rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name {
|
|
||||||
@include text-label;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status {
|
|
||||||
@include text-caption;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-2);
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.statusDot {
|
|
||||||
width: 0.5rem;
|
|
||||||
height: 0.5rem;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--color-success);
|
|
||||||
box-shadow: var(--profile-dock-status-ring);
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
gap: var(--space-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.action {
|
|
||||||
min-height: var(--profile-dock-action-min-height);
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: var(--space-1);
|
|
||||||
@include interactive-frame(var(--color-surface-muted));
|
|
||||||
@include interactive-frame-hover();
|
|
||||||
}
|
|
||||||
|
|
||||||
.actionLabel {
|
|
||||||
@include text-caption;
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
// Path: Frontend/src/components/shell/ProfileDock/ProfileDock.tsx
|
|
||||||
|
|
||||||
import type { JSX } from "solid-js";
|
|
||||||
import { Settings, User } from "../../../lib/icons";
|
|
||||||
import styles from "./ProfileDock.module.scss";
|
|
||||||
|
|
||||||
export const ProfileDock = (): JSX.Element => {
|
|
||||||
return (
|
|
||||||
<section class={styles.panel} aria-label="Profile dock">
|
|
||||||
<div class={styles.identity}>
|
|
||||||
<div class={styles.avatar} aria-hidden="true">
|
|
||||||
R
|
|
||||||
</div>
|
|
||||||
<div class={styles.copy}>
|
|
||||||
<span class={styles.name}>Ronald</span>
|
|
||||||
<span class={styles.status}>
|
|
||||||
<span class={styles.statusDot} aria-hidden="true" />
|
|
||||||
Online in Moku
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class={styles.actions}>
|
|
||||||
<button type="button" class={styles.action}>
|
|
||||||
<User size={16} strokeWidth={2} />
|
|
||||||
<span class={styles.actionLabel}>Account</span>
|
|
||||||
</button>
|
|
||||||
<button type="button" class={styles.action}>
|
|
||||||
<Settings size={16} strokeWidth={2} />
|
|
||||||
<span class={styles.actionLabel}>Prefs</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
.topBar {
|
|
||||||
--topbar-control-size: var(--control-size-md);
|
|
||||||
min-height: 4rem;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(0, 1fr) auto auto;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-3);
|
|
||||||
padding: var(--space-3) var(--space-4) var(--space-3);
|
|
||||||
background: var(--color-surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
.identity {
|
|
||||||
min-width: 0;
|
|
||||||
display: grid;
|
|
||||||
gap: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.eyebrow {
|
|
||||||
@include text-caption;
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
@include text-title;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-2);
|
|
||||||
|
|
||||||
strong {
|
|
||||||
font: inherit;
|
|
||||||
font-weight: var(--font-weight-title);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.context {
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.actionButton,
|
|
||||||
.themeButton {
|
|
||||||
height: var(--topbar-control-size);
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
@include interactive-frame();
|
|
||||||
}
|
|
||||||
|
|
||||||
.actionButton {
|
|
||||||
width: var(--topbar-control-size);
|
|
||||||
}
|
|
||||||
|
|
||||||
.themeButton {
|
|
||||||
width: auto;
|
|
||||||
padding-inline: var(--space-2);
|
|
||||||
gap: var(--space-1);
|
|
||||||
color: var(--color-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.actionButton,
|
|
||||||
.themeButton {
|
|
||||||
@include interactive-frame-hover();
|
|
||||||
}
|
|
||||||
|
|
||||||
.themeLabel {
|
|
||||||
@include text-label;
|
|
||||||
}
|
|
||||||
|
|
||||||
@include respond-down(mobile) {
|
|
||||||
.topBar {
|
|
||||||
grid-template-columns: minmax(0, 1fr) auto;
|
|
||||||
padding: var(--space-2) var(--space-3) var(--space-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
// Path: Frontend/src/components/shell/TopBar/TopBar.tsx
|
|
||||||
|
|
||||||
import { For, type JSX } from "solid-js";
|
|
||||||
import type { Theme } from "../../../helpers/theme";
|
|
||||||
import { ChevronDown } from "../../../lib/icons";
|
|
||||||
import { topBarActions } from "../data/shell.data";
|
|
||||||
import styles from "./TopBar.module.scss";
|
|
||||||
|
|
||||||
type TopBarProps = {
|
|
||||||
theme: Theme;
|
|
||||||
onToggleTheme: VoidFunction;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const TopBar = (props: TopBarProps): JSX.Element => {
|
|
||||||
return (
|
|
||||||
<header class={styles.topBar}>
|
|
||||||
<div class={styles.identity}>
|
|
||||||
<span class={styles.eyebrow}>Moku Work</span>
|
|
||||||
<div class={styles.title}>
|
|
||||||
<strong>Workspace Shell</strong>
|
|
||||||
<span class={styles.context}>Moku / Product</span>
|
|
||||||
<ChevronDown size={16} strokeWidth={2} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class={styles.themeButton} type="button" onClick={props.onToggleTheme}>
|
|
||||||
<span class={styles.themeLabel}>{props.theme === "dark" ? "Dark" : "Light"}</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class={styles.actions}>
|
|
||||||
<For each={topBarActions}>
|
|
||||||
{(item): JSX.Element => {
|
|
||||||
const Icon = item.icon;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button class={styles.actionButton} type="button" aria-label={item.label} title={item.label}>
|
|
||||||
<Icon size={18} strokeWidth={2} />
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
.sidebar {
|
|
||||||
--sidebar-nav-item-min-height: var(--control-size-lg);
|
|
||||||
--sidebar-dock-clearance: 8rem;
|
|
||||||
min-width: 0;
|
|
||||||
min-height: 0;
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: auto auto minmax(0, 1fr);
|
|
||||||
gap: var(--space-4);
|
|
||||||
padding: var(--space-4);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
display: grid;
|
|
||||||
gap: 0.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.eyebrow {
|
|
||||||
@include text-caption;
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
@include text-title;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta {
|
|
||||||
@include text-caption;
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
max-width: 28ch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section {
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: auto minmax(0, 1fr);
|
|
||||||
gap: var(--space-2);
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navScroller {
|
|
||||||
min-height: 0;
|
|
||||||
overflow-y: auto;
|
|
||||||
overscroll-behavior: contain;
|
|
||||||
padding-right: var(--space-1);
|
|
||||||
padding-bottom: calc(var(--space-4) + var(--sidebar-dock-clearance));
|
|
||||||
margin-right: calc(var(--space-1) * -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sectionLabel {
|
|
||||||
@include text-label;
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.navList {
|
|
||||||
list-style: none;
|
|
||||||
display: grid;
|
|
||||||
gap: var(--space-1);
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navItem {
|
|
||||||
width: 100%;
|
|
||||||
min-width: 0;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-2);
|
|
||||||
min-height: var(--sidebar-nav-item-min-height);
|
|
||||||
padding: var(--space-2) var(--space-3);
|
|
||||||
@include interactive-frame(transparent, transparent, var(--color-text-muted), var(--radius-lg));
|
|
||||||
text-align: left;
|
|
||||||
@include interactive-frame-hover(var(--color-surface-hover), transparent, var(--color-text));
|
|
||||||
}
|
|
||||||
|
|
||||||
.navItemActive {
|
|
||||||
border-color: var(--color-border);
|
|
||||||
background: var(--color-surface);
|
|
||||||
color: var(--color-text);
|
|
||||||
box-shadow: var(--shadow-soft);
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
color: inherit;
|
|
||||||
opacity: 0.85;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
|
||||||
@include text-label;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.itemMeta {
|
|
||||||
@include text-caption;
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
@include respond-down(mobile) {
|
|
||||||
.sidebar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
// Path: Frontend/src/components/shell/WorkspaceSidebar/WorkspaceSidebar.tsx
|
|
||||||
|
|
||||||
import { For, Show, type JSX } from "solid-js";
|
|
||||||
import { workspaceSidebarItems } from "../data/shell.data";
|
|
||||||
import styles from "./WorkspaceSidebar.module.scss";
|
|
||||||
|
|
||||||
export const WorkspaceSidebar = (): JSX.Element => {
|
|
||||||
return (
|
|
||||||
<aside class={styles.sidebar} aria-label="Workspace navigation">
|
|
||||||
<div class={styles.header}>
|
|
||||||
<span class={styles.eyebrow}>Workspace</span>
|
|
||||||
<h2 class={styles.title}>Product Operations</h2>
|
|
||||||
<p class={styles.meta}>A barebone shell for Moku’s first real workspace layout.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class={styles.section}>
|
|
||||||
<span class={styles.sectionLabel}>Navigation</span>
|
|
||||||
<div class={styles.navScroller}>
|
|
||||||
<ul class={styles.navList} role="list">
|
|
||||||
<For each={workspaceSidebarItems}>
|
|
||||||
{(item): JSX.Element => {
|
|
||||||
const Icon = item.icon;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
classList={{
|
|
||||||
[styles.navItem]: true,
|
|
||||||
[styles.navItemActive]: !!item.active,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon class={styles.icon} size={18} strokeWidth={2} />
|
|
||||||
<span class={styles.label}>{item.label}</span>
|
|
||||||
<Show when={item.meta}>
|
|
||||||
<span class={styles.itemMeta}>{item.meta}</span>
|
|
||||||
</Show>
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</For>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
// Path: Frontend/src/components/shell/data/shell.data.ts
|
|
||||||
|
|
||||||
import type { Component } from "solid-js";
|
|
||||||
import { Bell, Folder, Home, LayoutGrid, Plus, Search, Settings, User } from "../../../lib/icons";
|
|
||||||
|
|
||||||
type ShellIconProps = {
|
|
||||||
class?: string;
|
|
||||||
size?: number;
|
|
||||||
strokeWidth?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ShellIcon = Component<ShellIconProps>;
|
|
||||||
|
|
||||||
export type RailItem = {
|
|
||||||
id: string;
|
|
||||||
label: string;
|
|
||||||
abbreviation: string;
|
|
||||||
active?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SidebarItem = {
|
|
||||||
id: string;
|
|
||||||
label: string;
|
|
||||||
icon: ShellIcon;
|
|
||||||
active?: boolean;
|
|
||||||
meta?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TopBarAction = {
|
|
||||||
id: string;
|
|
||||||
label: string;
|
|
||||||
icon: ShellIcon;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const railItems: readonly RailItem[] = [
|
|
||||||
{ id: "personal", label: "Personal", abbreviation: "P" },
|
|
||||||
{ id: "moku", label: "Moku", abbreviation: "M", active: true },
|
|
||||||
{ id: "labs", label: "Labs", abbreviation: "L" },
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export const workspaceSidebarItems: readonly SidebarItem[] = [
|
|
||||||
{ id: "home", label: "Home", icon: Home, active: true },
|
|
||||||
{ id: "boards", label: "Boards", icon: LayoutGrid, meta: "0" },
|
|
||||||
{ id: "docs", label: "Docs", icon: Folder, meta: "0" },
|
|
||||||
{ id: "settings", label: "Settings", icon: Settings },
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export const topBarActions: readonly TopBarAction[] = [
|
|
||||||
{ id: "search", label: "Search", icon: Search },
|
|
||||||
{ id: "create", label: "Create", icon: Plus },
|
|
||||||
{ id: "inbox", label: "Inbox", icon: Bell },
|
|
||||||
{ id: "profile", label: "Profile", icon: User },
|
|
||||||
] as const;
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
.viewport {
|
|
||||||
--workspace-content-max-width: var(--content-width-wide);
|
|
||||||
--workspace-card-min-height: calc(var(--space-12) * 3);
|
|
||||||
min-width: 0;
|
|
||||||
min-height: 0;
|
|
||||||
display: grid;
|
|
||||||
align-content: start;
|
|
||||||
gap: var(--space-5);
|
|
||||||
padding: var(--space-5) var(--space-6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero {
|
|
||||||
display: grid;
|
|
||||||
gap: var(--space-3);
|
|
||||||
width: 100%;
|
|
||||||
max-width: var(--workspace-content-max-width);
|
|
||||||
}
|
|
||||||
|
|
||||||
.eyebrow {
|
|
||||||
@include text-caption;
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
@include text-display;
|
|
||||||
font-family: var(--font-family-display);
|
|
||||||
max-width: 12ch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.description {
|
|
||||||
max-width: 64ch;
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid {
|
|
||||||
width: 100%;
|
|
||||||
max-width: var(--workspace-content-max-width);
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
||||||
gap: var(--space-4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
display: grid;
|
|
||||||
gap: var(--space-2);
|
|
||||||
min-height: var(--workspace-card-min-height);
|
|
||||||
padding: var(--space-4);
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
border-radius: var(--radius-xl);
|
|
||||||
background: var(--color-surface);
|
|
||||||
box-shadow: var(--shadow-soft);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cardTitle {
|
|
||||||
@include text-title;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cardCopy {
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cardMeta {
|
|
||||||
@include text-caption;
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
@include respond-down(tablet) {
|
|
||||||
.grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@include respond-down(mobile) {
|
|
||||||
.viewport {
|
|
||||||
gap: var(--space-4);
|
|
||||||
padding: var(--space-4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
// Path: Frontend/src/components/workspace-home/WorkspaceHome/WorkspaceHome.tsx
|
|
||||||
|
|
||||||
import { For, type JSX } from "solid-js";
|
|
||||||
import styles from "./WorkspaceHome.module.scss";
|
|
||||||
|
|
||||||
type ShellCheckpointCard = {
|
|
||||||
title: string;
|
|
||||||
copy: string;
|
|
||||||
meta: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const shellCheckpointCards: readonly ShellCheckpointCard[] = [
|
|
||||||
{
|
|
||||||
title: "App shell",
|
|
||||||
copy: "Top bar, left rail, workspace sidebar, and content viewport are now split into modular components.",
|
|
||||||
meta: "Layout foundation",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Workspace context",
|
|
||||||
copy: "The shell already has clear places for org context, workspace switching, and future surface navigation.",
|
|
||||||
meta: "Navigation foundation",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Next build target",
|
|
||||||
copy: "You can now plug in workspace home content, auth state, and early primitives without redesigning the whole frame.",
|
|
||||||
meta: "Ready for v0.1.0 work",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const WorkspaceHome = (): JSX.Element => {
|
|
||||||
return (
|
|
||||||
<main class={styles.viewport}>
|
|
||||||
<section class={styles.hero}>
|
|
||||||
<span class={styles.eyebrow}>Workspace home</span>
|
|
||||||
<h1 class={styles.title}>Moku is ready for its first real shell.</h1>
|
|
||||||
<p class={styles.description}>
|
|
||||||
This is the barebone app frame for v0.1.0 — enough structure to start building real frontend surfaces on top of a real backend core.
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class={styles.grid} aria-label="Shell checkpoints">
|
|
||||||
<For each={shellCheckpointCards}>
|
|
||||||
{(card): JSX.Element => (
|
|
||||||
<article class={styles.card}>
|
|
||||||
<h2 class={styles.cardTitle}>{card.title}</h2>
|
|
||||||
<p class={styles.cardCopy}>{card.copy}</p>
|
|
||||||
<span class={styles.cardMeta}>{card.meta}</span>
|
|
||||||
</article>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,21 +1,4 @@
|
|||||||
// Path: Frontend/src/entry-client.tsx
|
|
||||||
|
|
||||||
// @refresh reload
|
// @refresh reload
|
||||||
import type { JSX } from "solid-js";
|
|
||||||
import { mount, StartClient } from "@solidjs/start/client";
|
import { mount, StartClient } from "@solidjs/start/client";
|
||||||
|
|
||||||
const getAppRoot = (): HTMLElement => {
|
mount(() => <StartClient />, document.getElementById("app")!);
|
||||||
const appRoot = document.getElementById("app");
|
|
||||||
|
|
||||||
if (!(appRoot instanceof HTMLElement)) {
|
|
||||||
throw new Error("App root element '#app' was not found.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return appRoot;
|
|
||||||
};
|
|
||||||
|
|
||||||
const mountApp = (): void => {
|
|
||||||
mount((): JSX.Element => <StartClient />, getAppRoot());
|
|
||||||
};
|
|
||||||
|
|
||||||
mountApp();
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
// Path: Frontend/src/entry-server.tsx
|
// Path: Frontend/src/entry-server.tsx
|
||||||
|
|
||||||
// @refresh reload
|
// @refresh reload
|
||||||
import type { JSX } from "solid-js";
|
|
||||||
import { createHandler, StartServer } from "@solidjs/start/server";
|
import { createHandler, StartServer } from "@solidjs/start/server";
|
||||||
|
|
||||||
const themeBootstrapScript = `
|
const themeBootstrapScript = `
|
||||||
@@ -20,14 +19,9 @@ const themeBootstrapScript = `
|
|||||||
})();
|
})();
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type DocumentRenderProps = {
|
export default createHandler(() => (
|
||||||
assets?: JSX.Element;
|
<StartServer
|
||||||
children?: JSX.Element;
|
document={({ assets, children, scripts }) => (
|
||||||
scripts?: JSX.Element;
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderDocument = ({ assets, children, scripts }: DocumentRenderProps): JSX.Element => {
|
|
||||||
return (
|
|
||||||
<html lang="en" data-theme="light">
|
<html lang="en" data-theme="light">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
@@ -41,11 +35,6 @@ const renderDocument = ({ assets, children, scripts }: DocumentRenderProps): JSX
|
|||||||
{scripts}
|
{scripts}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
)}
|
||||||
};
|
/>
|
||||||
|
));
|
||||||
const serverHandler = createHandler((): JSX.Element => {
|
|
||||||
return <StartServer document={renderDocument} />;
|
|
||||||
});
|
|
||||||
|
|
||||||
export default serverHandler;
|
|
||||||
|
|||||||
2
Frontend/src/global.d.ts
vendored
2
Frontend/src/global.d.ts
vendored
@@ -1,3 +1 @@
|
|||||||
// Path: Frontend/src/global.d.ts
|
|
||||||
|
|
||||||
/// <reference types="@solidjs/start/env" />
|
/// <reference types="@solidjs/start/env" />
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export const resolvePreferredTheme = (): Theme => {
|
|||||||
|
|
||||||
export const getDocumentTheme = (): Theme => (document.documentElement.getAttribute("data-theme") === "dark" ? "dark" : "light");
|
export const getDocumentTheme = (): Theme => (document.documentElement.getAttribute("data-theme") === "dark" ? "dark" : "light");
|
||||||
|
|
||||||
export const setTheme = (theme: Theme): void => {
|
export const setTheme = (theme: Theme) => {
|
||||||
document.documentElement.setAttribute("data-theme", theme);
|
document.documentElement.setAttribute("data-theme", theme);
|
||||||
localStorage.setItem(THEME_STORAGE_KEY, theme);
|
localStorage.setItem(THEME_STORAGE_KEY, theme);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,3 @@
|
|||||||
// Path: Frontend/src/lib/icons/index.ts
|
// Path: Frontend/src/lib/icons/index.ts
|
||||||
|
|
||||||
export { default as Bell } from "lucide-solid/icons/bell";
|
export { Bell, Check, ChevronDown, ChevronLeft, ChevronRight, Folder, FolderOpen, Home, LayoutGrid, MoreHorizontal, Plus, Search, Settings, User, X } from "lucide-solid";
|
||||||
export { default as ChevronDown } from "lucide-solid/icons/chevron-down";
|
|
||||||
export { default as Folder } from "lucide-solid/icons/folder";
|
|
||||||
export { default as Home } from "lucide-solid/icons/house";
|
|
||||||
export { default as LayoutGrid } from "lucide-solid/icons/layout-grid";
|
|
||||||
export { default as Plus } from "lucide-solid/icons/plus";
|
|
||||||
export { default as Search } from "lucide-solid/icons/search";
|
|
||||||
export { default as Settings } from "lucide-solid/icons/settings";
|
|
||||||
export { default as User } from "lucide-solid/icons/user";
|
|
||||||
|
|||||||
@@ -37,11 +37,6 @@
|
|||||||
--radius-xl: 1.25rem;
|
--radius-xl: 1.25rem;
|
||||||
--radius-pill: 999px;
|
--radius-pill: 999px;
|
||||||
|
|
||||||
--control-size-md: 2.25rem;
|
|
||||||
--control-size-lg: 2.5rem;
|
|
||||||
--content-width-wide: 72rem;
|
|
||||||
--blur-overlay: 18px;
|
|
||||||
|
|
||||||
--shadow-soft: 0 12px 32px hsl(220 30% 10% / 0.08);
|
--shadow-soft: 0 12px 32px hsl(220 30% 10% / 0.08);
|
||||||
--shadow-strong: 0 20px 48px hsl(220 30% 10% / 0.16);
|
--shadow-strong: 0 20px 48px hsl(220 30% 10% / 0.16);
|
||||||
|
|
||||||
|
|||||||
5
Frontend/src/styles/tools/_functions.scss
Normal file
5
Frontend/src/styles/tools/_functions.scss
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
/* Path: Frontend/src/styles/tools/_functions.scss */
|
||||||
|
|
||||||
|
@function rem($pixels, $base: 16) {
|
||||||
|
@return ($pixels / $base) * 1rem;
|
||||||
|
}
|
||||||
@@ -18,47 +18,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin respond-down($breakpoint) {
|
|
||||||
@if $breakpoint == mobile {
|
|
||||||
@media (max-width: calc($breakpoint-mobile - 0.01rem)) {
|
|
||||||
@content;
|
|
||||||
}
|
|
||||||
} @else if $breakpoint == tablet {
|
|
||||||
@media (max-width: calc($breakpoint-tablet - 0.01rem)) {
|
|
||||||
@content;
|
|
||||||
}
|
|
||||||
} @else if $breakpoint == desktop {
|
|
||||||
@media (max-width: calc($breakpoint-desktop - 0.01rem)) {
|
|
||||||
@content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin interactive-frame(
|
|
||||||
$background: var(--color-surface),
|
|
||||||
$border: var(--color-border),
|
|
||||||
$color: var(--color-text-muted),
|
|
||||||
$radius: var(--radius-md)
|
|
||||||
) {
|
|
||||||
border: 1px solid $border;
|
|
||||||
border-radius: $radius;
|
|
||||||
background: $background;
|
|
||||||
color: $color;
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin interactive-frame-hover(
|
|
||||||
$background: var(--color-surface-hover),
|
|
||||||
$border: var(--color-border-strong),
|
|
||||||
$color: var(--color-text)
|
|
||||||
) {
|
|
||||||
&:hover {
|
|
||||||
background: $background;
|
|
||||||
border-color: $border;
|
|
||||||
color: $color;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin text-caption {
|
@mixin text-caption {
|
||||||
font-size: var(--font-size-caption);
|
font-size: var(--font-size-caption);
|
||||||
font-weight: var(--font-weight-caption);
|
font-weight: var(--font-weight-caption);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
@@ -10,9 +9,8 @@
|
|||||||
"jsxImportSource": "solid-js",
|
"jsxImportSource": "solid-js",
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"skipLibCheck": true,
|
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"types": ["vite/client", "node"],
|
"types": ["vite/client"],
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"~/*": ["./src/*"]
|
"~/*": ["./src/*"]
|
||||||
|
|||||||
@@ -1,28 +1,17 @@
|
|||||||
// Path: Frontend/vite.config.ts
|
// Path: Frontend/vite.config.ts
|
||||||
|
|
||||||
|
import { nitroV2Plugin as nitro } from "@solidjs/vite-plugin-nitro-2";
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
|
|
||||||
import { solidStart } from "@solidjs/start/config";
|
import { solidStart } from "@solidjs/start/config";
|
||||||
|
|
||||||
const extraAllowedHosts = (process.env.ALLOWED_HOSTS ?? "")
|
|
||||||
.split(",")
|
|
||||||
.map((host) => host.trim())
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [solidStart({ ssr: false })],
|
|
||||||
server: {
|
|
||||||
allowedHosts: ["localhost", ...extraAllowedHosts],
|
|
||||||
},
|
|
||||||
preview: {
|
|
||||||
allowedHosts: ["localhost", ...extraAllowedHosts],
|
|
||||||
},
|
|
||||||
|
|
||||||
css: {
|
css: {
|
||||||
preprocessorOptions: {
|
preprocessorOptions: {
|
||||||
scss: {
|
scss: {
|
||||||
additionalData: `@use "/src/styles/tools/breakpoints" as *;\n@use "/src/styles/tools/mixins" as *;\n`,
|
additionalData: `@use "/src/styles/tools/functions" as *;\n@use "/src/styles/tools/breakpoints" as *;\n@use "/src/styles/tools/mixins" as *;\n`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
plugins: [solidStart(), nitro()],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
# Loader
|
|
||||||
|
|
||||||
Tiny first-request bootstrap document for Moku.
|
|
||||||
|
|
||||||
## Purpose
|
|
||||||
|
|
||||||
The loader is intended to be the smallest possible first paint shown before the
|
|
||||||
real application is handed off.
|
|
||||||
|
|
||||||
Long term responsibilities:
|
|
||||||
|
|
||||||
- capture original route intent
|
|
||||||
- collect lightweight client capability hints
|
|
||||||
- make a tiny boot decision
|
|
||||||
- hand off to login or app
|
|
||||||
|
|
||||||
## Route intent contract
|
|
||||||
|
|
||||||
The loader writes bootstrap state to `sessionStorage`:
|
|
||||||
|
|
||||||
- `moku.loader.intent` — the intended route path
|
|
||||||
- `moku.loader.meta` — JSON metadata for the current bootstrap event
|
|
||||||
|
|
||||||
The loader also writes a short-lived cookie:
|
|
||||||
|
|
||||||
- `moku_loader_seen=1`
|
|
||||||
|
|
||||||
The proxy uses that cookie to decide whether to keep serving the loader or to
|
|
||||||
serve the real app document on the next request.
|
|
||||||
|
|
||||||
## Current handoff behavior
|
|
||||||
|
|
||||||
For now, the loader simply hands off to the originally requested route. That
|
|
||||||
keeps the first implementation tiny while leaving a clear place to add auth or
|
|
||||||
perf-tier decisions later.
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en" data-theme="dark">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<meta name="color-scheme" content="dark" />
|
|
||||||
<title>Moku Loader</title>
|
|
||||||
<link rel="stylesheet" href="/__loader/styles.css" />
|
|
||||||
<script type="module" src="/__loader/scripts/main.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<main class="loader-shell" aria-busy="true" aria-live="polite">
|
|
||||||
<section class="loader-panel" aria-label="Application bootstrap">
|
|
||||||
<p class="loader-eyebrow">Moku bootstrap</p>
|
|
||||||
<h1 class="loader-title">Starting Moku</h1>
|
|
||||||
<p class="loader-copy">Capturing route intent and preparing the lightest possible handoff.</p>
|
|
||||||
|
|
||||||
<div class="loader-indicators" aria-hidden="true">
|
|
||||||
<span class="loader-dot"></span>
|
|
||||||
<span class="loader-dot"></span>
|
|
||||||
<span class="loader-dot"></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<dl class="loader-meta">
|
|
||||||
<div class="loader-meta-row">
|
|
||||||
<dt>Status</dt>
|
|
||||||
<dd id="loader-status">Initializing loader</dd>
|
|
||||||
</div>
|
|
||||||
<div class="loader-meta-row">
|
|
||||||
<dt>Intent</dt>
|
|
||||||
<dd id="loader-intent">Waiting for capture</dd>
|
|
||||||
</div>
|
|
||||||
</dl>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<noscript>
|
|
||||||
<section class="loader-noscript">
|
|
||||||
<strong>JavaScript is required.</strong>
|
|
||||||
<span>Moku's bootstrap loader needs JavaScript to hand off to the app.</span>
|
|
||||||
</section>
|
|
||||||
</noscript>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
// Path: Loader/scripts/config.js
|
|
||||||
|
|
||||||
export const LOADER_INTENT_STORAGE_KEY = "moku.loader.intent";
|
|
||||||
export const LOADER_META_STORAGE_KEY = "moku.loader.meta";
|
|
||||||
export const LOADER_SEEN_COOKIE_NAME = "moku_loader_seen";
|
|
||||||
export const LOADER_DEFAULT_INTENT = "/";
|
|
||||||
export const LOADER_ROOT_PREFIX = "/__loader";
|
|
||||||
export const LOADER_HANDOFF_DELAY_MS = 720;
|
|
||||||
export const LOADER_SEEN_COOKIE_MAX_AGE_SECONDS = 1800;
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
// Path: Loader/scripts/dom.js
|
|
||||||
|
|
||||||
export const getStatusElement = () => {
|
|
||||||
return document.getElementById("loader-status");
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getIntentElement = () => {
|
|
||||||
return document.getElementById("loader-intent");
|
|
||||||
};
|
|
||||||
|
|
||||||
export const setStatus = (value) => {
|
|
||||||
const statusElement = getStatusElement();
|
|
||||||
|
|
||||||
if (statusElement instanceof HTMLElement) {
|
|
||||||
statusElement.textContent = value;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const setIntentPreview = (value) => {
|
|
||||||
const intentElement = getIntentElement();
|
|
||||||
|
|
||||||
if (intentElement instanceof HTMLElement) {
|
|
||||||
intentElement.textContent = value;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
// Path: Loader/scripts/handoff.js
|
|
||||||
|
|
||||||
import { LOADER_HANDOFF_DELAY_MS } from "./config.js";
|
|
||||||
|
|
||||||
export const scheduleHandoff = (target, onBeforeHandoff) => {
|
|
||||||
window.setTimeout(() => {
|
|
||||||
if (typeof onBeforeHandoff === "function") {
|
|
||||||
onBeforeHandoff();
|
|
||||||
}
|
|
||||||
|
|
||||||
window.location.assign(target);
|
|
||||||
}, LOADER_HANDOFF_DELAY_MS);
|
|
||||||
};
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
// Path: Loader/scripts/hints.js
|
|
||||||
|
|
||||||
export const collectClientHints = () => {
|
|
||||||
const navigatorConnection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
|
|
||||||
|
|
||||||
return {
|
|
||||||
capturedAt: new Date().toISOString(),
|
|
||||||
language: navigator.language,
|
|
||||||
languages: Array.isArray(navigator.languages) ? navigator.languages : [],
|
|
||||||
hardwareConcurrency: navigator.hardwareConcurrency ?? null,
|
|
||||||
deviceMemory: navigator.deviceMemory ?? null,
|
|
||||||
networkType: navigatorConnection?.effectiveType ?? null,
|
|
||||||
saveData: navigatorConnection?.saveData ?? null,
|
|
||||||
viewport: {
|
|
||||||
width: window.innerWidth,
|
|
||||||
height: window.innerHeight,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
// Path: Loader/scripts/intent.js
|
|
||||||
|
|
||||||
import { LOADER_DEFAULT_INTENT, LOADER_ROOT_PREFIX } from "./config.js";
|
|
||||||
|
|
||||||
export const getExplicitIntent = () => {
|
|
||||||
const params = new URLSearchParams(window.location.search);
|
|
||||||
const next = params.get("next");
|
|
||||||
|
|
||||||
if (typeof next === "string" && next.startsWith("/")) {
|
|
||||||
return next;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getCurrentPathIntent = () => {
|
|
||||||
const current = `${window.location.pathname}${window.location.search}${window.location.hash}`;
|
|
||||||
|
|
||||||
if (current === "/" || current === LOADER_ROOT_PREFIX || current === `${LOADER_ROOT_PREFIX}/` || current.startsWith(`${LOADER_ROOT_PREFIX}/`)) {
|
|
||||||
return LOADER_DEFAULT_INTENT;
|
|
||||||
}
|
|
||||||
|
|
||||||
return current;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const resolveIntent = () => {
|
|
||||||
return getExplicitIntent() ?? getCurrentPathIntent();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const resolveHandoffTarget = (intent) => {
|
|
||||||
return intent || LOADER_DEFAULT_INTENT;
|
|
||||||
};
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
// Path: Loader/scripts/main.js
|
|
||||||
|
|
||||||
import { setIntentPreview, setStatus } from "./dom.js";
|
|
||||||
import { scheduleHandoff } from "./handoff.js";
|
|
||||||
import { resolveHandoffTarget, resolveIntent } from "./intent.js";
|
|
||||||
import { markLoaderSeen, persistLoaderState } from "./storage.js";
|
|
||||||
|
|
||||||
const bootLoader = () => {
|
|
||||||
const intent = resolveIntent();
|
|
||||||
const handoffTarget = resolveHandoffTarget(intent);
|
|
||||||
|
|
||||||
setStatus("Capturing route intent");
|
|
||||||
setIntentPreview(intent);
|
|
||||||
persistLoaderState(intent);
|
|
||||||
|
|
||||||
window.setTimeout(() => {
|
|
||||||
setStatus("Preparing handoff");
|
|
||||||
}, 180);
|
|
||||||
|
|
||||||
scheduleHandoff(handoffTarget, () => {
|
|
||||||
setStatus("Handing off to application");
|
|
||||||
markLoaderSeen();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
bootLoader();
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
// Path: Loader/scripts/storage.js
|
|
||||||
|
|
||||||
import { LOADER_INTENT_STORAGE_KEY, LOADER_META_STORAGE_KEY, LOADER_SEEN_COOKIE_MAX_AGE_SECONDS, LOADER_SEEN_COOKIE_NAME } from "./config.js";
|
|
||||||
import { collectClientHints } from "./hints.js";
|
|
||||||
|
|
||||||
export const persistLoaderState = (intent) => {
|
|
||||||
const payload = {
|
|
||||||
intent,
|
|
||||||
hints: collectClientHints(),
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
window.sessionStorage.setItem(LOADER_INTENT_STORAGE_KEY, intent);
|
|
||||||
window.sessionStorage.setItem(LOADER_META_STORAGE_KEY, JSON.stringify(payload));
|
|
||||||
} catch (error) {
|
|
||||||
console.warn("[Loader] Unable to persist bootstrap state.", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return payload;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const markLoaderSeen = () => {
|
|
||||||
document.cookie = `${LOADER_SEEN_COOKIE_NAME}=1; Path=/; Max-Age=${LOADER_SEEN_COOKIE_MAX_AGE_SECONDS}; SameSite=Lax`;
|
|
||||||
};
|
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
/* Path: Loader/styles.css */
|
|
||||||
|
|
||||||
:root {
|
|
||||||
color-scheme: dark;
|
|
||||||
--loader-bg: #07111f;
|
|
||||||
--loader-panel: rgba(15, 23, 42, 0.92);
|
|
||||||
--loader-panel-border: rgba(148, 163, 184, 0.18);
|
|
||||||
--loader-panel-highlight: rgba(255, 255, 255, 0.04);
|
|
||||||
--loader-text: #f8fafc;
|
|
||||||
--loader-text-muted: #94a3b8;
|
|
||||||
--loader-accent: #60a5fa;
|
|
||||||
--loader-accent-soft: rgba(96, 165, 250, 0.16);
|
|
||||||
--loader-shadow: 0 24px 64px rgba(2, 6, 23, 0.42);
|
|
||||||
--loader-radius: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
html,
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
min-height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
min-height: 100dvh;
|
|
||||||
font-family:
|
|
||||||
Inter,
|
|
||||||
ui-sans-serif,
|
|
||||||
system-ui,
|
|
||||||
-apple-system,
|
|
||||||
BlinkMacSystemFont,
|
|
||||||
"Segoe UI",
|
|
||||||
sans-serif;
|
|
||||||
background: radial-gradient(circle at top, rgba(37, 99, 235, 0.18), transparent 36%), linear-gradient(180deg, #091528 0%, var(--loader-bg) 100%);
|
|
||||||
color: var(--loader-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.loader-shell {
|
|
||||||
min-height: 100dvh;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
padding: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loader-panel {
|
|
||||||
width: min(100%, 30rem);
|
|
||||||
padding: 1.5rem;
|
|
||||||
border: 1px solid var(--loader-panel-border);
|
|
||||||
border-radius: var(--loader-radius);
|
|
||||||
background: linear-gradient(180deg, var(--loader-panel-highlight), transparent 16%), var(--loader-panel);
|
|
||||||
box-shadow: var(--loader-shadow);
|
|
||||||
backdrop-filter: blur(18px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.loader-eyebrow {
|
|
||||||
margin: 0 0 0.625rem;
|
|
||||||
color: var(--loader-text-muted);
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 0.12em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loader-title {
|
|
||||||
margin: 0;
|
|
||||||
font-size: clamp(1.75rem, 5vw, 2.25rem);
|
|
||||||
line-height: 1.05;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loader-copy {
|
|
||||||
margin: 0.875rem 0 0;
|
|
||||||
max-width: 30ch;
|
|
||||||
color: var(--loader-text-muted);
|
|
||||||
font-size: 0.975rem;
|
|
||||||
line-height: 1.55;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loader-indicators {
|
|
||||||
margin-top: 1.25rem;
|
|
||||||
display: inline-flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loader-dot {
|
|
||||||
width: 0.625rem;
|
|
||||||
height: 0.625rem;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: var(--loader-accent-soft);
|
|
||||||
animation: loader-pulse 1.4s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loader-dot:nth-child(2) {
|
|
||||||
animation-delay: 0.18s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loader-dot:nth-child(3) {
|
|
||||||
animation-delay: 0.36s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loader-meta {
|
|
||||||
margin: 1.5rem 0 0;
|
|
||||||
padding-top: 1rem;
|
|
||||||
border-top: 1px solid rgba(148, 163, 184, 0.12);
|
|
||||||
}
|
|
||||||
|
|
||||||
.loader-meta-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 4.5rem minmax(0, 1fr);
|
|
||||||
gap: 0.75rem;
|
|
||||||
align-items: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loader-meta-row + .loader-meta-row {
|
|
||||||
margin-top: 0.625rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loader-meta dt {
|
|
||||||
color: var(--loader-text-muted);
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loader-meta dd {
|
|
||||||
margin: 0;
|
|
||||||
min-width: 0;
|
|
||||||
color: var(--loader-text);
|
|
||||||
font-size: 0.925rem;
|
|
||||||
line-height: 1.45;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loader-noscript {
|
|
||||||
position: fixed;
|
|
||||||
right: 1rem;
|
|
||||||
bottom: 1rem;
|
|
||||||
left: 1rem;
|
|
||||||
display: grid;
|
|
||||||
gap: 0.375rem;
|
|
||||||
padding: 0.875rem 1rem;
|
|
||||||
border: 1px solid rgba(248, 113, 113, 0.28);
|
|
||||||
border-radius: 0.875rem;
|
|
||||||
background: rgba(69, 10, 10, 0.92);
|
|
||||||
color: #fecaca;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes loader-pulse {
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
transform: translateY(0);
|
|
||||||
background: var(--loader-accent-soft);
|
|
||||||
}
|
|
||||||
|
|
||||||
50% {
|
|
||||||
transform: translateY(-0.125rem);
|
|
||||||
background: var(--loader-accent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 47.99rem) {
|
|
||||||
.loader-shell {
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loader-panel {
|
|
||||||
padding: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loader-meta-row {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 0.25rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
# syntax=docker/dockerfile:1.7
|
|
||||||
|
|
||||||
## Frontend
|
|
||||||
## =========================
|
|
||||||
|
|
||||||
ARG NGINX_VERSION=1.29.8
|
|
||||||
|
|
||||||
FROM node:22-alpine AS frontend-dependencies
|
|
||||||
|
|
||||||
WORKDIR /workspace/Frontend
|
|
||||||
|
|
||||||
RUN corepack enable && corepack prepare pnpm@9.0.0 --activate
|
|
||||||
|
|
||||||
COPY Frontend/package.json Frontend/pnpm-lock.yaml Frontend/tsconfig.json Frontend/vite.config.ts ./
|
|
||||||
RUN pnpm install --frozen-lockfile
|
|
||||||
|
|
||||||
FROM frontend-dependencies AS frontend-build
|
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
|
||||||
|
|
||||||
COPY Frontend/ ./
|
|
||||||
|
|
||||||
RUN pnpm build:static
|
|
||||||
|
|
||||||
## Proxy
|
|
||||||
## =========================
|
|
||||||
|
|
||||||
FROM alpine:3.22 AS precompressed-assets
|
|
||||||
|
|
||||||
RUN apk add --no-cache brotli gzip
|
|
||||||
|
|
||||||
WORKDIR /workspace
|
|
||||||
|
|
||||||
COPY Loader ./html/__loader
|
|
||||||
COPY --from=frontend-build /workspace/Frontend/dist/client ./html
|
|
||||||
|
|
||||||
RUN find ./html -type f \( \
|
|
||||||
-name '*.html' -o \
|
|
||||||
-name '*.css' -o \
|
|
||||||
-name '*.js' -o \
|
|
||||||
-name '*.json' -o \
|
|
||||||
-name '*.svg' -o \
|
|
||||||
-name '*.txt' -o \
|
|
||||||
-name '*.xml' -o \
|
|
||||||
-name '*.webmanifest' \
|
|
||||||
\) -exec gzip -kf -9 {} \; \
|
|
||||||
&& find ./html -type f \( \
|
|
||||||
-name '*.html' -o \
|
|
||||||
-name '*.css' -o \
|
|
||||||
-name '*.js' -o \
|
|
||||||
-name '*.json' -o \
|
|
||||||
-name '*.svg' -o \
|
|
||||||
-name '*.txt' -o \
|
|
||||||
-name '*.xml' -o \
|
|
||||||
-name '*.webmanifest' \
|
|
||||||
\) -exec brotli -kf -Z {} \;
|
|
||||||
|
|
||||||
FROM alpine:3.22 AS brotli-modules
|
|
||||||
|
|
||||||
ARG NGINX_VERSION
|
|
||||||
|
|
||||||
RUN apk add --no-cache \
|
|
||||||
alpine-sdk \
|
|
||||||
cmake \
|
|
||||||
git \
|
|
||||||
linux-headers \
|
|
||||||
openssl-dev \
|
|
||||||
pcre2-dev \
|
|
||||||
wget \
|
|
||||||
zlib-dev
|
|
||||||
|
|
||||||
WORKDIR /tmp
|
|
||||||
|
|
||||||
RUN wget -O "nginx-${NGINX_VERSION}.tar.gz" "https://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz" \
|
|
||||||
&& tar -xzf "nginx-${NGINX_VERSION}.tar.gz" \
|
|
||||||
&& git clone --recurse-submodules https://github.com/google/ngx_brotli.git
|
|
||||||
|
|
||||||
WORKDIR /tmp/nginx-${NGINX_VERSION}
|
|
||||||
|
|
||||||
RUN cd /tmp/ngx_brotli/deps/brotli \
|
|
||||||
&& mkdir -p out \
|
|
||||||
&& cd out \
|
|
||||||
&& cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DCMAKE_C_FLAGS="-fPIC" -DCMAKE_CXX_FLAGS="-fPIC" .. \
|
|
||||||
&& cmake --build . --config Release -j"$(getconf _NPROCESSORS_ONLN)" \
|
|
||||||
&& cd /tmp/nginx-${NGINX_VERSION} \
|
|
||||||
&& ./configure --with-compat --add-dynamic-module=/tmp/ngx_brotli \
|
|
||||||
&& make modules
|
|
||||||
|
|
||||||
FROM nginx:${NGINX_VERSION}-alpine AS production
|
|
||||||
|
|
||||||
COPY Proxy/Local/nginx.conf /etc/nginx/nginx.conf
|
|
||||||
COPY Proxy/Local/default.conf /etc/nginx/conf.d/default.conf
|
|
||||||
COPY --from=brotli-modules /tmp/nginx-${NGINX_VERSION}/objs/ngx_http_brotli_filter_module.so /usr/lib/nginx/modules/
|
|
||||||
COPY --from=brotli-modules /tmp/nginx-${NGINX_VERSION}/objs/ngx_http_brotli_static_module.so /usr/lib/nginx/modules/
|
|
||||||
COPY --from=precompressed-assets /workspace/html /usr/share/nginx/html
|
|
||||||
|
|
||||||
EXPOSE 80
|
|
||||||
|
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
map $http_cookie $moku_bootstrap_document {
|
|
||||||
default /__loader/index.html;
|
|
||||||
"~*(^|; )moku_loader_seen=1($|;)" /index.html;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name _;
|
|
||||||
|
|
||||||
root /usr/share/nginx/html;
|
|
||||||
index index.html;
|
|
||||||
|
|
||||||
location = / {
|
|
||||||
add_header Cache-Control "no-store";
|
|
||||||
rewrite ^ $moku_bootstrap_document last;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ^~ /__loader/ {
|
|
||||||
access_log off;
|
|
||||||
add_header Cache-Control "no-store";
|
|
||||||
try_files $uri =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
location / {
|
|
||||||
try_files $uri $uri/ $moku_bootstrap_document;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /favicon.ico {
|
|
||||||
try_files $uri =404;
|
|
||||||
access_log off;
|
|
||||||
log_not_found off;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /_build/ {
|
|
||||||
access_log off;
|
|
||||||
add_header Cache-Control "public, max-age=31536000, immutable";
|
|
||||||
try_files $uri =404;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
load_module /usr/lib/nginx/modules/ngx_http_brotli_filter_module.so;
|
|
||||||
load_module /usr/lib/nginx/modules/ngx_http_brotli_static_module.so;
|
|
||||||
|
|
||||||
user nginx;
|
|
||||||
worker_processes auto;
|
|
||||||
|
|
||||||
error_log /var/log/nginx/error.log notice;
|
|
||||||
pid /run/nginx.pid;
|
|
||||||
|
|
||||||
events {
|
|
||||||
worker_connections 1024;
|
|
||||||
}
|
|
||||||
|
|
||||||
http {
|
|
||||||
include /etc/nginx/mime.types;
|
|
||||||
default_type application/octet-stream;
|
|
||||||
|
|
||||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
|
||||||
'$status $body_bytes_sent "$http_referer" '
|
|
||||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
|
||||||
|
|
||||||
access_log /var/log/nginx/access.log main;
|
|
||||||
|
|
||||||
sendfile on;
|
|
||||||
keepalive_timeout 65;
|
|
||||||
|
|
||||||
gzip on;
|
|
||||||
gzip_static on;
|
|
||||||
gzip_vary on;
|
|
||||||
gzip_proxied any;
|
|
||||||
gzip_min_length 1024;
|
|
||||||
gzip_comp_level 6;
|
|
||||||
gzip_types
|
|
||||||
text/plain
|
|
||||||
text/css
|
|
||||||
text/javascript
|
|
||||||
application/javascript
|
|
||||||
application/json
|
|
||||||
application/manifest+json
|
|
||||||
application/xml
|
|
||||||
application/rss+xml
|
|
||||||
image/svg+xml;
|
|
||||||
|
|
||||||
brotli on;
|
|
||||||
brotli_static on;
|
|
||||||
brotli_comp_level 5;
|
|
||||||
brotli_min_length 1024;
|
|
||||||
brotli_types
|
|
||||||
text/plain
|
|
||||||
text/css
|
|
||||||
text/javascript
|
|
||||||
application/javascript
|
|
||||||
application/json
|
|
||||||
application/manifest+json
|
|
||||||
application/xml
|
|
||||||
application/rss+xml
|
|
||||||
image/svg+xml;
|
|
||||||
|
|
||||||
include /etc/nginx/conf.d/*.conf;
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
variable "REGISTRY" {
|
|
||||||
default = "registry.example.com"
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "TAG" {
|
|
||||||
default = "latest"
|
|
||||||
}
|
|
||||||
|
|
||||||
target "_proxy" {
|
|
||||||
context = "."
|
|
||||||
dockerfile = "Proxy/Local/Dockerfile"
|
|
||||||
}
|
|
||||||
|
|
||||||
target "prod" {
|
|
||||||
inherits = ["_proxy"]
|
|
||||||
target = "production"
|
|
||||||
tags = ["moku/work-proxy:local-prod"]
|
|
||||||
}
|
|
||||||
|
|
||||||
target "prod-image" {
|
|
||||||
inherits = ["_proxy"]
|
|
||||||
target = "production"
|
|
||||||
tags = ["${REGISTRY}/moku/work-proxy:prod-${TAG}"]
|
|
||||||
}
|
|
||||||
|
|
||||||
group "local" {
|
|
||||||
targets = ["prod"]
|
|
||||||
}
|
|
||||||
|
|
||||||
group "registry" {
|
|
||||||
targets = ["prod-image"]
|
|
||||||
}
|
|
||||||
|
|
||||||
group "default" {
|
|
||||||
targets = ["prod"]
|
|
||||||
}
|
|
||||||
25
README.md
25
README.md
@@ -1,4 +1,27 @@
|
|||||||
# Moku Work
|
# Moku Base
|
||||||
|
|
||||||
|
Empty scaffold for a Monday-style replacement app.
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
- `Backend/` - blank backend placeholder
|
||||||
|
- `Commands/` - Just submodules and command entrypoints
|
||||||
|
- `Docker/` - local compose files
|
||||||
|
- `Env/` - local environment files
|
||||||
|
- `Frontend/` - SolidStart bare workspace shell
|
||||||
|
- `Proxy/` - blank proxy placeholder
|
||||||
|
|
||||||
|
## Local usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
just --list --list-submodules
|
||||||
|
```
|
||||||
|
|
||||||
|
Main local flow:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
just local dev
|
||||||
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user