feat: @gsc/web-kit v0.1.0 — Phase 1 scaffold

The kit. Drop into any GSC Next.js frontend; everything that's not
domain content lives here. Wraps @limitless/ui primitives with the
app-shaped patterns we keep reimplementing: layout, auth, data
display, forms, feedback, navigation.

Phase 1 ships the package skeleton:

- package.json with 14 sub-exports (./layout · ./auth · ./auth/server
  · ./auth/middleware · ./shell · ./shell/server · ./data · ./forms ·
  ./feedback · ./navigation · ./api · ./utils + the root and ./css).
- Empty module stubs so the import map resolves while later phases
  fill in real surface area.
- Canonical CSS bundle at @gsc/web-kit/css — all.min.css +
  sidebar-overrides.css + the seven layout-3 background images,
  copied from chronos and committed in one place so no app has to
  ship the 1MB sidecar on its own anymore.
- tsc-based build + a postbuild script that mirrors @limitless/ui:
  emits .js + .d.ts, copies styles/, rewrites bare ESM imports to
  include .js extensions.
- Peer deps on next, react, react-dom, bootstrap.
- Hard deps on @limitless/ui (file: dep), next-auth, next-intl, zod.

Build verified: tsc emits, all 14 export paths resolve under dist/.
No functional code yet — Phase 2 lands AppLayout / createAuth /
fetchShellConfig and the gscCRM pilot cuts over.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude
2026-05-11 00:09:36 +02:00
commit 957880e5c5
29 changed files with 34491 additions and 0 deletions

11
.gitignore vendored Normal file
View File

@@ -0,0 +1,11 @@
# Build output (regenerated by `npm run build`)
/dist/
*.tsbuildinfo
# Deps
/node_modules/
# Editor / OS
.vscode/
.idea/
.DS_Store

50
README.md Normal file
View File

@@ -0,0 +1,50 @@
# `@gsc/web-kit`
App skeleton for GSC Next.js frontends. Wraps `@limitless/ui` primitives into a pre-configured layout + auth + data/forms/feedback stack so apps just write their domain pages.
See the implementation plan in the parent repo for the full module map. This is a `file:` dep consumed by every GSC frontend.
## Install (in a consumer app)
```jsonc
// package.json
{
"dependencies": {
"@gsc/web-kit": "file:../../../templates/gsc-web-kit"
}
}
```
## Layered architecture
```
your app
└── @gsc/web-kit ← this package (layout, auth, data, forms…)
└── @limitless/ui ← Bootstrap-flavoured primitives
└── bootstrap
```
## Sub-exports
```ts
import "@gsc/web-kit/css"; // CSS bundle (layout-3 + JetBrains Mono)
import { AppLayout } from "@gsc/web-kit/layout";
import { createAuth } from "@gsc/web-kit/auth/server";
import { fetchShellConfig } from "@gsc/web-kit/shell/server";
import { EntityList } from "@gsc/web-kit/data";
import { Form, TextField } from "@gsc/web-kit/forms";
import { useToast } from "@gsc/web-kit/feedback";
import { Breadcrumbs } from "@gsc/web-kit/navigation";
import { createApiClient } from "@gsc/web-kit/api";
import { formatCurrency } from "@gsc/web-kit/utils";
```
## Phases
| Phase | Scope | Status |
|---|---|---|
| 1 | Package scaffold + CSS bundle + sub-export stubs | **in progress** |
| 2 | layout · auth · shell — usable end-to-end with shell-api | planned |
| 3 | data · forms — EntityList, Form, DefineAction | planned |
| 4 | feedback · navigation · api · utils | planned |
| 5 | Roll out to gscCRM / gscChronos / gscAdmin / gscPortal | planned |

1913
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

51
package.json Normal file
View File

