From d430680df59a2be99aafc8238ca1e722fdca45f4 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 11 May 2026 08:26:30 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20v0.3.0=20=E2=80=94=20Phase=203/4=20fa?= =?UTF-8?q?=C3=A7ades=20+=20AppLayout=20on=20AppShell?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Curated re-exports from @limitless/ui through /forms, /data, /feedback, /navigation, /utils sub-paths so apps stop importing from the lower layer. - /forms also re-exports the full @limitless/ui validation surface (hooks, format/security/address validators, types). - AppLayout is now a thin wrapper over @limitless/ui's — same ShellConfig DTO, no duplicated chrome code. - shell/types + shell/index re-export from @limitless/ui to keep one canonical type and one shared context. - auth middleware: loose NextRequestLike typing to avoid two-copies- of-next conflict with the consumer's next. - postbuild: rewrite ../images/ to ./images/ in copied CSS so refs resolve in dist/styles/. Widget family in /data is the intersection of limitless's two Widget files (Widget.d.ts vs Widget/index.d.ts collision in dist); upstream fix needed before exposing IconWidget/UserWidget/etc. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 65 +++++-- package-lock.json | 148 ++++++--------- package.json | 77 ++++++-- scripts/postbuild.cjs | 12 ++ src/auth/middleware.ts | 16 +- src/data/index.ts | 39 +++- src/feedback/index.ts | 27 ++- src/forms/index.ts | 148 ++++++++++++++- src/layout/AppLayout.tsx | 390 +-------------------------------------- src/navigation/index.ts | 28 ++- src/shell/index.ts | 22 +-- src/shell/types.ts | 59 ++---- src/utils/index.ts | 9 +- 13 files changed, 460 insertions(+), 580 deletions(-) diff --git a/README.md b/README.md index eb91969..86ea3dc 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # `@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. +App skeleton for GSC Next.js frontends. Curates `@limitless/ui` primitives behind a pre-configured layout + auth + data/forms/feedback/navigation 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. @@ -28,23 +28,64 @@ your app ```ts import "@gsc/web-kit/css"; // CSS bundle (layout-3 + JetBrains Mono) + +// Chrome import { AppLayout } from "@gsc/web-kit/layout"; -import { createAuth } from "@gsc/web-kit/auth/server"; +import { useShell } from "@gsc/web-kit/shell"; 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"; + +// Auth (NextAuth v5 + Keycloak) +import { createAuth } from "@gsc/web-kit/auth/server"; +import { createAuthMiddleware } from "@gsc/web-kit/auth/middleware"; +import { signInRedirect } from "@gsc/web-kit/auth"; + +// Building blocks — curated re-exports from @limitless/ui +import { + Table, DataTable, Pagination, TreeView, Timeline, + Calendar, Gallery, Sortable, ListGroup, + StatWidget, ProgressWidget, ChartWidget, ContentWidget, +} from "@gsc/web-kit/data"; + +import { + FormGroup, FormControl, FormCheck, Select, InputGroup, + SelectSingle, MultiSelect, TagsSelect, AsyncSelect, + DatePicker, ColorPicker, TagInput, FileUpload, + Slider, Rating, DualListBox, ImageCropper, Wizard, Stepper, + // validation + useValidation, useFieldValidation, required, email, password, + noInjection, europeanAddress, /* …etc */ +} from "@gsc/web-kit/forms"; + +import { + Alert, Toast, Notification, Modal, Offcanvas, + Popover, Tooltip, SweetAlert, Spinner, + Progress, ProgressStacked, IdleTimeout, FAB, +} from "@gsc/web-kit/feedback"; + +import { + Breadcrumbs, Nav, Tabs, Pills, Dropdown, ContextMenu, + Scrollspy, PageHeader, Accordion, Collapse, Carousel, + Embed, SyntaxHighlighter, Card, Badge, Button, Media, +} from "@gsc/web-kit/navigation"; + +import { useDisclosure } from "@gsc/web-kit/utils"; ``` +The `/api` sub-export is reserved for a future HTTP client helper; it currently re-exports nothing. + ## 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 | +| 1 | Package scaffold + CSS bundle + sub-export stubs | **done** | +| 2 | layout · auth · shell — usable end-to-end with shell-api | **done** | +| 3 | data · forms — curated re-exports from limitless + validation | **done (v0.3.0)** | +| 4 | feedback · navigation · utils — curated re-exports from limitless | **done (v0.3.0)** | +| 4a | api · HTTP client helper (Bearer injection, 401 → signInRedirect) | planned | | 5 | Roll out to gscCRM / gscChronos / gscAdmin / gscPortal | planned | + +## Notes + +- `AppLayout` is a thin wrapper around `` from `@limitless/ui` — they share the `ShellConfig` DTO, so the kit owns the consumer-facing surface without duplicating chrome code. +- All form, data, feedback, and navigation modules are **curated re-exports**: apps should never need to reach into `@limitless/ui` directly. +- The `Widget` family in `/data` is intentionally narrow (`StatWidget`, `ProgressWidget`, `ChartWidget`, `ContentWidget`) — the installed limitless dist has a duplicate `Widget` file/folder collision, so only names exported by both are passed through. diff --git a/package-lock.json b/package-lock.json index ebc0a2c..7ebfc65 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@types/node": "^20.11.0", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", + "next": "16.1.1", "typescript": "^5.4.0" }, "peerDependencies": { @@ -25,6 +26,11 @@ "next": ">=15.0.0", "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "bootstrap": { + "optional": true + } } }, "../limitless-ui": { @@ -87,7 +93,6 @@ "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -128,7 +133,6 @@ "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">=18" } @@ -145,7 +149,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -168,7 +171,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -191,7 +193,6 @@ "os": [ "darwin" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -208,7 +209,6 @@ "os": [ "darwin" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -225,7 +225,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -242,7 +241,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -259,7 +257,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -276,7 +273,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -293,7 +289,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -310,7 +305,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -327,7 +321,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -344,7 +337,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -361,7 +353,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -384,7 +375,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -407,7 +397,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -430,7 +419,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -453,7 +441,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -476,7 +463,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -499,7 +485,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -522,7 +507,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -542,7 +526,6 @@ ], "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, - "peer": true, "dependencies": { "@emnapi/runtime": "^1.7.0" }, @@ -565,7 +548,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -585,7 +567,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -605,7 +586,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -618,16 +598,15 @@ "link": true }, "node_modules/@next/env": { - "version": "16.2.6", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.6.tgz", - "integrity": "sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw==", - "license": "MIT", - "peer": true + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.1.tgz", + "integrity": "sha512-3oxyM97Sr2PqiVyMyrZUtrtM3jqqFxOQJVuKclDsgj/L728iZt/GyslkN4NwarledZATCenbk4Offjk1hQmaAA==", + "license": "MIT" }, "node_modules/@next/swc-darwin-arm64": { - "version": "16.2.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.6.tgz", - "integrity": "sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg==", + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.1.tgz", + "integrity": "sha512-JS3m42ifsVSJjSTzh27nW+Igfha3NdBOFScr9C80hHGrWx55pTrVL23RJbqir7k7/15SKlrLHhh/MQzqBBYrQA==", "cpu": [ "arm64" ], @@ -636,15 +615,14 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 10" } }, "node_modules/@next/swc-darwin-x64": { - "version": "16.2.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.6.tgz", - "integrity": "sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ==", + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.1.tgz", + "integrity": "sha512-hbyKtrDGUkgkyQi1m1IyD3q4I/3m9ngr+V93z4oKHrPcmxwNL5iMWORvLSGAf2YujL+6HxgVvZuCYZfLfb4bGw==", "cpu": [ "x64" ], @@ -653,15 +631,14 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 10" } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.2.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.6.tgz", - "integrity": "sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w==", + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.1.tgz", + "integrity": "sha512-/fvHet+EYckFvRLQ0jPHJCUI5/B56+2DpI1xDSvi80r/3Ez+Eaa2Yq4tJcRTaB1kqj/HrYKn8Yplm9bNoMJpwQ==", "cpu": [ "arm64" ], @@ -670,15 +647,14 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10" } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.2.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.6.tgz", - "integrity": "sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==", + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.1.tgz", + "integrity": "sha512-MFHrgL4TXNQbBPzkKKur4Fb5ICEJa87HM7fczFs2+HWblM7mMLdco3dvyTI+QmLBU9xgns/EeeINSZD6Ar+oLg==", "cpu": [ "arm64" ], @@ -687,15 +663,14 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10" } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.2.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.6.tgz", - "integrity": "sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==", + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.1.tgz", + "integrity": "sha512-20bYDfgOQAPUkkKBnyP9PTuHiJGM7HzNBbuqmD0jiFVZ0aOldz+VnJhbxzjcSabYsnNjMPsE0cyzEudpYxsrUQ==", "cpu": [ "x64" ], @@ -704,15 +679,14 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10" } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "16.2.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.6.tgz", - "integrity": "sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==", + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.1.tgz", + "integrity": "sha512-9pRbK3M4asAHQRkwaXwu601oPZHghuSC8IXNENgbBSyImHv/zY4K5udBusgdHkvJ/Tcr96jJwQYOll0qU8+fPA==", "cpu": [ "x64" ], @@ -721,15 +695,14 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10" } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.2.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.6.tgz", - "integrity": "sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==", + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.1.tgz", + "integrity": "sha512-bdfQkggaLgnmYrFkSQfsHfOhk/mCYmjnrbRCGgkMcoOBZ4n+TRRSLmT/CU5SATzlBJ9TpioUyBW/vWFXTqQRiA==", "cpu": [ "arm64" ], @@ -738,15 +711,14 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 10" } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.2.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.6.tgz", - "integrity": "sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA==", + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.1.tgz", + "integrity": "sha512-Ncwbw2WJ57Al5OX0k4chM68DKhEPlrXBaSXDCi2kPi5f4d8b3ejr3RRJGfKBLrn2YJL5ezNS7w2TZLHSti8CMw==", "cpu": [ "x64" ], @@ -755,7 +727,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 10" } @@ -1069,6 +1040,7 @@ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", "license": "MIT", + "optional": true, "peer": true, "funding": { "type": "opencollective", @@ -1284,7 +1256,6 @@ "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.8.0" } @@ -1333,7 +1304,6 @@ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.29.tgz", "integrity": "sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==", "license": "Apache-2.0", - "peer": true, "bin": { "baseline-browser-mapping": "dist/cli.cjs" }, @@ -1356,6 +1326,7 @@ } ], "license": "MIT", + "optional": true, "peer": true, "peerDependencies": { "@popperjs/core": "^2.11.8" @@ -1379,15 +1350,13 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "CC-BY-4.0", - "peer": true + "license": "CC-BY-4.0" }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/csstype": { "version": "3.2.3", @@ -1471,7 +1440,6 @@ } ], "license": "MIT", - "peer": true, "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -1489,15 +1457,14 @@ } }, "node_modules/next": { - "version": "16.2.6", - "resolved": "https://registry.npmjs.org/next/-/next-16.2.6.tgz", - "integrity": "sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==", + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/next/-/next-16.1.1.tgz", + "integrity": "sha512-QI+T7xrxt1pF6SQ/JYFz95ro/mg/1Znk5vBebsWwbpejj1T0A23hO7GYEaVac9QUOT2BIMiuzm0L99ooq7k0/w==", "license": "MIT", - "peer": true, "dependencies": { - "@next/env": "16.2.6", + "@next/env": "16.1.1", "@swc/helpers": "0.5.15", - "baseline-browser-mapping": "^2.9.19", + "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" @@ -1509,15 +1476,15 @@ "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "16.2.6", - "@next/swc-darwin-x64": "16.2.6", - "@next/swc-linux-arm64-gnu": "16.2.6", - "@next/swc-linux-arm64-musl": "16.2.6", - "@next/swc-linux-x64-gnu": "16.2.6", - "@next/swc-linux-x64-musl": "16.2.6", - "@next/swc-win32-arm64-msvc": "16.2.6", - "@next/swc-win32-x64-msvc": "16.2.6", - "sharp": "^0.34.5" + "@next/swc-darwin-arm64": "16.1.1", + "@next/swc-darwin-x64": "16.1.1", + "@next/swc-linux-arm64-gnu": "16.1.1", + "@next/swc-linux-arm64-musl": "16.1.1", + "@next/swc-linux-x64-gnu": "16.1.1", + "@next/swc-linux-x64-musl": "16.1.1", + "@next/swc-win32-arm64-msvc": "16.1.1", + "@next/swc-win32-x64-msvc": "16.1.1", + "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", @@ -1676,8 +1643,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/picomatch": { "version": "4.0.4", @@ -1716,7 +1682,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", @@ -1781,7 +1746,6 @@ "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", "license": "ISC", "optional": true, - "peer": true, "bin": { "semver": "bin/semver.js" }, @@ -1796,7 +1760,6 @@ "hasInstallScript": true, "license": "Apache-2.0", "optional": true, - "peer": true, "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", @@ -1840,7 +1803,6 @@ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "license": "BSD-3-Clause", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -1850,7 +1812,6 @@ "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", "license": "MIT", - "peer": true, "dependencies": { "client-only": "0.0.1" }, @@ -1873,8 +1834,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/typescript": { "version": "5.9.3", diff --git a/package.json b/package.json index 53a60e9..bc841bf 100644 --- a/package.json +++ b/package.json @@ -1,27 +1,66 @@ { "name": "@gsc/web-kit", - "version": "0.2.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.", + "version": "0.3.0", + "description": "GSC web app skeleton \u2014 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" } + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./css": "./dist/styles/index.css", + "./layout": { + "types": "./dist/layout/index.d.ts", + "import": "./dist/layout/index.js" + }, + "./auth": { + "types": "./dist/auth/index.d.ts", + "import": "./dist/auth/index.js" + }, + "./auth/server": { + "types": "./dist/auth/server.d.ts", + "import": "./dist/auth/server.js" + }, + "./auth/middleware": { + "types": "./dist/auth/middleware.d.ts", + "import": "./dist/auth/middleware.js" + }, + "./shell": { + "types": "./dist/shell/index.d.ts", + "import": "./dist/shell/index.js" + }, + "./shell/server": { + "types": "./dist/shell/server.d.ts", + "import": "./dist/shell/server.js" + }, + "./data": { + "types": "./dist/data/index.d.ts", + "import": "./dist/data/index.js" + }, + "./forms": { + "types": "./dist/forms/index.d.ts", + "import": "./dist/forms/index.js" + }, + "./feedback": { + "types": "./dist/feedback/index.d.ts", + "import": "./dist/feedback/index.js" + }, + "./navigation": { + "types": "./dist/navigation/index.d.ts", + "import": "./dist/navigation/index.js" + }, + "./api": { + "types": "./dist/api/index.d.ts", + "import": "./dist/api/index.js" + }, + "./utils": { + "types": "./dist/utils/index.d.ts", + "import": "./dist/utils/index.js" + } }, "sideEffects": [ "**/*.css" @@ -43,10 +82,14 @@ "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0" }, + "peerDependenciesMeta": { + "bootstrap": { "optional": true } + }, "devDependencies": { "@types/node": "^20.11.0", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", + "next": "16.1.1", "typescript": "^5.4.0" } -} +} \ No newline at end of file diff --git a/scripts/postbuild.cjs b/scripts/postbuild.cjs index 464875e..5e05bcc 100644 --- a/scripts/postbuild.cjs +++ b/scripts/postbuild.cjs @@ -25,6 +25,18 @@ function copyDir(src, dst) { } if (fs.existsSync(srcStyles)) { copyDir(srcStyles, distStyles); + + // Rewrite ../images/... references in any copied CSS to ./images/... + // The source CSS assumes a sibling src/images/ dir; in dist/ the images + // live at dist/styles/images/ alongside the CSS, so the relative path + // is one level shorter. + for (const f of fs.readdirSync(distStyles)) { + if (!f.endsWith(".css")) continue; + const p = path.join(distStyles, f); + const c = fs.readFileSync(p, "utf8"); + const fixed = c.replace(/\.\.\/images\//g, "./images/"); + if (fixed !== c) fs.writeFileSync(p, fixed); + } } // 2. Fix ESM relative imports: append `.js` to bare specifiers like diff --git a/src/auth/middleware.ts b/src/auth/middleware.ts index a69a540..9f5e888 100644 --- a/src/auth/middleware.ts +++ b/src/auth/middleware.ts @@ -1,4 +1,16 @@ -import { NextResponse, type NextRequest } from "next/server"; +import { NextResponse } from "next/server"; + +// Loose local typing for NextRequest. Importing next's official type +// here pulls in `next/server` from THIS package's node_modules, which +// causes a "two copies of next" type conflict with the consumer's +// next. The shape below is the surface area the middleware actually +// uses; structural typing is enough at the call site. +interface NextRequestLike { + nextUrl: URL; + cookies: { + get(name: string): { value: string } | undefined; + }; +} export interface AuthMiddlewareOptions { /** @@ -38,7 +50,7 @@ export function createAuthMiddleware(opts: AuthMiddlewareOptions = {}) { const publicRoutes = opts.publicRoutes ?? []; const signInPath = opts.signInPath ?? "/api/auth/signin/keycloak"; - return function middleware(req: NextRequest) { + return function middleware(req: NextRequestLike) { const { pathname } = req.nextUrl; if (isAlwaysAllowed(pathname)) { diff --git a/src/data/index.ts b/src/data/index.ts index 4e4024d..61bf8bc 100644 --- a/src/data/index.ts +++ b/src/data/index.ts @@ -1,2 +1,37 @@ -// @gsc/web-kit/data — Phase 1 stub. Real surface lands in later phases. -export {}; +/** + * @gsc/web-kit/data — data display primitives. + * + * Curated re-export from @limitless/ui. Use these for entity lists, + * detail panels, and timeline/calendar views. + * + * The Widget family is intentionally narrow: only names that exist in + * both Widget.d.ts and Widget/index.d.ts are re-exported, because the + * installed limitless dist has a duplicate-file collision there. If + * apps need IconWidget / UserWidget / etc., import from @limitless/ui + * directly until upstream is fixed. + */ + +export { + Table, + DataTable, + Pagination, + TreeView, + Timeline, + Calendar, + Gallery, + Sortable, + ListGroup, + // Widget family (intersection of the two Widget files in dist) + StatWidget, + ProgressWidget, + ChartWidget, + ContentWidget, +} from "@limitless/ui"; + +export type { + DataTableProps, + StatWidgetProps, + ProgressWidgetProps, + ChartWidgetProps, + ContentWidgetProps, +} from "@limitless/ui"; diff --git a/src/feedback/index.ts b/src/feedback/index.ts index f8cca73..cba0c48 100644 --- a/src/feedback/index.ts +++ b/src/feedback/index.ts @@ -1,2 +1,25 @@ -// @gsc/web-kit/feedback — Phase 1 stub. Real surface lands in later phases. -export {}; +/** + * @gsc/web-kit/feedback — overlays, toasts, status indicators. + * + * Curated re-export from @limitless/ui. + */ + +export { + Alert, + Toast, + Notification, + Modal, + Offcanvas, + Popover, + Tooltip, + SweetAlert, + Spinner, + Progress, + ProgressStacked, + IdleTimeout, + FAB, +} from "@limitless/ui"; + +export type { + ToastProps, +} from "@limitless/ui"; diff --git a/src/forms/index.ts b/src/forms/index.ts index 18c748d..a340fb3 100644 --- a/src/forms/index.ts +++ b/src/forms/index.ts @@ -1,2 +1,146 @@ -// @gsc/web-kit/forms — Phase 1 stub. Real surface lands in later phases. -export {}; +/** + * @gsc/web-kit/forms — form primitives + validation. + * + * Curated re-export from @limitless/ui. Apps import everything here + * instead of reaching into the lower layer: + * + * import { Form, FormGroup, FormControl, Select, SelectSingle, + * useValidation, required, email } from "@gsc/web-kit/forms"; + */ + +// Form primitives +export { + FormGroup, + FormControl, + FormCheck, + Select, + InputGroup, +} from "@limitless/ui"; +export type { + FormGroupProps, + FormControlProps, + FormCheckProps, + SelectOption, + SelectProps, + InputGroupProps, +} from "@limitless/ui"; + +// Rich selects (react-select family — AdvancedSelect module) +export { + SelectSingle, + MultiSelect, + TagsSelect, + AsyncSelect, +} from "@limitless/ui"; +export type { + SelectSingleProps, + SelectMultiProps, + CreatableSelectProps, + AsyncSelectProps, +} from "@limitless/ui"; + +// Rich inputs +export { + DatePicker, + ColorPicker, + TagInput, + FileUpload, + Slider, + Rating, + DualListBox, + ImageCropper, +} from "@limitless/ui"; + +// Multi-step +export { Wizard, Stepper } from "@limitless/ui"; + +// Validation — hooks (re-exported via @limitless/ui root: `export * from './validation'`) +export { + useValidation, + useFieldValidation, + useAddressAutocomplete, + loadGoogleMapsScript, +} from "@limitless/ui"; + +// Validation — format validators +export { + required, + minLength, + maxLength, + pattern, + matches, + email, + url, + tel, + number, + range, + date, + time, + datetimeLocal, + month, + week, + color, + search, + password, + getPasswordStrength, + file, + checkbox, + radio, +} from "@limitless/ui"; + +// Validation — security +export { + noInjection, + noSqlInjection, + noScriptInjection, + noHtmlInjection, + noPromptInjection, + detectInjectionType, + SQL_INJECTION_PATTERNS, + SCRIPT_INJECTION_PATTERNS, + HTML_INJECTION_PATTERNS, + PROMPT_INJECTION_PATTERNS, +} from "@limitless/ui"; + +// Validation — address +export { + postalCode, + europeanPostalCode, + city, + streetAddress, + houseNumber, + europeanCountry, + europeanAddress, + getCountryName, + getPostalCodeHint, + POSTAL_CODE_PATTERNS, + EU_COUNTRIES, + EEA_COUNTRIES, + EUROPEAN_COUNTRIES, +} from "@limitless/ui"; + +// Validation — types +export type { + ValidationStatus, + ValidationResult, + FieldState, + FormState, + ValidatorFn, + FieldSchema, + FormSchema, + EuropeanAddress, + AddressSuggestion, + SecurityValidationOptions, + PasswordStrength, + PasswordOptions, + PhoneOptions, + NumberOptions, + DateOptions, + TimeOptions, + FileOptions, + UseValidationOptions, + UseFieldValidationOptions, + UseAddressAutocompleteOptions, + ServerValidationSchema, + ServerValidationResult, +} from "@limitless/ui"; diff --git a/src/layout/AppLayout.tsx b/src/layout/AppLayout.tsx index 62df102..1a942f9 100644 --- a/src/layout/AppLayout.tsx +++ b/src/layout/AppLayout.tsx @@ -1,14 +1,9 @@ "use client"; -import React, { useEffect, useState } from "react"; +import React from "react"; +import { AppShell } from "@limitless/ui"; -import { ShellProvider } from "../shell/index"; -import type { - ShellConfig, - ShellMenuItem, -} from "../shell/types"; - -const SIDEBAR_COLLAPSED_KEY = "gsc-web-kit-sidebar-collapsed"; +import type { ShellConfig } from "../shell/types"; export interface AppLayoutProps { /** Pre-resolved chrome config. Fetch with `fetchShellConfig()` server-side. */ @@ -26,378 +21,11 @@ export interface AppLayoutProps { } /** - * The chrome. Renders a Bootstrap-Layout-3 navbar / sidebar / footer - * shell from a `ShellConfig`. Apps wrap their `[locale]/layout.tsx` - * children in this and get a consistent app look for free. + * GSC app chrome. Thin wrapper around `` from `@limitless/ui` — + * the two share the same `ShellConfig` DTO. Kept as a separate export so + * the kit owns the consumer-facing surface and can add GSC-only props + * (telemetry, feature flags, etc.) without forking limitless. */ -export function AppLayout({ - config, - currentPath, - translate, - onSignOut, - navbarExtras, - children, -}: AppLayoutProps) { - const t = translate ?? ((k: string) => k); - const path = - currentPath ?? - (typeof window !== "undefined" ? window.location.pathname : "/"); - - return ( - - -
- -
- {children} -
-
- -
- ); -} - -// ─── Navbar ──────────────────────────────────────────────────────────────────── - -function AdminNavbar({ - config, - t, - onSignOut, - navbarExtras, -}: { - config: ShellConfig; - t: (k: string) => string; - onSignOut?: () => void; - navbarExtras?: React.ReactNode; -}) { - const [showUserMenu, setShowUserMenu] = useState(false); - - const initials = - config.user.displayName - ?.split(" ") - .map((n) => n[0]) - .filter(Boolean) - .join("") - .slice(0, 2) - .toUpperCase() || "?"; - - const userMenuItems = config.menus["user-menu"] ?? []; - - return ( - - ); -} - -// ─── Sidebar ────────────────────────────────────────────────────────────────── - -function AdminSidebar({ - config, - t, - pathname, -}: { - config: ShellConfig; - t: (k: string) => string; - pathname: string; -}) { - const [collapsed, setCollapsed] = useState(false); - - useEffect(() => { - if (typeof window === "undefined") return; - const saved = window.localStorage.getItem(SIDEBAR_COLLAPSED_KEY); - if (saved === "true") setCollapsed(true); - }, []); - useEffect(() => { - if (typeof window === "undefined") return; - window.localStorage.setItem(SIDEBAR_COLLAPSED_KEY, String(collapsed)); - }, [collapsed]); - - const sidebarClasses = [ - "sidebar", - "sidebar-light", - "sidebar-main", - "sidebar-expand-lg", - "align-self-start", - collapsed ? "sidebar-main-resized" : "", - ] - .filter(Boolean) - .join(" "); - - const items = config.menus.sidebar ?? []; - const strippedPath = stripLocale(pathname); - - return ( -
-
-
-
- {!collapsed && ( -
- {config.branding.productName} -
- )} - -
-
- -
-
    - {items.map((item) => ( - - ))} -
