Status: actively used — consumed by 6 apps as a local path dependency; no version tags and no test target · Platform: SwiftPM package, iOS 26 / macOS 26
The shared design system for the Deets apps — DeetsTrips, DeetsMusic, DeetsBoard, DeetsJack, DeetsHealth, and DeetsTasks. 38 public SwiftUI components across 36 files, with no external dependencies. It is the single source of truth for the visual language — colors, typography, the glass look, screen scaffolding, and the reusable glass components — so every Deets app looks and feels identical without copy-pasting the theme layer.
Two orthogonal axes drive everything: a theme (8 built-in palettes plus a user-authored slot) sets color, and a skin (3 on iOS, 4 on macOS) sets material and structure. Both propagate through SwiftUI environment values, so adding a skin restyles every consuming app without touching app code. Generated palettes run through a WCAG 2.x contrast check before they're allowed through — the model picks the vibe, the package guarantees the contrast.
This package holds the design vocabulary, not product screens. Anything that encodes what a trip or a health metric is belongs in the app, not here. DeetsKit stays product-blind: if a type would mean nothing to a second, unrelated app, it does not go in DeetsKit.
Start here: new to DeetsKit?
docs/QUICKSTART.mdgets an app running in three steps and indexes every primitive the package offers.
DeetsKit is a plain SwiftPM package, but the Deets apps consume it as a path dependency:
each app's project.yml declares
packages:
DeetsKit:
path: ../DeetsKitso to build any of the apps (DeetsJack,
DeetsMusic), clone this repo as a sibling
directory named DeetsKit first:
git clone https://github.com/deets-137/DeetsKit.git
git clone https://github.com/deets-137/DeetsMusic.git # or DeetsJack, …
There is no version pin — edits to DeetsKit are picked up on the app's next build. For standalone use, add the package to any iOS 26+ / macOS 26+ project by path or by URL; it depends only on system frameworks.
A Swift package, iOS 26+ / macOS 26+, Swift 6 language mode. One concern per file:
| File | Provides |
|---|---|
Palette.swift | Palette (canvas/blob1/blob2/glassTint/ink/onGlassTint), the 8 AppThemes (AppTheme.all, AppTheme.with(id:)), the \.deetsPalette environment value, and .deetsSecondary (dimmed-ink ShapeStyle). |
Skin.swift | Skin (the second, color-independent look axis: surface/border/shadow/corner/canvas/hold-fill/reveal/typeface), the launch skins (Skin.all = glass/comic/retro/sunshine, Skin.with(id:), .resolvedCardCorner), and the \.deetsSkin environment value. |
Fonts.swift | DeetsFont semantic type scale (.screenTitle, .cardTitle, .body, …) and the .deetsFont(_:) modifier (Dynamic-Type + Bold-Text reactive; face follows the skin). |
GlassStyle.swift | .deetsGlass(in:) (the surface — frost or flat per skin); button styles DeetsProminentButtonStyle (full-width tinted CTA), DeetsGlassButtonStyle (sheet "Done" pill), DeetsActionPillButtonStyle (tap-only icon+label action pill). |
HoldActions.swift | .deetsHoldToFire(progress:duration:) (the shared press-and-hold gesture core, Reduce-Motion aware, scroll-friendly), DeetsHoldActionPill (hold-to-fill capsule with a legible fill; pass icon: "xmark" for a delete pill), and DeetsHoldActionCard (hold-to-fill glass card; title convenience or content slot). Both render via DeetsHoldFill, so they restyle per skin automatically. |
HoldFill.swift | DeetsHoldFill(progress:) — the skin-aware hold-to-fire fill (the skin's holdFill axis): glass floods left→right, sunshine sunbursts from the top-trailing corner, comic fizzles in as a halftone dot dissolve, retro glitches in as staggered scanline bands. Deterministic and purely progress-driven (Reduce-Motion safe); usable both as the ink overlay and as the .mask on an onGlassTint label re-render — the two stay pixel-identical. |
Layout.swift | DeetsLayout constants (content width, spacing, padding, corner radius, canvas animation durations), the DeetsScreen backdrop+column wrapper (scrollable: / backdrop: / maxWidth: options), and ScreenTitle. |
Components.swift | GlassCard, CircleGlassButton, WarningCard (the off-palette scarlet error/warning banner), GuidanceCard (its calmer mango sibling — dark espresso text, for notices/orientation, never blocking), and the .ifManageable(_:_:) helper. |
Stat.swift | The stat/data-display family: DeetsStat (value + label + caption), DeetsMeter (a 0…1 labelled track+fill, i.e. a probability bar), DeetsDistributionBar (DeetsDistributionSegments summing to ~1, a categorical spread with legend). Product-blind, palette-blind, self-voicing. |
CanvasBackground.swift | CanvasBackground — the backdrop (drifting blobs / dot-grid / scanlines / solid, per skin); init(extent:) renders it across a DeetsCanvas's full grid. BlobPlacement + \.deetsBlobPlacement opt the blobs into width-aware placement (they slide to re-balance on rotation / app request). |
Canvas.swift | DeetsCanvas(columns:rows:selection:cell:) — a pannable grid of viewport-sized cells (programmatic, selection-driven nav), and CanvasPosition (grid coordinate + 3×3 compass conveniences). |
MotionMode.swift | DeetsMotionMode (slide / fade / snap) + the \.deetsMotionMode environment value — the DeetsCanvas navigation animation, always gated by Reduce Motion. |
Split.swift | DeetsSplit — responsive two-panel layout: side-by-side (proportional ratio) on regular width, stacked on compact. |
NavCard.swift | NavCard(action:label:) — a frosted, skin-aware navigation card (hover / press affordances) for building destination grids. |
RailCard.swift | DeetsRailCard { content } rail: { … } — asymmetric "subject + action rail" card: a free-form content column beside a right-pinned vertical action rail that spans the column's full height. Content-driven height (no greedy stretch); children: .contain keeps both slots' controls individually focusable. Open-ended (now-playing, or any subject+actions surface). |
RailSigil.swift | DeetsRailSigil(systemName:accessibilityLabel:hint:enabled:action:) — the action tile for a DeetsRailCard rail: an SF Symbol in a skin-aware glass rounded-rect, flexing to an equal share of the rail height (≥44pt floor, 0.45 disabled dim, label required). DeetsHoldRailSigil is the hold-to-fire commit variant (skin hold-fill flood, legible-glyph recolor, explicit accessibility action). DeetsRailSigilLabel is the bare face for ShareLink-style wrappers and placeholders. |
ExpandMenu.swift | DeetsExpandMenu(alignment:accessibilityLabel:items:glyph:) — the corner-pinned "glass circle expands upward into pills" chrome menu. ViewBuilder glyph slot (or the glyphSystemName: convenience); DeetsExpandMenuItem tap-pills fire-and-collapse; a non-collapsing extra slot (.aboveItems/.belowItems) hosts hold-to-fire controls, its builder receiving a collapse closure. Reveal motion rides the skin's reveal axis (.menu context). |
Reveal.swift | The skin's reveal axis (Skin.reveal), as modifiers: .deetsReveal(edge:context:) on revealed content + .deetsRevealAnimation(value:context:) on its container — skin-voiced show/hide (glass soft spring, comic pop with overshoot + scale, retro near-instant blit) with the Reduce Motion collapse (opacity-only) built in. Two timing tokens per skin: .panel (inline dropdowns) and .menu (expand-menu pills). |
EditorFieldRow.swift | EditorTextRow / EditorComboRow<Picker> — labeled editor rows (uppercase label · field/picker · hairline rule). The shared horizontal field rhythm every Deets detail editor inside a GlassCard uses. |
PanelTabDropdown.swift | PanelTab protocol + PanelTabDropdownLabel + PanelTabDropdownPopover<Tab> + .panelTabDropdownOverlay(isPresented:content:) — tab dropdown for a panel header inside a GlassCard (popover renders at screen level so the glass reads opaque). |
StructuresList.swift | StructuresList<RowID: Hashable> — scrollable list of vocabulary sections with inline add and press-and-hold delete (`Delete |
VocabPicker.swift | VocabPicker<Item: Hashable> — combo-box picker over a vocabulary list with .popover search + filter. Generic over Hashable with a name: (Item) -> String extractor; pairs with EditorComboRow. |
ThemePicker.swift | ThemePickerGrid(selection:) — the 2×4 swatch grid for choosing a theme. |
SkinPicker.swift | SkinPickerGrid(selection:) — tiles previewing each skin in the current palette. |
AppearanceCard.swift | AppearanceCard(themeID:skinID:…) — the whole settings appearance section (Theme + Skin pickers, labeled, in one card). Drop-in; apps don't lay out the pickers. Optional custom-theme params add the 9th "Custom" tile + generator callbacks. |
Contrast.swift | DeetsContrast (WCAG checks + "Fix Up"), and Color(deetsHex:) / .deetsHex(in:) — the Swift port of the contrast audit, so the theme editor rates colors exactly as docs/contrast-audit.html does. |
CustomTheme.swift | CustomPaletteData (the Codable, locally-stored custom palette + fromStored/stored JSON helpers) and EditablePalette (the editor's working model). |
PaletteGenerator.swift | GeneratedPalette (@Generable) + PaletteGenerator — the on-device Foundation Models generator: a vibe → a palette. PaletteGenerator.isAvailable gates the UI. |
PaletteEditor.swift | PaletteEditor — color-picker rows + live WCAG pass/fail + "Fix Up". The manual floor when the model is unavailable. |
ThemeGeneratorView.swift | ThemeGeneratorView(existing:onSave:) — the "Theme" screen (vibe field + live preview + editor + 10s revert). Apps present it as a sheet from the Custom tile. |
What's intentionally internal (not part of the public API): Color(hex:),
Font.deets(size:), and Font.deetsScale. Call sites use the semantic tokens, never a
raw size or hex — keep it that way.
The theme generator (PaletteGenerator.swift) is the sole use of AI anywhere in DeetsKit. It
uses Apple's on-device Foundation Models (import FoundationModels, iOS 26 — DeetsKit's one
system-framework dependency, no SPM/entitlement change) to do exactly one thing: turn a user's
free-text vibe into a Palette via guided generation (the model fills a @GenerableGeneratedPalette; we parse its hex and validate contrast app-side). It runs fully on-device —
no network, nothing leaves the phone — and is device-gated: PaletteGenerator.isAvailable is
false on ineligible devices and usually in the Simulator, so the generator always falls back to the
manual color pickers. Nothing else in DeetsKit touches AI; everything else is pure SwiftUI. Full
design + cold-handoff notes in docs/THEME_GENERATOR.md.
A screen is composed from the primitives:
import DeetsKit
DeetsScreen{ // tinted canvas backdrop + width-capped content column
ScreenTitle(title:"…") // serif title, plain on the canvas
GlassCard{…} // an inset frosted surface
}Inject the active palette and skin once at the app root and let everything read them:
@AppStorage("themeID")privatevarthemeID="rose"@AppStorage("skinID")privatevarskinID="glass"privatevartheme:AppTheme{AppTheme.with(id: themeID)}privatevarskin:Skin{Skin.with(id: skinID)}
// …
ContentView().environment(\.deetsPalette, theme.palette).environment(\.deetsSkin, skin).preferredColorScheme(theme.isDark ?.dark :.light)Then never hardcode a color or font — read @Environment(\.deetsPalette) and use
.deetsFont(.body) / .foregroundStyle(.deetsSecondary). That's what keeps every Deets
app on one design language.
The look is theme (which colors) × skin (how surfaces/borders/canvas/type are drawn), two independent pickers. A skin is palette-blind — it reads the active palette exactly like the glass surface does and only decides how those colors render, so every screen restyles for free and the two laws stay intact. Glass is the default (byte-for-byte the original frosted look); comic and retro are flat. For a settings screen, drop in the one card that owns both pickers:
AppearanceCard(themeID: $themeID, skinID: $skinID) // $-bindings to the @AppStorage idsFor full design rationale and the open decisions, see docs/SKINS.md.
For landscape / iPad / Mac scaling, see docs/ADAPTIVE_LAYOUT.md — how DeetsScreen(maxWidth:),
DeetsSplit, and DeetsCanvas adapt to available width. DeetsKit reacts to size class, never
raw device orientation.
Card corners must follow the skin. Read
skin.resolvedCardCorner(not the rawDeetsLayout.cardCorner) for any full-card rounded-rect shape — the glass surface, aclipShape, acontentShape— so a skin that changes the corner radius needs no app edits.
- Adding a theme: append an
AppThemetoAppTheme.allwith its palette +isDark. Keep every new color WCAG-passing on glass and the flat skins (docs/contrast-audit.htmlhas rows for glass + comic + retro). - Adding a skin: append a
SkintoSkin.all(it composes existing surface/border/shadow/ canvas/typeface options — keep it palette-blind, no fixed colors). New surface/border/canvas treatments branch inGlassStyle.swift/CanvasBackground.swift; a new typeface maps a name inFonts.swift. Re-run the contrast audit if the skin introduces a new flat fill. - Adding a type role: add a
DeetsFonttoken rather than hardcoding a size at a call site. - The public/Sendable discipline: anything an app touches must be
public. New value types that back astatic lettable (likePalette/AppTheme/DeetsFont) must also beSendable, or Swift 6 strict concurrency rejects the global. Structs need an explicitpublic init— the synthesized memberwise init is internal. - The glass look is centralized in
GlassStyle.swift. It's Materials-based today; the Liquid Glass (iOS 26) equivalents are kept inline-commented so the whole look flips in one file.
DeetsKit is a sibling of the apps, not nested inside either:
~/Documents/GitHub/
├── DeetsKit/ ← this repo (one source of truth)
├── DeetsJack/ → depends on ../DeetsKit (project.yml `packages:`)
├── DeetsMusic/ → depends on ../DeetsKit (project.yml `packages:`)
├── DeetsTrips/ → depends on ../DeetsKit (project.yml `packages:`)
├── DeetsBoard/ → depends on ../DeetsKit (project.yml `packages:`)
└── DeetsHealth/ → depends on ../DeetsKit (theme + skin wired)
Each app references DeetsKit by relative path. A fresh clone of an app alone won't build — it needs DeetsKit checked out next to it. The package is linked statically into each app binary at build time, so it never ships as a separate artifact and has no effect on App Store distribution.
MIT — see LICENSE. Copyright (c) 2026 Aditya Sundaram.