Web UI Foundation
Developer reference for GlycemicGPT web themes, semantic tokens, typography, icons, and UI primitives.
Web UI Foundation
This page documents the shared frontend foundation for apps/web. It is meant for contributors and maintainers who need to build UI that survives theme changes without rewriting component internals.
This is not a full product design system. It is the current foundation: semantic colors, class driven themes, font roles, sprite icons, base primitives, product components, and the checks that keep those pieces reliable.
Ownership Map
| Area | Canonical file |
|---|---|
| Raw shared color values | apps/web/src/styles/config/colors.css |
| Semantic theme variables and mode mappings | apps/web/src/styles/config/theme.css |
| Legacy compatibility variables | apps/web/src/styles/config/legacy-theme.css |
| Class driven variants | apps/web/src/styles/config/custom-variants.css |
| Font families and role utilities | apps/web/src/styles/config/fonts.css |
| Shared responsive spacing tokens | apps/web/src/styles/config/spacing.css |
| Shared radius tokens | apps/web/src/styles/config/radius.css |
| Shared animations | apps/web/src/styles/config/animations.css |
| Global element defaults | apps/web/src/styles/base.css |
| Base primitives | apps/web/src/base |
| Product components | apps/web/src/components |
| Product compositions | apps/web/src/compositions |
| Icon sprite | apps/web/public/static_assets/iconSprite.svg |
| Icon names, titles, and sizes | apps/web/src/base/Icon/iconConfig.ts |
| Class composition | apps/web/src/lib/ui/twMerge.ts |
| Runtime theme choices | apps/web/src/providers/theme-config.ts |
Global styling enters through apps/web/src/app/globals.css. Keep the import order stable:
- Tailwind CSS theme, preflight, and utilities.
- Raw color tokens from
colors.css. - Class driven variants from
custom-variants.css. - Font utilities from
fonts.css. - Shared responsive spacing tokens from
spacing.css. - Shared radius tokens from
radius.css. - Shared animations from
animations.css. - Legacy compatibility variables from
legacy-theme.css. - Semantic theme variables from
theme.css. - Global base rules from
base.css.
UI Version Boundary
The redesigned application is implemented under apps/web/src/app/v2, but /v2 is an internal routing detail. Middleware rewrites canonical public URLs such as /dashboard, /login, and /settings/account to the redesigned route tree.
The exact request header x-glycemicgpt-ui-version: legacy selects the legacy implementation. Legacy pages and legacy owned components remain unchanged from origin/develop.
Component ownership is not versioned. Durable redesigned product components live directly in apps/web/src/components, application level assemblies live in apps/web/src/compositions, and settings components live in apps/web/src/components/settings. Do not create dashboard-new-design, settings-new, or generic v2 component namespaces.
Legacy UI must not import redesigned product components. Shared API clients, hooks, types, providers, neutral base primitives, semantic tokens, and class composition utilities may serve both implementations.
Theme Model
Raw colors and semantic theme variables are separate on purpose.
A semantic token is a named design role. It says what a color is for, not what color it is. For example, --color-accent means "the current action color". It does not mean blue, yellow, green, or any other fixed hue.
That indirection lets components stay stable while themes change underneath them. A button can keep using bg-accent text-accent-foreground, and each theme decides which raw colors those roles map to.
Example flow:
/* colors.css */
:root {
--color-base-accent-blue: #5eb1ff;
--color-base-ink: #191919;
--color-base-cool-silver: #f7f9fb;
}
/* theme.css */
:root {
--color-accent: var(--color-base-accent-blue);
--color-accent-foreground: var(--color-base-ink);
}
.theme-dark {
--color-accent: var(--color-base-accent-blue);
--color-accent-foreground: var(--color-base-ink);
}
.theme-seasonal {
--color-accent: var(--color-base-cool-silver);
--color-accent-foreground: var(--color-base-ink);
}<button className="bg-accent text-accent-foreground">Save changes</button>The component only knows that it needs the accent background and the approved foreground for text placed on that background. It does not know or care which palette value the current theme uses.
- Add raw palette values as
--color-base-*tokens incolors.css. - Map raw tokens to semantic variables in
theme.css. - Reassign the same semantic variables inside theme classes such as
.theme-dark. - Use Tailwind utilities that reference semantic variables inside components.
Components should describe intent, not raw color. Use classes such as bg-surface-page, bg-surface-primary, text-foreground-primary, text-foreground-secondary, bg-accent, text-accent-foreground, border-border-default, text-signal-error-text, and bg-signal-check-fill.
Do not use raw palette classes such as text-slate-500, bg-blue-600, or hover:bg-red-700 in new shared UI.
Light and dark mode are class driven. New semantic theme mode is enabled by adding a theme-* class to a wrapper. During the migration, the app root also keeps legacy light and dark classes so existing dark:* utilities continue to work. Do not use prefers-color-scheme in CSS for theme decisions.
Semantic Color Families
Semantic variables describe usage, not literal hue.
| Family | Purpose | Example utilities |
|---|---|---|
surface-* | Page backgrounds, panels, raised areas, inverse regions | bg-surface-page, bg-surface-primary, bg-surface-secondary, bg-surface-elevated |
foreground-* | Text and icons placed on approved surfaces | text-foreground-primary, text-foreground-secondary, text-foreground-muted |
accent-* | Primary action color and text placed on accent backgrounds | bg-accent, hover:bg-accent-hover, text-accent-foreground |
brand-* | Theme adaptive color stops for branded visual assets | text-brand-gradient-middle |
border-* | Outlines, dividers, hover borders, focus borders | border-border-default, ring-border-active |
signal-*-fill | Status fills, indicators, charts, diagrams, and badges | bg-signal-check-fill, bg-signal-warning-fill |
signal-*-text | Signal colored text and icons on neutral surfaces | text-signal-error-text, text-signal-info-text |
overlay-* | Scrims above app surfaces | bg-overlay-primary |
Avoid vague names such as ui-primary, ui-secondary, and ui-background. If a new shared meaning is needed, add one semantic token and give it matching assignments for every supported theme.
Color Accessibility
Color pairings must follow the Color Accessibility Guide. The current guide defines approved combinations for light, dark, and seasonal theme modes.
Normal text needs AA contrast, non text UI needs enough contrast to identify controls and state, and color must not be the only way to communicate medical or product status.
Useful WCAG references:
- WCAG 2.2 Conformance
- WCAG 1.4.3 Contrast Minimum
- WCAG 1.4.11 Non Text Contrast
- WCAG 1.4.1 Use of Color
- WCAG 2.4.7 Focus Visible
- WCAG 2.4.13 Focus Appearance
Baseline rules:
- Normal text needs at least
4.5:1contrast. - Large text needs at least
3:1contrast. - Important icons, borders, selected states, focus indicators, and controls need at least
3:1contrast against adjacent colors. - High contrast mode is an enhancement. It is not permission to ship failures in the default theme.
- Do not rely on color alone for state, status, or medical meaning.
- If a pairing is not documented, calculate contrast before using it.
Accent rules:
- Use
bg-accent text-accent-foregroundfor selected tabs, filled primary controls, checked controls, and highlight surfaces with text inside. - Do not put white text on
bg-accent. - Use
text-foreground-secondaryfor secondary paragraphs and labels onbg-surface-pageorbg-surface-primary. - Use
text-accentonly for short emphasis on approved neutral surfaces.
Typography
Typography utilities live in apps/web/src/styles/config/fonts.css.
Use complete role utilities instead of repeated font family, size, weight, line height, and spacing stacks:
font_header_1font_header_2font_header_3font_header_4font_body_1font_body_2font_body_3font_body_4font_metric_labelfont_metric_captionfont_nav_link
Font families:
| Role | Family | Utility |
|---|---|---|
| Primary UI text | Poppins | font_poppins |
| Labels and compact metric text | JetBrains Mono | font_jetbrains_mono |
Font delivery rules:
- Register app fonts with
next/font/local, notnext/font/google. - Keep font files in
apps/web/src/app/fontsso builds do not depend on Google Fonts or CDN reachability. - Keep Inter as the app default through
--font-interand--font-sansunless the product intentionally changes the global app font. - Scope Poppins and JetBrains Mono through CSS variables and role utilities until the broader app is intentionally migrated.
- Register variable fonts with an explicit weight range so Next.js generates real variable font CSS instead of synthetic weights.
- Keep the matching SIL OFL 1.1 license files beside the redistributed font files.
Role utilities should define font family, size, weight, line height, and letter spacing. Do not usually combine role utilities with font_regular, font_bold, font_normal, or font_medium. If a repeated visual style needs that, add or adjust a role utility instead.
Font utilities should use rem units so text follows browser text size settings.
Useful typography accessibility references:
Radius
Shared radius tokens live in apps/web/src/styles/config/radius.css.
Use radius tokens as opt in utilities in components, such as rounded-button and rounded-panel. Do not apply radius tokens globally to raw elements. A token should make a radius reusable, not silently restyle every matching element in the app.
Spacing
Shared responsive spacing tokens live in apps/web/src/styles/config/spacing.css.
Use dashboard-panel-gap utilities for the space around and between dashboard panels. The underlying --dashboard-panel-gap custom property is 4px on mobile and 16px from the lg breakpoint. Tailwind utilities such as p-dashboard-panel-gap, gap-dashboard-panel-gap, and space-y-dashboard-panel-gap all resolve through that shared property.
Class Composition
Use twMerge from apps/web/src/lib/ui/twMerge.ts for dynamic class composition.
Do not call clsx, classnames, or tailwind-merge directly inside components. The local wrapper understands project font classes, so it can normalize conflicts such as font_header_1 followed by font_body_1.
When adding a local utility class group that can conflict with itself, update twMerge.ts and add focused tests.
Components
Base primitives live in apps/web/src/base. They provide accessible structure, stable typing, and default behavior. They may own minimal structural styling when it supports the primitive contract, such as layout reset classes, intrinsic sizing, hidden input mechanics, or sprite rendering defaults. They may also accept className props so composed components can apply semantic classes.
Base primitives should not own product appearance. Keep semantic colors, rich visual states, typography roles, branded treatments, and heavy layout styling in apps/web/src/components.
Current primitives:
Buttonowns the accessible button shell and defaulttype="button"behavior.Inputowns the shared input shell.Iconowns sprite based icon rendering.
Product components live in apps/web/src/components and compose base primitives with semantic classes. Current examples include PrimaryButton, SecondaryButton, HighlightButton, TextInput, and Checkbox.
Product compositions live in apps/web/src/compositions. A composition assembles multiple product components, providers, navigation regions, or page sections into an application level structure. Use this boundary for assemblies whose primary API is children, slots, or regions. Do not move a component merely because it is large or has several internal parts. A cohesive product widget remains a component.
Current compositions:
AppShellassembles the authenticated application gates, providers, banner, and dashboard layout.DashboardLayoutassembles persistent navigation and the main content region.
Authenticated V2 Scroll Ownership
AppShell is fixed to the viewport and clips outer overflow. DashboardLayout owns the only vertical page scroller through its main content region. Taking the shell out of document flow prevents route content from increasing the document height while keeping the banner, desktop sidebar, mobile navigation, overlays, and route content inside one viewport.
Authenticated V2 pages must not use h-screen or min-h-screen. Those utilities claim another viewport inside the shell and create empty scrollable space. Let normal page content determine its height. Fixed overlays may use viewport dimensions because they do not participate in page layout.
The persistent dashboard scroller resets when the route changes. Shared pagination also resets that scroller when the current page or total page count changes. New paginated V2 surfaces should reuse Pagination instead of implementing page navigation and scroll behavior locally.
Reusable component folders should follow the colocated pattern:
Component.tsxowns rendering.Component.types.tsorcomponent.types.tsowns props and exported types.Component.test.tsxorComponent.spec.tsxowns tests.index.tsowns public exports.- Component CSS stays beside the component only when the component owns local theme variables.
Keep base components thin and visually neutral. Minimal structural classes are acceptable in apps/web/src/base. Semantic color classes, product level visual states, branded styling, and product behavior belong in apps/web/src/components.
Component Accessibility
Components must support keyboard use, visible focus, correct names, roles, values, and labels.
Rules:
- Icon only controls need an accessible name through visible text,
aria-label, oraria-labelledby. - Interactive controls need visible focus through semantic border or ring tokens.
- Disabled states must remain identifiable without relying only on color.
- Form inputs need clear labels and useful error text.
- Status and medical meaning must use text, icon shape, or labels in addition to color.
Useful component accessibility references:
- WCAG 2.1.1 Keyboard
- WCAG 2.4.7 Focus Visible
- WCAG 4.1.2 Name, Role, Value
- WCAG 3.3.2 Labels or Instructions
Icons
Shared icons use the SVG sprite at apps/web/public/static_assets/iconSprite.svg.
Use Icon from apps/web/src/base/Icon when a symbol exists in the sprite. Register every symbol in apps/web/src/base/Icon/iconConfig.ts with a default title and size. IconName is derived from that config.
Icon rules:
- Add only icons required by current work.
- Keep sprite symbols minimal and reusable.
- Use
currentColorin sprite paths so icons follow text color utilities. - Let the configured
titleandsizebe the default for shared icons. - Use the
titleprop only when visible context needs a more specific accessible name. - Use
decorativewhen an icon is purely visual and should be hidden from assistive technology. - Do not duplicate inline SVG markup in components when a shared sprite symbol exists.
Current icon sources:
Useful icon accessibility references:
Adding A Theme
Follow this path for a new semantic theme:
- Add raw palette values to
colors.css. - Reassign existing semantic tokens in a new
.theme-*block intheme.css. - Add the selectable theme to
theme-config.ts. - Set
semanticClassto the new class without the dot. - Set
legacyClassto onlylightordark. - Update color accessibility documentation with approved pairings.
- Run
npm run validate:theme-colorsfromapps/web. - Run focused provider tests,
npm run typecheck, and the checks for the changed surface.
Do not create legacy classes for new semantic themes. Do not create per component color variable families such as --background-color-primary-button or --color-primary-button.
Tests
Every base component added to apps/web/src/base needs colocated unit tests.
Colocate unit tests with the source module they own, including components, hooks, providers, and focused library utilities. Keep page, routing, middleware, accessibility, smoke, API contract, and cross-feature integration tests in apps/web/__tests__.
Legacy component tests may remain centralized while the legacy UI is intentionally kept unchanged. Do not move a legacy test into a redesigned component folder with a similar name.
Test for:
- Meaningful rendering contracts.
- Accessible attributes and labels.
- State behavior.
- Emitted events.
- Class composition behavior when it matters.
Avoid low value snapshot tests. Prefer direct assertions for roles, attributes, callbacks, and sprite references.
Before considering UI foundation work done, run these commands from apps/web:
npm test
npm run typecheck
npm run build