-
-
-
- ); -} - -function SidebarNavItem({ - item, - pathname, - collapsed, - t, -}: { - item: ShellMenuItem; - pathname: string; - collapsed: boolean; - t: (k: string) => string; -}) { - const hasChildren = !!item.children?.length; - const active = isActiveHref(item.href, pathname); - const childActive = !!item.children?.some((c) => - isActiveHref(c.href, pathname), - ); - const [open, setOpen] = useState(childActive); - const [hovered, setHovered] = useState(false); - - const icon = item.icon ? : null; - - if (hasChildren) { - const showFlyout = collapsed && hovered; - const showInline = !collapsed && open; - const navItemClasses = [ - "nav-item", - "nav-item-submenu", - showInline ? "nav-item-open" : "", - showFlyout ? "nav-group-sub-visible" : "", - ] - .filter(Boolean) - .join(" "); - - return ( -
  • setHovered(true) : undefined} - onMouseLeave={collapsed ? () => setHovered(false) : undefined} - > - { - e.preventDefault(); - setOpen((o) => !o); - }} - > - {icon} - {!collapsed && {t(item.translationKey)}} - - -
  • - ); - } - - return ( -
  • - - {icon} - {!collapsed && {t(item.translationKey)}} - -
  • - ); -} - -// ─── Footer ─────────────────────────────────────────────────────────────────── - -function AdminFooter({ - config, - t, -}: { - config: ShellConfig; - t: (k: string) => string; -}) { - const items = config.menus.footer ?? []; - return ( -
    -
    - - {config.branding.footerHtml ? ( - - ) : ( - <>© {new Date().getFullYear()} GoSec Cloud - )} - - {items.length > 0 && ( - - )} -
    -
    - ); -} - -// ─── helpers ────────────────────────────────────────────────────────────────── - -function isActiveHref(href: string, currentPath: string): boolean { - if (!href || href === "#") return false; - if (href === "/") return currentPath === "/"; - return currentPath === href || currentPath.startsWith(`${href}/`); -} - -function stripLocale(p: string): string { - return p.replace(/^\/[a-z]{2}(?=\/|$)/, "") || "/"; +export function AppLayout(props: AppLayoutProps) { + return ; } diff --git a/src/navigation/index.ts b/src/navigation/index.ts index f885adf..ea928ae 100644 --- a/src/navigation/index.ts +++ b/src/navigation/index.ts @@ -1,2 +1,26 @@ -// @gsc/web-kit/navigation — Phase 1 stub. Real surface lands in later phases. -export {}; +/** + * @gsc/web-kit/navigation — nav, breadcrumbs, page chrome, content + * organisers. + * + * Curated re-export from @limitless/ui. + */ + +export { + Breadcrumbs, + Nav, + Tabs, + Pills, + Dropdown, + ContextMenu, + Scrollspy, + PageHeader, + Accordion, + Collapse, + Carousel, + Embed, + SyntaxHighlighter, + Card, + Badge, + Button, + Media, +} from "@limitless/ui"; diff --git a/src/shell/index.ts b/src/shell/index.ts index dae7c3f..0fb4e18 100644 --- a/src/shell/index.ts +++ b/src/shell/index.ts @@ -1,8 +1,12 @@ "use client"; -import { createContext, useContext } from "react"; +// Client surface for @gsc/web-kit/shell. +// +// The context provider lives inside @limitless/ui's ; we just +// re-export the `useShell()` hook so pages rendered under +// (which delegates to AppShell) can read the config. -import type { ShellConfig } from "./types"; +export { useShell } from "@limitless/ui"; export type { ShellApp, @@ -12,17 +16,3 @@ export type { ShellMenuZone, ShellUser, } from "./types"; - -const ShellContext = createContext(null); - -/** Provider used by ``; rarely needed directly. */ -export const ShellProvider = ShellContext.Provider; - -/** Read the current ShellConfig anywhere inside ``. */ -export function useShell(): ShellConfig { - const cfg = useContext(ShellContext); - if (!cfg) { - throw new Error("useShell must be used inside "); - } - return cfg; -} diff --git a/src/shell/types.ts b/src/shell/types.ts index 6efb0e7..079610c 100644 --- a/src/shell/types.ts +++ b/src/shell/types.ts @@ -1,48 +1,11 @@ -/** - * Shape of what gsc-shell-api returns. Mirror of the Go service's DTO. - * If you change one, change both — there's a runtime contract in - * between, not a code-generator. - */ - -export type ShellMenuZone = "topbar" | "sidebar" | "footer" | "user-menu"; - -export interface ShellMenuItem { - id: string; - key: string; - translationKey: string; - href: string; - icon?: string; - isExternal?: boolean; - children?: ShellMenuItem[]; -} - -export interface ShellApp { - key: string; - displayName: string; - baseUrl: string; -} - -export interface ShellBranding { - logoUrl: string; - productName: string; - footerHtml?: string; - brandColor?: string; -} - -export interface ShellUser { - id: string; - email?: string; - displayName: string; - givenName?: string; - familyName?: string; - tenantId?: string; - roles: string[]; -} - -export interface ShellConfig { - version: number; - app: ShellApp; - branding: ShellBranding; - user: ShellUser; - menus: Partial>; -} +// Re-export from @limitless/ui so the kit and the underlying AppShell +// share one canonical type for ShellConfig. Diverging here would mean +// the kit's can't be fed by limitless and vice versa. +export type { + ShellApp, + ShellBranding, + ShellConfig, + ShellMenuItem, + ShellMenuZone, + ShellUser, +} from "@limitless/ui"; diff --git a/src/utils/index.ts b/src/utils/index.ts index 7fec649..a86ced7 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,2 +1,7 @@ -// @gsc/web-kit/utils — Phase 1 stub. Real surface lands in later phases. -export {}; +/** + * @gsc/web-kit/utils — shared hooks and helpers. + * + * Curated re-export from @limitless/ui. + */ + +export { useDisclosure } from "@limitless/ui";