Feat: First Commit + Project Scaffolding + Initialize Styling

License Added
This commit is contained in:
MangoPig
2026-06-13 07:21:40 +01:00
commit fa45b46cd2
38 changed files with 7849 additions and 0 deletions

5
Frontend/.dockerignore Normal file
View File

@@ -0,0 +1,5 @@
.DS_Store
.git
.output
dist
node_modules

42
Frontend/Dockerfile Normal file
View File

@@ -0,0 +1,42 @@
# syntax=docker/dockerfile:1.7
FROM node:22-alpine AS base
WORKDIR /app
RUN corepack enable && corepack prepare pnpm@9.0.0 --activate
FROM base AS dependencies
COPY package.json pnpm-lock.yaml tsconfig.json vite.config.ts ./
RUN pnpm install --frozen-lockfile
FROM dependencies AS development
ENV NODE_ENV=development
ENV PORT=3333
COPY . .
EXPOSE 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"]

32
Frontend/README.md Normal file
View File

@@ -0,0 +1,32 @@
# SolidStart
Everything you need to build a Solid project, powered by [`solid-start`](https://start.solidjs.com);
## Creating a project
```bash
# create a new project in the current directory
npm init solid@latest
# create a new project in my-app
npm init solid@latest my-app
```
## Developing
Once you've created a project and installed dependencies with `pnpm install`, start a development server:
```bash
pnpm dev
# or start the server and open the app in a new browser tab
pnpm dev -- --open
```
## Building
Solid apps are built with _presets_, which optimise your project for deployment to different environments.
By default, `pnpm build` will generate a Node app that you can run with `pnpm start`.
## This project was created with the [Solid CLI](https://github.com/solidjs-community/solid-cli)

48
Frontend/docker-bake.hcl Normal file
View File

@@ -0,0 +1,48 @@
variable "REGISTRY" {
default = "registry.example.com"
}
variable "TAG" {
default = "latest"
}
target "_app" {
context = "."
dockerfile = "Dockerfile"
}
target "dev" {
inherits = ["_app"]
target = "development"
tags = ["moku/work-frontend:dev"]
}
target "prod" {
inherits = ["_app"]
target = "production"
tags = ["moku/work-frontend:prod"]
}
target "dev-image" {
inherits = ["_app"]
target = "development"
tags = ["${REGISTRY}/moku/work-frontend:dev-${TAG}"]
}
target "prod-image" {
inherits = ["_app"]
target = "production"
tags = ["${REGISTRY}/moku/work-frontend:prod-${TAG}"]
}
group "local" {
targets = ["dev", "prod"]
}
group "registry" {
targets = ["dev-image", "prod-image"]
}
group "default" {
targets = ["dev"]
}

31
Frontend/package.json Normal file
View File

@@ -0,0 +1,31 @@
{
"name": "moku-frontend",
"type": "module",
"packageManager": "pnpm@9.0.0",
"engines": {
"node": ">=22"
},
"browserslist": [
"defaults"
],
"scripts": {
"dev": "vite dev",
"build": "vite build",
"start": "vite start",
"preview": "vite preview"
},
"dependencies": {
"@solidjs/start": "2.0.0-alpha.2",
"@solidjs/vite-plugin-nitro-2": "^0.1.0",
"postcss": "^8.5.15",
"sass": "^1.101.0",
"solid-js": "^1.9.5",
"vite": "^7.0.0"
},
"devDependencies": {
"autoprefixer": "^10.5.0",
"cssnano": "^8.0.2",
"postcss-preset-env": "^11.3.0",
"sass-embedded": "^1.100.0"
}
}

6961
Frontend/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,5 @@
// Path: Frontend/postcss.config.cjs
module.exports = {
plugins: [require("autoprefixer"), require("postcss-preset-env"), require("cssnano")],
};

BIN
Frontend/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 664 B

7
Frontend/src/app.tsx Normal file
View File

@@ -0,0 +1,7 @@
// Path: Frontend/src/app.tsx
import "./styles/main.scss";
export default function App() {
return <main />;
}

View File

@@ -0,0 +1,4 @@
// @refresh reload
import { mount, StartClient } from "@solidjs/start/client";
mount(() => <StartClient />, document.getElementById("app")!);

View File

@@ -0,0 +1,40 @@
// Path: Frontend/src/entry-server.tsx
// @refresh reload
import { createHandler, StartServer } from "@solidjs/start/server";
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");
}
})();
`;
export default createHandler(() => (
<StartServer
document={({ assets, children, scripts }) => (
<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 innerHTML={themeBootstrapScript} />
{assets}
</head>
<body>
<div id="app">{children}</div>
{scripts}
</body>
</html>
)}
/>
));

1
Frontend/src/global.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="@solidjs/start/env" />

View File

@@ -0,0 +1,22 @@
// Path: Frontend/src/helpers/theme.ts
export type Theme = "light" | "dark";
export const THEME_STORAGE_KEY = "theme";
export const resolvePreferredTheme = (): Theme => {
const stored = localStorage.getItem(THEME_STORAGE_KEY);
if (stored === "light" || stored === "dark") {
return stored;
}
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
};
export const getDocumentTheme = (): Theme => (document.documentElement.getAttribute("data-theme") === "dark" ? "dark" : "light");
export const setTheme = (theme: Theme) => {
document.documentElement.setAttribute("data-theme", theme);
localStorage.setItem(THEME_STORAGE_KEY, theme);
};

View File

@@ -0,0 +1,6 @@
/* Path: Frontend/src/styles/base/_fonts.scss */
:root {
--font-family-sans: "Inter", "Geist", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-family-mono: "Geist Mono", "SFMono-Regular", ui-monospace, "Cascadia Code", "Roboto Mono", monospace;
}

View File

@@ -0,0 +1,100 @@
/* Path: Frontend/src/styles/base/_globals.scss */
@use "../tools/mixins" as *;
html {
background: var(--color-canvas);
color: var(--color-text);
font-family: var(--font-family-sans);
scroll-behavior: smooth;
transition:
background-color var(--motion-duration-base) var(--motion-ease-standard),
color var(--motion-duration-base) var(--motion-ease-standard);
}
body {
background: var(--color-canvas);
color: var(--color-text);
}
a,
button,
input,
textarea,
select {
transition:
background-color var(--motion-duration-fast) var(--motion-ease-standard),
border-color var(--motion-duration-fast) var(--motion-ease-standard),
color var(--motion-duration-fast) var(--motion-ease-standard),
box-shadow var(--motion-duration-fast) var(--motion-ease-standard),
transform var(--motion-duration-fast) var(--motion-ease-standard);
}
a {
color: inherit;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
:focus-visible {
outline: 2px solid var(--color-focus-ring);
outline-offset: 2px;
}
::selection {
background: var(--color-accent-soft);
color: var(--color-text);
}
h1 {
@include text-display;
}
h2 {
@include text-heading;
}
h3 {
@include text-title;
}
h4,
h5,
h6 {
@include text-label;
}
p,
span,
button,
input,
textarea,
label,
li {
@include text-body;
}
code,
pre,
kbd,
samp {
font-family: var(--font-family-mono);
}
#app {
isolation: isolate;
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
scroll-behavior: auto !important;
transition-duration: 0.01ms !important;
}
}

View File

@@ -0,0 +1,69 @@
/* Path: Frontend/src/styles/base/_reset.scss */
*,
*::before,
*::after {
box-sizing: border-box;
}
* {
margin: 0;
}
html {
-webkit-text-size-adjust: 100%;
}
html,
body,
#app {
min-height: 100%;
}
body {
line-height: 1.5;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
img,
picture,
video,
canvas,
svg {
display: block;
max-width: 100%;
}
input,
button,
textarea,
select {
font: inherit;
color: inherit;
}
p,
h1,
h2,
h3,
h4,
h5,
h6 {
overflow-wrap: break-word;
}
ul[role="list"],
ol[role="list"] {
list-style: none;
padding: 0;
}
[hidden] {
display: none !important;
}
button {
border: 0;
background: none;
}

View File

@@ -0,0 +1,9 @@
/* Path: Frontend/src/styles/main.scss */
@use "./base/reset";
@use "./base/fonts";
@use "./base/globals";
@use "./themes/tokens";
@use "./themes/light";
@use "./themes/dark";

View File

@@ -0,0 +1,28 @@
/* Path: Frontend/src/styles/themes/_dark.scss */
[data-theme="dark"] {
color-scheme: dark;
--color-canvas: var(--gray-900);
--color-surface: hsl(220 23% 14% / 0.92);
--color-surface-muted: hsl(220 22% 12% / 0.96);
--color-surface-hover: hsl(220 18% 20% / 0.96);
--color-border: hsl(220 12% 26% / 0.9);
--color-border-strong: hsl(220 12% 38% / 0.9);
--color-text: hsl(210 20% 96%);
--color-text-muted: hsl(220 12% 70%);
--color-accent: hsl(217 91% 67%);
--color-accent-strong: hsl(218 88% 61%);
--color-accent-soft: hsl(217 91% 67% / 0.18);
--color-accent-contrast: hsl(220 28% 10%);
--color-success: hsl(154 55% 48%);
--color-danger: hsl(0 72% 62%);
--color-warning: hsl(36 100% 60%);
--color-focus-ring: hsl(217 91% 67% / 0.65);
--shadow-soft: 0 16px 40px hsl(220 40% 3% / 0.45);
--shadow-strong: 0 24px 60px hsl(220 40% 3% / 0.55);
}

View File

@@ -0,0 +1,26 @@
/* Path: Frontend/src/styles/themes/_light.scss */
:root,
[data-theme="light"] {
color-scheme: light;
--color-canvas: var(--gray-50);
--color-surface: hsl(0 0% 100% / 0.9);
--color-surface-muted: var(--gray-0);
--color-surface-hover: var(--gray-100);
--color-border: hsl(220 15% 85% / 0.9);
--color-border-strong: hsl(220 12% 70% / 0.9);
--color-text: var(--gray-800);
--color-text-muted: var(--gray-500);
--color-accent: var(--blue-500);
--color-accent-strong: var(--blue-600);
--color-accent-soft: hsl(218 88% 61% / 0.12);
--color-accent-contrast: hsl(0 0% 100%);
--color-success: var(--green-500);
--color-danger: var(--red-500);
--color-warning: var(--amber-500);
--color-focus-ring: hsl(221 83% 53% / 0.55);
}

View File

@@ -0,0 +1,54 @@
/* Path: Frontend/src/styles/themes/_tokens.scss */
:root {
--gray-0: hsl(210 20% 99%);
--gray-50: hsl(220 20% 97%);
--gray-100: hsl(220 16% 93%);
--gray-200: hsl(220 13% 87%);
--gray-300: hsl(220 11% 75%);
--gray-400: hsl(220 9% 58%);
--gray-500: hsl(220 10% 45%);
--gray-600: hsl(220 14% 34%);
--gray-700: hsl(220 18% 24%);
--gray-800: hsl(220 22% 16%);
--gray-900: hsl(220 28% 10%);
--blue-400: hsl(218 88% 61%);
--blue-500: hsl(221 83% 53%);
--blue-600: hsl(224 76% 48%);
--green-500: hsl(154 60% 40%);
--red-500: hsl(0 72% 54%);
--amber-500: hsl(36 100% 50%);
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-5: 1.25rem;
--space-6: 1.5rem;
--space-8: 2rem;
--space-10: 2.5rem;
--space-12: 3rem;
--radius-sm: 0.375rem;
--radius-md: 0.625rem;
--radius-lg: 0.875rem;
--radius-xl: 1.25rem;
--radius-pill: 999px;
--shadow-soft: 0 12px 32px hsl(220 30% 10% / 0.08);
--shadow-strong: 0 20px 48px hsl(220 30% 10% / 0.16);
--z-base: 1;
--z-dropdown: 100;
--z-sticky: 200;
--z-overlay: 400;
--z-modal: 500;
--z-toast: 600;
--motion-duration-fast: 140ms;
--motion-duration-base: 220ms;
--motion-duration-slow: 320ms;
--motion-ease-standard: cubic-bezier(0.2, 0.8, 0.2, 1);
}

View File

@@ -0,0 +1,5 @@
/* Path: Frontend/src/styles/tools/_breakpoints.scss */
$breakpoint-mobile: 48rem;
$breakpoint-tablet: 64rem;
$breakpoint-desktop: 90rem;

View File

@@ -0,0 +1,5 @@
/* Path: Frontend/src/styles/tools/_functions.scss */
@function rem($pixels, $base: 16) {
@return ($pixels / $base) * 1rem;
}

View File

@@ -0,0 +1,55 @@
/* Path: Frontend/src/styles/tools/_mixins.scss */
@use "./breakpoints" as *;
@mixin respond-up($breakpoint) {
@if $breakpoint == mobile {
@media (min-width: $breakpoint-mobile) {
@content;
}
} @else if $breakpoint == tablet {
@media (min-width: $breakpoint-tablet) {
@content;
}
} @else if $breakpoint == desktop {
@media (min-width: $breakpoint-desktop) {
@content;
}
}
}
@mixin text-caption {
font-size: clamp(0.75rem, 0.72rem + 0.12vw, 0.875rem);
font-weight: 500;
line-height: 1.4;
}
@mixin text-label {
font-size: clamp(0.875rem, 0.84rem + 0.15vw, 1rem);
font-weight: 600;
line-height: 1.4;
}
@mixin text-body {
font-size: clamp(0.95rem, 0.92rem + 0.18vw, 1.05rem);
font-weight: 400;
line-height: 1.6;
}
@mixin text-title {
font-size: clamp(1.1rem, 1rem + 0.4vw, 1.35rem);
font-weight: 600;
line-height: 1.25;
}
@mixin text-heading {
font-size: clamp(1.45rem, 1.2rem + 1vw, 2rem);
font-weight: 650;
line-height: 1.1;
}
@mixin text-display {
font-size: clamp(2rem, 1.45rem + 2.1vw, 3.5rem);
font-weight: 700;
line-height: 1;
}

19
Frontend/tsconfig.json Normal file
View File

@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"jsx": "preserve",
"jsxImportSource": "solid-js",
"allowJs": true,
"strict": true,
"noEmit": true,
"types": ["vite/client"],
"isolatedModules": true,
"paths": {
"~/*": ["./src/*"]
}
}
}

17
Frontend/vite.config.ts Normal file
View File

@@ -0,0 +1,17 @@
// Path: Frontend/vite.config.ts
import { nitroV2Plugin as nitro } from "@solidjs/vite-plugin-nitro-2";
import { defineConfig } from "vite";
import { solidStart } from "@solidjs/start/config";
export default defineConfig({
css: {
preprocessorOptions: {
scss: {
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()],
});