@@ -0,0 +1,51 @@
{
"name": "@gsc/web-kit",
"version": "0.1.0",
"description": "GSC web app skeleton — layout, auth, data, forms, feedback, navigation. Built on @limitless/ui. Drop into a Next.js app and just write pages.",
"license": "MIT",
"type": "module",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": { "import": "./dist/index.js", "types": "./dist/index.d.ts" },
"./css": "./dist/styles/index.css",
"./layout": { "import": "./dist/layout/index.js", "types": "./dist/layout/index.d.ts" },
"./auth": { "import": "./dist/auth/index.js", "types": "./dist/auth/index.d.ts" },
"./auth/server":{ "import": "./dist/auth/server.js", "types": "./dist/auth/server.d.ts" },
"./auth/middleware": { "import": "./dist/auth/middleware.js", "types": "./dist/auth/middleware.d.ts" },
"./shell": { "import": "./dist/shell/index.js", "types": "./dist/shell/index.d.ts" },
"./shell/server":{ "import": "./dist/shell/server.js", "types": "./dist/shell/server.d.ts" },
"./data": { "import": "./dist/data/index.js", "types": "./dist/data/index.d.ts" },
"./forms": { "import": "./dist/forms/index.js", "types": "./dist/forms/index.d.ts" },
"./feedback": { "import": "./dist/feedback/index.js", "types": "./dist/feedback/index.d.ts" },
"./navigation": { "import": "./dist/navigation/index.js", "types": "./dist/navigation/index.d.ts" },
"./api": { "import": "./dist/api/index.js", "types": "./dist/api/index.d.ts" },
"./utils": { "import": "./dist/utils/index.js", "types": "./dist/utils/index.d.ts" }
},
"sideEffects": [
"**/*.css"
],
"scripts": {
"build": "tsc -p tsconfig.json --noEmit false",
"postbuild": "node ./scripts/postbuild.cjs",
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@limitless/ui": "file:../limitless-ui",
"next-auth": "^5.0.0-beta.25",
"next-intl": "^4.6.1",
"zod": "^3.23.0"
},
"peerDependencies": {
"bootstrap": "^5.3.3",
"next": ">=15.0.0",
"react": "^18.2.0 || ^19.0.0",
"react-dom": "^18.2.0 || ^19.0.0"
},
"devDependencies": {
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"typescript": "^5.4.0"
}
}

65
scripts/postbuild.cjs Normal file
View File

@@ -0,0 +1,65 @@
/* Post-build: copy CSS + image assets into dist/, then fix ESM imports.
*
* tsc only emits .js + .d.ts. The CSS lives in src/styles/ alongside an
* `index.css` that @imports the other sheets; we copy the whole tree
* to dist/ so `@gsc/web-kit/css` resolves correctly.
*/
const fs = require("fs");
const path = require("path");
const root = path.join(__dirname, "..");
const distDir = path.join(root, "dist");
const srcStyles = path.join(root, "src", "styles");
const distStyles = path.join(distDir, "styles");
// 1. Mirror src/styles/ → dist/styles/ (CSS + images).
function copyDir(src, dst) {
fs.mkdirSync(dst, { recursive: true });
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
const s = path.join(src, entry.name);
const d = path.join(dst, entry.name);
if (entry.isDirectory()) copyDir(s, d);
else fs.copyFileSync(s, d);
}
}
if (fs.existsSync(srcStyles)) {
copyDir(srcStyles, distStyles);
}
// 2. Fix ESM relative imports: append `.js` to bare specifiers like
// `from './layout/AppLayout'` so the resolver can find the file.
function fixImports(dir) {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
fixImports(full);
continue;
}
if (!entry.name.endsWith(".js")) continue;
const content = fs.readFileSync(full, "utf8");
const fixed = content.replace(
/((?:from|import)\s+['"])(\.{1,2}\/[^'"]+?)(['"])/g,
(match, prefix, rel, quote) => {
if (rel.endsWith(".js") || rel.endsWith(".css")) return match;
const absDir = path.dirname(full);
const target = path.resolve(absDir, rel);
if (fs.existsSync(target) && fs.statSync(target).isDirectory()) {
if (fs.existsSync(path.join(target, "index.js"))) {
return `${prefix}${rel}/index.js${quote}`;
}
}
if (fs.existsSync(`${target}.js`)) {
return `${prefix}${rel}.js${quote}`;
}
return match;
},
);
if (fixed !== content) fs.writeFileSync(full, fixed);
}
}
if (fs.existsSync(distDir)) {
fixImports(distDir);
}
console.log("Post-build: CSS copied, ESM imports fixed");

2
src/api/index.ts Normal file
View File

@@ -0,0 +1,2 @@
// @gsc/web-kit/api — Phase 1 stub. Real surface lands in later phases.
export {};

2
src/auth/index.ts Normal file
View File

@@ -0,0 +1,2 @@
// @gsc/web-kit/auth — Phase 1 stub. Real surface lands in later phases.
export {};

2
src/auth/middleware.ts Normal file
View File

@@ -0,0 +1,2 @@
// @gsc/web-kit/auth/middleware — Phase 1 stub. Real createAuthMiddleware() lands in Phase 2.
export {};

2
src/auth/server.ts Normal file
View File

@@ -0,0 +1,2 @@
// @gsc/web-kit/auth/server — Phase 1 stub. Real createAuth() / requireAuth() lands in Phase 2.
export {};

2
src/data/index.ts Normal file
View File

