ho-06.4 — palana palette: the semantic color-token layer
Give the Workshop a real palette. Today the app leans on the raw macOS system
accent (usually blue) and scatters .red / .green / .secondary across the
views. This replaces that with a small named token layer that maps the
pālana design system palette to
semantic roles — accent, drift, in-sync, ink text, surfaces — wired through the
existing AppAppearance light/dark/system override.
It is a foundation ho: it changes no behavior and adds no feature. It exists
so the upcoming right-side-panel ho (the failed-at-gate chrome from ho-06.2,
owed ) has a clean panelGround and a coherent accent to build on,
and so Sharibako and pālana read as siblings rather than two apps that
happened to pick similar greens.
Numbering. Provisional 06.4, to be ratified in the owed K4 overview pass
(the panel, multi-root management, and first-run/ingest hos are all still
unplaced). The panel ho follows as 06.5 there. This number lets the work start
now; it does not pre-empt the K4 pass.
Out of scope:
- The right-side collapsible panel itself — the next ho (
06.5). This one only hands it tokens. - Typography, spacing, radii, and motion. pālana specifies all of these too; this ho is color only. A later pass can consume the rest.
- Flattening the vibrancy materials.
.barand.quaternary(translucent system materials) stay as-is — see Decision 4. The panel ho makes the holistic flat-vs-vibrancy call across the whole window. - Editing pālana. The palette is read as the source of truth; zero files under
../../../palana/change. The dark values designed here (Decision 2) are documented in a form portable back to pālana, but that upstreaming is a separate pālana-side decision, not this ho. - Any change under
Sources/SharibakoCLI/orSources/SharibakoCore/— pure app-layer, same rule as ho-05 / 06.1 / 06.2. The CLI has no color.
Phase 1 —
Four decisions, ratified 2026-07-11.
Decision 1 — Nine semantic tokens, not a palette dump
The view code should never name a hue. It names a role. The token set:
| Token | pālana source | Role in Sharibako |
|---|---|---|
accentMoss |
accent (moss) |
The one interactive color — tint, selection, link icon, CTA wash |
drift |
alarm (rust) |
Drift, errors, failures, destructive intent |
inSync |
moss family (own tuned tone) | The affirmative — status/success pulse. A cleaner/brighter moss than the accent (gate-tuned, see ); still moss, no green traffic-light |
ink |
ink |
Primary text |
inkSecondary |
inkFaint |
Secondary facts — the workhorse .secondary replacement |
inkTertiary |
inkFaint (fainter) |
Faintest hints — the .tertiary replacement |
ground |
ground |
Warm paper — defined now, consumed by the panel ho |
groundDeep |
groundDeep |
A shade off ground — headers, chips (panel ho) |
panelGround |
panelGround |
The cooler "data" surface — the panel ho's reason this ho exists |
pālana's optional plugin/umber is dropped: Sharibako has no category axis
(pālana §8 explicitly permits this). That keeps us at pālana's three working
hues — paper, ink, moss — plus the single rust alarm voice.
Decision 2 — pālana is light-only; the dark set is designed here (as siblings, portable upstream)
pālana §2 gives only light values (warm paper ground, near-black ink).
Sharibako must read well in both appearances (the AppAppearance override is
live). So the dark half of every token is new , in pālana's spirit — warm
throughout, never pure black or white, one moss accent, one rust alarm, lifted
just enough to hold contrast on a dark ground.
| Token | Light (pālana §2, authoritative) | Dark (designed here) |
|---|---|---|
ground |
#FAF7F3 |
#1B1A17 (warm near-black) |
groundDeep |
#F4F1EA |
#24221E (raised warm) |
panelGround |
#EDEEF1 (cool slate) |
#20222A (cool dark slate) |
ink |
#1D1B18 |
#ECE7DF (warm off-white) |
inkSecondary |
ink @ 0.55α |
ink(dark) @ 0.60α |
inkTertiary |
ink @ 0.35α |
ink(dark) @ 0.40α |
accentMoss |
#5A7552 |
#7E9B72 (lifted moss) |
inSync (own tone) |
#5E8A50 |
#85BE73 (cleaner/brighter, gate-tuned) |
drift |
#984D3C |
#C56B57 (lifted rust) |
These dark values are the candidate to bring pālana in line later; they are written here so that a future pālana dark section can adopt them verbatim. pālana files are not touched in this ho.
Decision 3 — Colorblind-safety is unchanged: stays primary
inSync moss vs drift rust is a red/green axis — the exact thing the standing
constraint guards. It holds because color stays secondary: the sidebar
glyphs and drift badges keep their shape distinction (mappin.circle.fill vs
arrow.up.forward.circle; checkmark.seal vs exclamationmark.triangle.fill)
and their neutral inkSecondary fill. Only transient status pulses and text
labels carry the moss/rust tint, and each of those also carries a word ("In
sync" / "Differs") or a shape. No state is distinguishable by hue alone. The
glyph/badge colors are therefore not migrated to moss/rust — they stay
neutral, as ho-06.2 designed them.
Decision 4 — Migrate text + accent + status now; keep the vibrancy materials
Scope of the swap this ho performs:
- Accent — set the app
.tinttoaccentMoss(was: unset → system blue);.accentColorusages →accentMoss..borderedProminentCTAs inherit the tint automatically, soprimaryActionButton()flips to moss with no change. - Status — all
.red→drift; the oneColor.green→inSync. - Text —
.secondary→inkSecondary,.tertiary→inkTertiary. - Kept as-is — the
.barand.quaternarymaterials (translucent, already appearance-adaptive) and theground*tokens' consumption. Replacing vibrancy with flat palette grounds is the visible "notebook-over-Mac" move; it wants to be made holistically in the panel ho, across the whole window, not piecemeal here. The surface tokens are defined now so the panel can consume them; the existing materials are left now so this foundation stays low-risk in both appearances.
Mechanism
Color+Theme.swift — a Color extension over an internal Theme/Palette
layer. Each token is an appearance-aware color built with
NSColor(name:dynamicProvider:): no asset catalog, no Package.swift resource
change, and — because the raw values live in a pure Palette.resolved(dark:)
seam — unit-testable without a running scene. The file carries real coverage
and is therefore not added to the CI exclusion list.
Tokens are named Color.accentMoss etc. and used explicitly (Color.drift, not
.drift) because foregroundStyle(_: some ShapeStyle) won't resolve a
leading-dot member against the generic parameter.
Phase 2 — Build
Sources/Sharibako/Color+Theme.swift—RGBA,Palette,Theme, and theColorextension exposing the nine tokens.Sources/Sharibako/App.swift—.tint(Color.accentMoss)on every scene root.- View migrations (Decision 4) across
SecretList,SecretDetail,ScopeSidebar,WorkshopWindow, and the three Add sheets. Tests/SharibakoAppTests/ColorThemeTests.swift— assert every token's resolved light and dark RGBA against Decision 2, plus a dynamic-resolution test that exercises the appearance both ways.
Verification each change: swift package clean && swift test →
swift build -Xswiftc -warnings-as-errors →
swift-format lint --strict --recursive Sources Tests →
swiftlint lint --strict → coverage ≥90%.
Phase 3 — Reflect
As built (2026-07-11). The token layer shipped as planned. Color+Theme.swift
holds the nine tokens over a pure Palette.resolved(dark:) seam and a dynamic
NSColor(name:dynamicProvider:) provider — no asset catalog, no Package.swift
change. Migrations landed across all seven views + App.swift (the .tint on
every scene root). .borderedProminent CTAs picked up moss for free through the
tint, so primaryActionButton() was untouched. The vibrancy materials
(.bar, .quaternary) were left as-is (Decision 4); ground/groundDeep/
panelGround are defined and tested but not yet consumed — they are the panel
ho's to spend. One explicit .primary (SecretDetail) was deliberately left on
the system label to stay consistent with all the unstyled default body text;
Color.ink is defined for the panel ho to consume alongside a real ground.
Verification: 731 tests pass under -warnings-as-errors; both linters clean
--strict; Color+Theme.swift at 100% line/region/function coverage; overall
line coverage 94.78% (floor 90%). No CI exclusion added — the token file is
fully tested, unlike the declarative View bodies.
Visual gate — passed (2026-07-11). Unit tests prove the tokens resolve correctly but not that the palette reads well, so it was eyeballed on a real run in both appearances (selection/CTA moss, drift rust, status/error pulses, light and dark both holding as siblings with pālana). Andrew called it good.
One gate-tuned change: the success/status pulse (moss @ 0.25 over the dark
.bar) muddied to olive and didn't pop, so inSync was decoupled from the
accent into its own cleaner/brighter moss (#5E8A50 / #85BE73) and both
pulses were raised 0.25 → 0.40 (the accent on buttons/selection was left
untouched — it read fine). Rust was confirmed scoped to drift, errors, and
validation only — deliberately not the success pulse, since success and
error must never share a hue. (Destructive-verb rust, which pālana also calls
for, is not yet wired — Sharibako's delete affordances were outside this ho's
migration; flagged for a later pass.)
As-built gate note (correcting a wrong turn taken this session): the Workshop's
real build and path is the committed xcode/Sharibako.xcodeproj —
a -authored Xcode project over the same Swift package, signed Developer ID,
producing Sharibako.app in /Applications ( §"Building from source").
That is how the GUI has been gated in every prior ho. During this session I
wrongly reached for swift run and then a throwaway .app wrapper instead —
both the wrong tool, both abandoned. The theme is reviewed by rebuilding the
Xcode app. There is no owed signed-GUI install path; that path already
exists — an earlier draft of this note claimed otherwise and was wrong.
Forward — pālana upstream. The dark values (Decision 2) are the candidate to bring pālana in line (Andrew's note: the two must read as siblings, and this may recurse back into pālana). That is a pālana-side decision, tracked, not part of this ho — pālana files stayed untouched.
State-summary
- COMPLETED: ho-06.4 — nine-token palana palette wired through
AppAppearance; accent/status/secondary/tertiary migrated across the app; gate-tuned the success pulse (decoupledinSync, 0.40 opacity). Full stack green (731 tests, ~94.8% coverage, both linters strict, theme file 100%). Visual light/dark gate passed; the signed Xcode app was the review surface. Merged tomainvia PR #14. - NEXT: the right-side chrome/panel ho (provisional 06.5) — the failed-at-gate
06.2 chrome, forward-only; it consumes this ho's
ground*/panelGroundtokens and rebases ontomain. K4 pass ratifies the 06.4/06.5 numbering. - ACTION ITEMS / BLOCKS: none. (An earlier draft here wrongly listed a
signed-GUI install path as owed — corrected: the Workshop builds from the
committed
xcode/Sharibako.xcodeprojinto a signed/Applicationsapp.) - PROJECT LIFECYCLE: dev.
Rendered from the corpus, verbatim · source on GitHub →