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>
11
.gitignore
vendored
Normal 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
@@ -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
51
package.json
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,2 @@
|
||||
// @gsc/web-kit/forms — Phase 1 stub. Real surface lands in later phases.
|
||||
export {};
|
||||
15
src/index.ts
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
BIN
src/styles/images/boxed_bg.png
Executable file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
src/styles/images/layers-2x.png
Executable file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
src/styles/images/layers.png
Executable file
|
After Width: | Height: | Size: 696 B |
BIN
src/styles/images/login_cover.jpg
Executable file
|
After Width: | Height: | Size: 202 KiB |
BIN
src/styles/images/marker-icon-2x.png
Executable file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
src/styles/images/marker-icon.png
Executable file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
src/styles/images/marker-shadow.png
Executable file
|
After Width: | Height: | Size: 618 B |
16
src/styles/index.css
Normal 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";
|
||||
187
src/styles/sidebar-overrides.css
Normal 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
@@ -0,0 +1,2 @@
|
||||
// @gsc/web-kit/utils — Phase 1 stub. Real surface lands in later phases.
|
||||
export {};
|
||||
24
tsconfig.json
Normal 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"]
|
||||
}
|
||||