@@ -0,0 +1,2 @@
// @gsc/web-kit/data — Phase 1 stub. Real surface lands in later phases.
export {};

2
src/feedback/index.ts Normal file
View File

@@ -0,0 +1,2 @@
// @gsc/web-kit/feedback — Phase 1 stub. Real surface lands in later phases.
export {};

2
src/forms/index.ts Normal file
View File

@@ -0,0 +1,2 @@
// @gsc/web-kit/forms — Phase 1 stub. Real surface lands in later phases.
export {};

15
src/index.ts Normal file
View File

@@ -0,0 +1,15 @@
// @gsc/web-kit — top-level re-exports.
//
// Prefer the sub-exports (`@gsc/web-kit/layout`, `…/auth`, etc.) so
// tree-shaking can drop modules you don't use. This index is here
// for convenience and discovery.
export * from "./layout/index";
export * from "./shell/index";
export * from "./data/index";
export * from "./forms/index";
export * from "./feedback/index";
export * from "./navigation/index";
export * from "./api/index";
export * from "./utils/index";
// auth is client-only; auth/server is server-only. Don't aggregate.

2
src/layout/index.ts Normal file
View File

@@ -0,0 +1,2 @@
// @gsc/web-kit/layout — Phase 1 stub. Real surface lands in later phases.
export {};

2
src/navigation/index.ts Normal file
View File

@@ -0,0 +1,2 @@
// @gsc/web-kit/navigation — Phase 1 stub. Real surface lands in later phases.
export {};

2
src/shell/index.ts Normal file
View File

@@ -0,0 +1,2 @@
// @gsc/web-kit/shell — Phase 1 stub. Real surface lands in later phases.
export {};

2
src/shell/server.ts Normal file
View File

@@ -0,0 +1,2 @@
// @gsc/web-kit/shell/server — Phase 1 stub. Real fetchShellConfig() lands in Phase 2.
export {};

32135
src/styles/all.min.css vendored Executable file

File diff suppressed because it is too large Load Diff

BIN
src/styles/images/boxed_bg.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

BIN
src/styles/images/layers-2x.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
src/styles/images/layers.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 B

BIN
src/styles/images/login_cover.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
src/styles/images/marker-icon.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 B

16
src/styles/index.css Normal file
View File

@@ -0,0 +1,16 @@
/* @gsc/web-kit canonical stylesheet.
*
* Consumers should ALSO import (in this order, before this file):
* import "bootstrap/dist/css/bootstrap.min.css";
* import "@limitless/ui/css";
* import "@gsc/web-kit/css";
* import "@phosphor-icons/web/regular";
*
* The two sheets below own the layout-3 chrome (sidebar/page-content/
* navbar-footer rules and the JetBrains Mono font face). Bundlers
* inline them; the image refs in all.min.css resolve to ./images/
* relative to this file.
*/
@import "./all.min.css";
@import "./sidebar-overrides.css";

View File

