New `./chrome` entrypoint exporting `<AdminShell>` and the header
components (Search, SearchHistory, SearchOptions, Messages, BrowseApps,
HeaderCustomers, HeaderContacts, LogoutButton). Refactored from the
Chronos-style AdminShell that gscCRM was vendoring byte-for-byte —
header/footer/sidebar are now a single shared surface across apps.
Explicit props contract (no site-informations.json, no internal data
sources): `menus`, `apps`, `user`, `brand` are required; `features.*`
flags gate every section (search/browseApps/messages/notifications/
subbar*/pageHeader*/activityPanel/chat/footer); `slots.*` lets apps
inject content; `labels` overrides the next-intl "chrome" namespace.
Locale-aware navigation: chrome calls useLocale() and prepends
/{locale} to internal menu URLs, leaving externals (http(s)://…) and
the "#" sentinel alone. Breadcrumbs and the path-derived page title
strip the leading locale segment so they read "Contacts" not
"En › Contacts". Necessary for `localePrefix: 'always'` consumers like
gscCRM.
Phosphor 2.x icons: `normalizeIconClass` prepends the base `ph` class
(compound selectors `.ph.ph-house:before` require both). All hardcoded
`<i className="ph-…">` sites switched to `ph ph-…`.
`next-intl` and `next-auth` moved to peerDependencies (with devDep
copies for the kit's own typecheck/build). Consumers must symlink their
installed copies into the kit's node_modules at build time — otherwise
useTranslations()/useSession() bind to a separate React context and
next-intl throws Error(void 0) on render.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
236 lines
8.3 KiB
Markdown
236 lines
8.3 KiB
Markdown
# `@gsc/web-kit`
|
|
|
|
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.
|
|
|
|
## 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)
|
|
|
|
// Chrome — unified header/footer/sidebar shell
|
|
import { AdminShell } from "@gsc/web-kit/chrome";
|
|
|
|
// Lower-level layout primitives
|
|
import { AppLayout } from "@gsc/web-kit/layout";
|
|
import { useShell } from "@gsc/web-kit/shell";
|
|
import { fetchShellConfig } from "@gsc/web-kit/shell/server";
|
|
|
|
// 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 | **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)** |
|
|
| 5 | chrome · AdminShell + headers + LogoutButton + nav migrations | **done (v0.4.0)** |
|
|
| 5a | Roll out chrome to gscCRM / gscChronos / gscAdmin / gscPortal | in progress |
|
|
| 6 | api · HTTP client helper (Bearer injection, 401 → signInRedirect) | planned |
|
|
|
|
---
|
|
|
|
## Chrome (`/chrome`) — v0.4.0
|
|
|
|
Unified `AdminShell` providing navbar, subbar, page-header, sidebar, footer,
|
|
optional chat overlay and activity panel. Every app receives the same UI
|
|
chrome and toggles features it doesn't use via `features` props.
|
|
|
|
### Adopting chrome in a new app
|
|
|
|
1. **Add the dep** (already a `file:` resolve to this kit) and import:
|
|
|
|
```tsx
|
|
// app/[locale]/layout.tsx
|
|
import { AdminShell } from "@gsc/web-kit/chrome";
|
|
import "@gsc/web-kit/css";
|
|
```
|
|
|
|
2. **Apply nav migrations** to your app's database:
|
|
|
|
```bash
|
|
# In your app's migrations directory, copy the two kit-canonical files verbatim
|
|
cp node_modules/@gsc/web-kit/migrations/nav-schema.up.sql apps/<your-app>/migrations/00X_nav_schema.up.sql
|
|
cp node_modules/@gsc/web-kit/migrations/nav-apps-seed.up.sql apps/<your-app>/migrations/00Y_nav_apps_seed.up.sql
|
|
# Then copy the menu-items template once and adapt to your app's menu
|
|
cp node_modules/@gsc/web-kit/migrations/nav-menu-items-template.sql apps/<your-app>/migrations/00Z_nav_menu_items.up.sql
|
|
# edit 00Z_… to replace example rows with your app's sidebar/topbar entries
|
|
psql "$DATABASE_URL" -f apps/<your-app>/migrations/00X_nav_schema.up.sql
|
|
psql "$DATABASE_URL" -f apps/<your-app>/migrations/00Y_nav_apps_seed.up.sql
|
|
psql "$DATABASE_URL" -f apps/<your-app>/migrations/00Z_nav_menu_items.up.sql
|
|
```
|
|
|
|
Re-copy `nav-schema` + `nav-apps-seed` on every kit upgrade. The
|
|
menu-items file is yours after the first copy — the kit never touches it
|
|
again.
|
|
|
|
3. **Add Prisma** to read the data:
|
|
|
|
```prisma
|
|
// prisma/schema.prisma
|
|
datasource db {
|
|
provider = "postgresql"
|
|
url = env("DATABASE_URL")
|
|
schemas = ["nav"]
|
|
}
|
|
generator client {
|
|
provider = "prisma-client-js"
|
|
previewFeatures = ["multiSchema"]
|
|
}
|
|
```
|
|
|
|
Add `"postinstall": "prisma generate"` to package.json. Copy `prisma/`
|
|
into the Docker image *before* `npm install` so the generate step sees it.
|
|
|
|
4. **Wire the layout server component**:
|
|
|
|
```tsx
|
|
const [sidebar, topbar, subbar, apps] = await Promise.all([
|
|
getMenuItemsByType("sidebar"),
|
|
getMenuItemsByType("topbar"),
|
|
getMenuItemsByType("subbar"),
|
|
getApps(),
|
|
]);
|
|
|
|
return (
|
|
<AdminShell
|
|
menus={{ sidebar, topbar, subbar }}
|
|
apps={apps}
|
|
user={{ displayName, email, avatarUrl }}
|
|
brand={{
|
|
name: "MyApp",
|
|
product: "GoSec MyApp",
|
|
logoUrl: "https://assets.gosec.cloud/logos/logo.svg",
|
|
websiteUrl: "https://gosec.cloud",
|
|
supportUrl: "https://support.gosec.cloud/",
|
|
docsUrl: "https://support.gosec.cloud/docs",
|
|
copyrightStartYear: 2024,
|
|
}}
|
|
features={{ chat: false, activityPanel: false }}
|
|
>
|
|
{children}
|
|
</AdminShell>
|
|
);
|
|
```
|
|
|
|
### Props reference
|
|
|
|
`<AdminShell>` is the kit's contract — see `src/chrome/types.ts` for the
|
|
authoritative TypeScript definition. Summary:
|
|
|
|
- **Data**: `menus`, `apps`, `user`, `notificationCount?`, `activity?`
|
|
- **Brand** (required): `name`, `product`, `logoUrl`, `websiteUrl`,
|
|
`supportUrl`, `docsUrl`, `copyrightStartYear` (+ optional `logoSmallUrl`)
|
|
- **`features`** (all optional booleans, sensible defaults):
|
|
`search`, `searchHistory`, `searchOptions`, `browseApps`, `messages`,
|
|
`notifications`, `subbar`, `subbarSupport`, `subbarSettings`,
|
|
`pageHeader`, `pageHeaderCustomers`, `pageHeaderContacts`,
|
|
`activityPanel`, `chat`, `footer`
|
|
- **`slots`** (ReactNode overrides):
|
|
`pageTitle`, `pageHeaderExtras`, `subbarExtras`, `activityPanel`,
|
|
`navbarExtras`, `footerExtras`
|
|
- **Behavior**: `onSignOut?` (default `next-auth signOut`),
|
|
`labels?: Partial<ChromeLabels>` (override individual chrome strings)
|
|
|
|
### i18n
|
|
|
|
Chrome's own strings come from next-intl namespace `chrome` with English
|
|
fallbacks. Add to your app's `common.json`:
|
|
|
|
```json
|
|
{
|
|
"chrome": {
|
|
"navigation": "Navigation",
|
|
"logout": "Logout",
|
|
"support": "Support",
|
|
...
|
|
}
|
|
}
|
|
```
|
|
|
|
Menu item labels (`menu.dashboard`, `menu.accounts`, …) live in your app's
|
|
existing translation namespace.
|
|
|
|
### Semver
|
|
|
|
Public surface = `<AdminShell>` props + exported types + migration files +
|
|
chrome CSS class names.
|
|
|
|
- **Major** — removed/renamed prop, changed prop shape, removed feature
|
|
flag, renamed CSS class, changed migration DDL.
|
|
- **Minor** — new optional prop, new feature flag (default off), new
|
|
slot, new export, new migration file.
|
|
- **Patch** — bugfix, internal refactor.
|
|
|
|
Deprecations land for one minor release with `console.warn` and a
|
|
`CHANGELOG.md` note before removal in the next major. Pin
|
|
`"@gsc/web-kit": "^X.Y.Z"` to receive minors automatically.
|
|
|
|
---
|
|
|
|
## Notes
|
|
|
|
- `AppLayout` is a thin wrapper around `<AppShell>` 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.
|