Instruction file imported from mGhassen/Forja (
.cursor/rules/forja-design-system.mdc). Copyright stays with the author.
Forja design system (RFC-025 · RFC-095)
Source of truth: apps/forja/lib/shared/foundation/primitives/ — import via package:forja/shared/foundation/primitives/primitives.dart.
Design alone / data alone (RFC-095)
Foundation is a shadcn-style design system. Composers under foundation/components/ take props + callbacks only. Features own controllers/stores and map domain data into those props.
| Layer | Owns | Forbidden |
|---|---|---|
primitives/ · components/ |
Visual chrome, layout, tokens | features/** imports, god controllers, ref.watch for product state |
features/** + boot hooks |
Controllers, match, config; fill chip/panel props | Re-drawing shared chrome that belongs in foundation |
Example: KitPortalsChip / KitSidePanelOverlay / KitPortalListPanel ← IPTV (IptvPortalsTopBarButton, IptvPortalsChromeHooks) wires portal label, health, seats, list.
Golden reference: KitSourcesPanel — features wire load/play via callbacks.
Visual language
- Flat cinematic shell — no
BackdropFilter, frosted glass, or ambient glows on shell nav, home chrome, or player control bars. - Exception — sliding side panels: media-details Sources uses
ForjaFrostedPanelwithBackdropFilter. Player overlays (Episodes, torrent files) pass a video screenshot asfrozenFrameand blur that withImageFiltered— do not putBackdropFilterover live video (freezes macOS). Do not scatter ad-hoc blur elsewhere. - Hero overlays use
ForjaShellColors.cinematic(fixed light-on-imagery), notAppTheme.primaryColor. - Spacing and breakpoints come from
ShellTokens/DetailsTokens— do not hardcode magic numbers that already exist there.
Tokens
ShellTokens — shell chrome, Home, catalog
| Area | Use |
|---|---|
| Nav / top bar | navRailWidth, homeTopBarHeight, shellNavCompactMaxWidth |
| Home hero | heroDesktopMinBodyWidth, heroTextColumnWidthDesktop, heroOverviewSlotHeightDesktop, homeSectionHorizontalPadding |
| Catalog rows | homeSectionTitleTop, bodyMaxWidthDesktop |
DetailsTokens — media details
| Area | Use |
|---|---|
| Hero / body | heroHeight, sectionSpacing, heroBodyOverlap, contentHorizontalPadding |
| Sources panel | sourcesPanelPadding (player overlays keep ShellTokens.playerSidePanelPadding) |
Details gutters deliberately alias ShellTokens.homeSectionHorizontalPadding / bodyMaxWidthDesktop so details stay aligned with catalog rows.
When adding a reused constant: shell/catalog → ShellTokens, details surfaces → DetailsTokens. One-off padding inside a single widget is fine.
Colors (ForjaShellColors)
- Shell chrome:
textPrimary,textSecondary,iconMuted,borderSubtle,surfaceElevated - Selection / resume:
chipSelectedBg,chipSelectedBorder,progressFill - Menus / dropdowns:
ForjaShellColors.cinematic.menuSurface - Floating player menus:
PlayerPopupTokensinshared/player/controls/menus/player_popup_panel.dart(translucent dark shell ~α0.82 like player side panels; select cards use idlecardBg, selected/hover/focusaccentFill+ green border — never solidaccentfill) - Do not use raw
Colors.white54for new shell UI when a semantic token exists.
Buttons
| Widget | When |
|---|---|
ForjaGhostButton |
Primary CTA on hero / details (Play, Watch Now) — text + optional icon, no fill |
ForjaPlainIcon |
Secondary icon actions (+, info, overflow) — no border box |
ForjaIconButton |
Sparingly — bordered square; prefer ForjaPlainIcon in hero chrome |
ForjaInteractive |
Custom hit targets that need hover/press scale |
Toggles
| Widget | When |
|---|---|
ForjaSwitch |
All on/off toggles (settings rows, player chrome, etc.) |
forjaSwitchThemeData |
App ThemeData.switchTheme — also styles SwitchListTile that do not override colors |
Prefer ForjaSwitch(scale: ForjaSwitch.settingsScale) in dense settings lists. Do not style raw Switch( with ad-hoc activeTrackColor / thumbColor.
Section titles
- Horizontal catalog rows:
ShellSectionTitleorShellSectionTitle.titleStyle - Media details sections (Cast, Trailers): same
titleStyle— plain text, no underline, no icon prefix
Focus / TV
- Interactive list tiles and cards: wrap with
FocusableControlfromapp_theme.dart(keyboard + focus ring). - Platform layout and input: read
ShellScope.metrics/ShellScope.inputPolicy— notShellTokens.isTvLayoutin widgets. - Platform shell switches live only in
shell/adapters/(ShellHost,DesktopShell,TvShell,MobileShell).
Text inputs (TV) — focus only until OK
D-pad land = focus/highlight only. Never activate. Opening the keyboard, edit mode, or search chrome requires explicit OK / Select / Enter / tap — never onFocusChange(true) or autofocus-into-edit.
| Widget | Role |
|---|---|
TvBrowseTextField |
TV search/query fields — browse until activate, then edit + IME |
SettingsTextField |
Settings rows — same browse/edit split |
Forbidden on TV: raw TextField that becomes editable / opens IME when focused; wiring focus listeners to openSearch / beginEditing. Full rule: forja-tv-scope.mdc → TV text inputs.
Forbidden without explicit approval
- Ad-hoc
BackdropFilter/ImageFilter.bluroutsideForjaFrostedPanel(and existing IPTV frosted panels) - Ad-hoc pill/chip styles that duplicate
TorrentSourceToggle/ chip selected tokens - Ad-hoc
Switch(/SwitchListTilecolor overrides — useForjaSwitch/ theme tokens - Duplicating hero title or meta rendering — use
shared/foundation/components/hero/(seeforja-shared-ui.mdc)