@@ -0,0 +1,187 @@
@font-face {
font-family: "JetBrains Mono";
font-style: normal;
font-weight: 200;
font-display: swap;
src: url("https://assets.gosec.cloud/fonts/jetbrains/JetBrainsMono-ExtraLight.woff2") format("woff2");
}
@font-face {
font-family: "JetBrains Mono";
font-style: italic;
font-weight: 200;
font-display: swap;
src: url("https://assets.gosec.cloud/fonts/jetbrains/JetBrainsMono-ExtraLightItalic.woff2") format("woff2");
}
@font-face {
font-family: "JetBrains Mono";
font-style: normal;
font-weight: 300;
font-display: swap;
src: url("https://assets.gosec.cloud/fonts/jetbrains/JetBrainsMono-Light.woff2") format("woff2");
}
@font-face {
font-family: "JetBrains Mono";
font-style: italic;
font-weight: 300;
font-display: swap;
src: url("https://assets.gosec.cloud/fonts/jetbrains/JetBrainsMono-LightItalic.woff2") format("woff2");
}
@font-face {
font-family: "JetBrains Mono";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("https://assets.gosec.cloud/fonts/jetbrains/JetBrainsMono-Regular.woff2") format("woff2");
}
@font-face {
font-family: "JetBrains Mono";
font-style: italic;
font-weight: 400;
font-display: swap;
src: url("https://assets.gosec.cloud/fonts/jetbrains/JetBrainsMono-Italic.woff2") format("woff2");
}
@font-face {
font-family: "JetBrains Mono";
font-style: normal;
font-weight: 500;
font-display: swap;
src: url("https://assets.gosec.cloud/fonts/jetbrains/JetBrainsMono-Medium.woff2") format("woff2");
}
@font-face {
font-family: "JetBrains Mono";
font-style: italic;
font-weight: 500;
font-display: swap;
src: url("https://assets.gosec.cloud/fonts/jetbrains/JetBrainsMono-MediumItalic.woff2") format("woff2");
}
@font-face {
font-family: "JetBrains Mono";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("https://assets.gosec.cloud/fonts/jetbrains/JetBrainsMono-SemiBold.woff2") format("woff2");
}
@font-face {
font-family: "JetBrains Mono";
font-style: italic;
font-weight: 600;
font-display: swap;
src: url("https://assets.gosec.cloud/fonts/jetbrains/JetBrainsMono-SemiBoldItalic.woff2") format("woff2");
}
@font-face {
font-family: "JetBrains Mono";
font-style: normal;
font-weight: 700;
font-display: swap;
src: url("https://assets.gosec.cloud/fonts/jetbrains/JetBrainsMono-Bold.woff2") format("woff2");
}
@font-face {
font-family: "JetBrains Mono";
font-style: italic;
font-weight: 700;
font-display: swap;
src: url("https://assets.gosec.cloud/fonts/jetbrains/JetBrainsMono-BoldItalic.woff2") format("woff2");
}
@font-face {
font-family: "JetBrains Mono";
font-style: normal;
font-weight: 800;
font-display: swap;
src: url("https://assets.gosec.cloud/fonts/jetbrains/JetBrainsMono-ExtraBold.woff2") format("woff2");
}
@font-face {
font-family: "JetBrains Mono";
font-style: italic;
font-weight: 800;
font-display: swap;
src: url("https://assets.gosec.cloud/fonts/jetbrains/JetBrainsMono-ExtraBoldItalic.woff2") format("woff2");
}
body,
html {
font-family: "JetBrains Mono", monospace !important;
}
:root {
--body-font-family: "JetBrains Mono", monospace;
--font-sans-serif: "JetBrains Mono", monospace;
}
.sidebar-main-resized .nav-sidebar > .nav-item > .nav-link,
.sidebar-main-resized .nav-sidebar > .nav-item-submenu > .nav-link {
display: flex;
align-items: center;
justify-content: center !important;
padding-left: 0 !important;
padding-right: 0 !important;
}
.sidebar-main-resized .nav-sidebar > .nav-item > .nav-link i,
.sidebar-main-resized .nav-sidebar > .nav-item-submenu > .nav-link i {
margin-top: 0 !important;
margin-bottom: 0 !important;
margin-left: 0 !important;
margin-right: 0 !important;
float: none !important;
}
.sidebar-main-resized .nav-item-submenu > .nav-link:after {
content: none !important;
display: none !important;
}
.sidebar-main-resized .nav-item-submenu {
position: relative;
}
.sidebar-main-resized .nav-item-submenu > .nav-group-sub.nav-group-sub-flyout {
position: absolute;
top: calc(var(--nav-link-padding-y) * -1);
left: 100%;
right: auto;
width: var(--ll-sidebar-width, 18.75rem);
background-color: var(--sidebar-bg, #fff);
border: 1px solid var(--border-color-translucent, #dee2e6);
box-shadow: var(--box-shadow, 0 0 1rem rgba(0, 0, 0, 0.15));
border-radius: var(--border-radius, 0.25rem);
padding-left: 0;
list-style: none;
display: none !important;
z-index: 1000;
}
.sidebar-main-resized
.nav-item-submenu
> .nav-group-sub.nav-group-sub-flyout[data-submenu-title]:before {
content: attr(data-submenu-title);
display: block;
padding: var(--nav-link-padding-y) var(--nav-link-padding-x);
padding-bottom: 0;
margin-top: var(--nav-link-padding-y);
opacity: 0.5;
}
.sidebar-main-resized
.nav-item-submenu.nav-group-sub-visible
> .nav-group-sub,
.sidebar-main-resized
.nav-item-submenu:hover
> .nav-group-sub,
.sidebar-main-resized
.nav-item-submenu:focus-within
> .nav-group-sub {
display: block !important;
}

2
src/utils/index.ts Normal file
View File

@@ -0,0 +1,2 @@
// @gsc/web-kit/utils — Phase 1 stub. Real surface lands in later phases.
export {};

24
tsconfig.json Normal file
View File

@@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "ES2019",
"module": "ESNext",
"moduleResolution": "Node",
"jsx": "react-jsx",
"strict": true,
"declaration": true,
"declarationDir": "dist",
"emitDeclarationOnly": false,
"outDir": "dist",
"rootDir": "src",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"lib": ["dom", "dom.iterable", "esnext"],
"noEmit": true,
"incremental": true,
"isolatedModules": true
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}