Every app that imports the kit was shipping a /favicon.ico 404
because none of them wired up Next.js's metadata.icons. This adds
a tiny helper so an app only has to:
export const metadata: Metadata = {
title: brand.product,
icons: brandIcons(brand),
};
brandIcons() returns icon/shortcut/apple entries pointing at
brand.faviconUrl (new optional field, defaults to brand.logoUrl).
MIME type inferred from the URL extension (svg/png/ico).
Brand gains the optional faviconUrl field. Existing apps that just
pass logoUrl keep working — they'll now render the logo as the
favicon by default. Apps that want a separate icon set
faviconUrl explicitly.
First consumer: gscSounds layout — verified /favicon.ico now
serves the proper icon and /icon.svg works too.
@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)
// 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
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
-
Add the dep (already a
file:resolve to this kit) and import:// app/[locale]/layout.tsx import { AdminShell } from "@gsc/web-kit/chrome"; import "@gsc/web-kit/css"; -
Apply nav migrations to your app's database:
# 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.sqlRe-copy
nav-schema+nav-apps-seedon every kit upgrade. The menu-items file is yours after the first copy — the kit never touches it again. -
Add Prisma to read the data:
// 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. Copyprisma/into the Docker image beforenpm installso the generate step sees it. -
Wire the layout server component:
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(+ optionallogoSmallUrl) features(all optional booleans, sensible defaults):search,searchHistory,searchOptions,browseApps,messages,notifications,subbar,subbarSupport,subbarSettings,pageHeader,pageHeaderCustomers,pageHeaderContacts,activityPanel,chat,footerslots(ReactNode overrides):pageTitle,pageHeaderExtras,subbarExtras,activityPanel,navbarExtras,footerExtras- Behavior:
onSignOut?(defaultnext-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:
{
"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
AppLayoutis a thin wrapper around<AppShell>from@limitless/ui— they share theShellConfigDTO, 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/uidirectly. - The
Widgetfamily in/datais intentionally narrow (StatWidget,ProgressWidget,ChartWidget,ContentWidget) — the installed limitless dist has a duplicateWidgetfile/folder collision, so only names exported by both are passed through.