Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
897a33b
feat(keyboard): add M0116 keycap style
esphynox Aug 25, 2026
de289d1
refactor(keyboard): restrict theme options by keycap style
esphynox Aug 25, 2026
d82e5c5
docs(keyboard): clarify allowed theme picker sections
esphynox Aug 25, 2026
689b4aa
refactor(keyboard): move keycap style helpers into extensions
esphynox Aug 25, 2026
7154cfd
refactor(keyboard): split keycap appearance into style files
esphynox Aug 25, 2026
f061df2
refactor(keyboard): extract shared keycap appearance tokens
esphynox Aug 25, 2026
b74b860
refactor(keyboard): streamline shared keycap appearance access
esphynox Aug 26, 2026
05b8a56
refactor(keyboard): add style-specific keycap presentation policy
esphynox Aug 26, 2026
f8df758
feat(keyboard): add dashed arrow legends for m0116 style
esphynox Aug 26, 2026
7b02c20
feat(keyboard): hide space legend for m0116 style
esphynox Aug 26, 2026
bbda51f
feat(keyboard): render text-only return key for m0116
esphynox Aug 26, 2026
6f3725b
feat(keyboard): render text-only tab key for m0116
esphynox Aug 26, 2026
ac9e682
feat(keyboard): render m0116 shift without modifier glyph
esphynox Aug 26, 2026
7a22727
feat(keyboard): hide control glyphs for m0116 modifiers
esphynox Aug 26, 2026
4c2aecc
feat(keyboard): remove m0116 option and delete glyphs
esphynox Aug 26, 2026
1d7deab
feat(keyboard): remove m0116 command glyph
esphynox Aug 26, 2026
b9c256c
feat(keyboard): render m0116 fn key as label-only
esphynox Aug 26, 2026
1278901
docs(keyboard): document keycap presentation policy contract
esphynox Aug 26, 2026
7773301
feat(keyboard): hide caps lock indicator dot for m0116
esphynox Aug 26, 2026
057bb92
feat(keyboard): darken m0116 legend color
esphynox Aug 26, 2026
a36728b
feat(keyboard): refine m0116 keycap presentation
esphynox Aug 26, 2026
fafd959
feat(keyboard): wrap multiline m0116 key legends
esphynox Aug 26, 2026
507b2ab
feat(keyboard): keep m0116 navigation keys at regular width
esphynox Aug 26, 2026
26b9171
feat(keyboard): restore sculpted m0116 keycap geometry
esphynox Aug 27, 2026
b9e0d6c
feat(keyboard): clarify Apple visualizer style names
esphynox Aug 27, 2026
d795579
fix(settings): widen keyboard style picker
esphynox Aug 27, 2026
fa77df5
refactor(keyboard): split keycap appearance theme tokens into separat…
esphynox Aug 27, 2026
05a7749
Merge branch 'main' into feature/m0116-visualizer
esphynox Aug 27, 2026
f4460a3
refactor(keyboard): align keycap shared appearance initializer ordering
esphynox Aug 27, 2026
ddb411d
refactor(keyboard): add explicit keycap theme tokens initializer
esphynox Aug 27, 2026
46893d9
refactor(keyboard): split m0116 side seam paths by side
esphynox Aug 27, 2026
c8bd4c5
test(keyboard): update m0116 settings color expectations
esphynox Aug 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ struct KeyboardSettingsPane: View {
subtitle: L10n.KeyboardVisualizer.themeSubtitle
) {
Picker("", selection: self.$model.themeSelection) {
ForEach(Array(KeyboardVisualizerTheme.pickerSections.enumerated()), id: \.offset) { index, section in
ForEach(Array(self.model.allowedThemeSections.enumerated()), id: \.offset) { index, section in
if index > 0 {
Divider()
}
Expand Down Expand Up @@ -341,7 +341,7 @@ struct KeyboardSettingsPane: View {
) -> some View {
SettingsControlRow(title: title, subtitle: subtitle) {
Picker("", selection: selection) {
ForEach(Array(KeyboardVisualizerTheme.pickerSections.enumerated()), id: \.offset) { index, section in
ForEach(Array(self.model.allowedThemeSections.enumerated()), id: \.offset) { index, section in
if index > 0 {
Divider()
}
Expand Down Expand Up @@ -381,7 +381,7 @@ struct KeyboardSettingsPane: View {
}
.labelsHidden()
.accessibilityLabel(L10n.KeyboardVisualizer.styleLabel)
.frame(width: Size.Control.settingsPickerWidth, alignment: .trailing)
.frame(width: Size.Control.keyboardStylePickerWidth, alignment: .trailing)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,15 @@ final class KeyboardSettingsPaneViewModel: ObservableObject {
}

@Published var theme: KeyboardVisualizerTheme {
didSet { self.settings.theme = self.theme }
didSet {
guard self.theme != oldValue else { return }
let sanitized = self.style.sanitize(theme: self.theme)
guard sanitized == self.theme else {
self.theme = sanitized
return
}
self.settings.theme = sanitized
}
}

@Published var legendColorMode: KeyboardLegendColorMode {
Expand Down Expand Up @@ -77,27 +85,71 @@ final class KeyboardSettingsPaneViewModel: ObservableObject {
}

@Published var modifierTheme: KeyboardVisualizerTheme {
didSet { self.settings.modifierTheme = self.modifierTheme }
didSet {
guard self.modifierTheme != oldValue else { return }
let sanitized = self.style.sanitize(theme: self.modifierTheme)
guard sanitized == self.modifierTheme else {
self.modifierTheme = sanitized
return
}
self.settings.modifierTheme = sanitized
}
}

@Published var specialTheme: KeyboardVisualizerTheme {
didSet { self.settings.specialTheme = self.specialTheme }
didSet {
guard self.specialTheme != oldValue else { return }
let sanitized = self.style.sanitize(theme: self.specialTheme)
guard sanitized == self.specialTheme else {
self.specialTheme = sanitized
return
}
self.settings.specialTheme = sanitized
}
}

@Published var mediaTheme: KeyboardVisualizerTheme {
didSet { self.settings.mediaTheme = self.mediaTheme }
didSet {
guard self.mediaTheme != oldValue else { return }
let sanitized = self.style.sanitize(theme: self.mediaTheme)
guard sanitized == self.mediaTheme else {
self.mediaTheme = sanitized
return
}
self.settings.mediaTheme = sanitized
}
}

@Published var mouseTheme: KeyboardVisualizerTheme {
didSet { self.settings.mouseTheme = self.mouseTheme }
didSet {
guard self.mouseTheme != oldValue else { return }
let sanitized = self.style.sanitize(theme: self.mouseTheme)
guard sanitized == self.mouseTheme else {
self.mouseTheme = sanitized
return
}
self.settings.mouseTheme = sanitized
}
}

@Published var groupBackgroundTheme: KeyboardVisualizerTheme {
didSet { self.settings.groupBackgroundTheme = self.groupBackgroundTheme }
didSet {
guard self.groupBackgroundTheme != oldValue else { return }
let sanitized = self.style.sanitize(theme: self.groupBackgroundTheme)
guard sanitized == self.groupBackgroundTheme else {
self.groupBackgroundTheme = sanitized
return
}
self.settings.groupBackgroundTheme = sanitized
}
}

@Published var style: KeycapStyle {
didSet { self.settings.style = self.style }
didSet {
guard self.style != oldValue else { return }
self.settings.style = self.style
self.syncThemeStateFromSettings()
}
}

@Published var scale: Double {
Expand Down Expand Up @@ -175,6 +227,11 @@ final class KeyboardSettingsPaneViewModel: ObservableObject {
].joined(separator: "-")
}

/// Theme picker sections filtered to the themes allowed by the selected keycap style.
var allowedThemeSections: [[KeyboardVisualizerTheme]] {
KeyboardVisualizerTheme.pickerSections(for: self.style.allowedThemes)
}

var previewRenderSettings: KeyboardVisualizerSettings {
let settings = KeyboardVisualizerSettings(store: InMemoryKeyValueStore())
settings.registerDefaults()
Expand Down Expand Up @@ -246,6 +303,15 @@ final class KeyboardSettingsPaneViewModel: ObservableObject {
self.customLegendColor = color
self.legendColorSelectionOverride = Self.customLegendColorSelectionID
}

private func syncThemeStateFromSettings() {
self.theme = self.settings.theme
self.modifierTheme = self.settings.modifierTheme
self.specialTheme = self.settings.specialTheme
self.mediaTheme = self.settings.mediaTheme
self.mouseTheme = self.settings.mouseTheme
self.groupBackgroundTheme = self.settings.groupBackgroundTheme
}
}

extension KeyboardSettingsPaneViewModel {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//
// KeycapAppearance+Apple.swift
// Keyty
//
// SPDX-FileCopyrightText: 2026 Serhii Bykov
// SPDX-License-Identifier: BSD-3-Clause
//

import AppKit

extension KeycapAppearance {
struct Apple: KeycapAppearanceProviding {
let shared: Shared
let strokeColor: NSColor
let undersideGradient: NSGradient?
let mainGradient: NSGradient?

init(tokens: KeycapThemeTokens) {
self.shared = Shared(tokens: tokens)
self.strokeColor = tokens.surfaceBorderColor
let undersideEdgeColor = tokens.undersideEdgeColor ?? tokens.recessColor
let undersideCenterColor = tokens.undersideCenterColor ?? tokens.surfaceBaseColor
self.undersideGradient = NSGradient(colorsAndLocations:
(undersideEdgeColor, 0.0),
(undersideCenterColor, 0.5),
(undersideEdgeColor, 1.0)
)
self.mainGradient = NSGradient(colorsAndLocations:
(tokens.surfaceBaseColor, 0.0),
(tokens.surfaceShadowColor, 1.0)
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//
// KeycapAppearance+M0116.swift
// Keyty
//
// SPDX-FileCopyrightText: 2026 Serhii Bykov
// SPDX-License-Identifier: BSD-3-Clause
//

import AppKit

extension KeycapAppearance {
/// Sculpted beige-era keycap: a lit body, an inset top face, and a front skirt,
/// all sitting in a near-black well like the plate of an Apple Standard Keyboard.
struct M0116: KeycapAppearanceProviding {
let shared: Shared
let wellColor: NSColor
let bodyGradient: NSGradient?
let skirtGradient: NSGradient?
let faceGradient: NSGradient?
let creaseColor: NSColor
let bodyStrokeColor: NSColor

init(tokens: KeycapThemeTokens) {
let bodyEdge = tokens.surfaceShadowColor.darkened(by: 0.28)
let bodyHighlight = tokens.surfaceHighlightColor.blended(
withFraction: 0.35,
of: tokens.surfaceBaseColor
) ?? tokens.surfaceHighlightColor
let legendTextColor = tokens.textColor.darkened(by: 0.18)

self.shared = Shared(
badgeFillColor: tokens.badgeFillColor,
badgeHighlightColor: tokens.badgeHighlightColor,
badgeStrokeColor: tokens.badgeStrokeColor,
badgeTextColor: tokens.badgeTextColor,
groupBackgroundColor: tokens.groupBackgroundColor,
groupStrokeColor: tokens.groupStrokeColor,
textColor: legendTextColor
)
self.wellColor = tokens.recessColor.darkened(by: 0.45)
// Lit from the left: bright edge, base across the middle, shaded right edge.
self.bodyGradient = NSGradient(colorsAndLocations:
(tokens.surfaceHighlightColor, 0.0),
(bodyHighlight, 0.14),
(tokens.surfaceBaseColor, 0.55),
(tokens.surfaceShadowColor, 0.92),
(bodyEdge, 1.0)
)
// The front skirt catches more light at its lower edge than the face above it.
self.skirtGradient = NSGradient(colorsAndLocations:
(bodyEdge, 0.0),
(tokens.surfaceBaseColor, 0.45),
(tokens.surfaceHighlightColor, 1.0)
)
self.faceGradient = NSGradient(colorsAndLocations:
(tokens.surfaceHighlightColor, 0.0),
(tokens.surfaceBaseColor, 0.58),
(tokens.surfaceShadowColor, 1.0)
)
self.creaseColor = tokens.surfaceBorderColor.withAlphaComponent(0.42)
self.bodyStrokeColor = tokens.surfaceBorderColor
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//
// KeycapAppearance+Minimal.swift
// Keyty
//
// SPDX-FileCopyrightText: 2026 Serhii Bykov
// SPDX-License-Identifier: BSD-3-Clause
//

import AppKit

extension KeycapAppearance {
struct Minimal: KeycapAppearanceProviding {
let shared: Shared

init(tokens: KeycapThemeTokens) {
self.shared = Shared(tokens: tokens)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//
// KeycapAppearance+PBT.swift
// Keyty
//
// SPDX-FileCopyrightText: 2026 Serhii Bykov
// SPDX-License-Identifier: BSD-3-Clause
//

import AppKit

extension KeycapAppearance {
struct PBT: KeycapAppearanceProviding {
let shared: Shared
let bodyGradient: NSGradient?
let bodyStrokeColor: NSColor
let underDishColor: NSColor
let dishGradient: NSGradient?

init(tokens: KeycapThemeTokens) {
let bodyTop = tokens.surfaceBaseColor.lightened(by: 0.08)
let bodyMid = tokens.surfaceShadowColor.lightened(by: 0.03)
let bodyBottom = tokens.surfaceShadowColor.darkened(by: 0.08)
let dishTop = tokens.surfaceBaseColor.lightened(by: 0.05)
let dishMid = tokens.surfaceBaseColor.lightened(by: 0.06)
let dishBottom = tokens.surfaceShadowColor.lightened(by: 0.05)

self.shared = Shared(tokens: tokens)
self.bodyGradient = NSGradient(colorsAndLocations:
(bodyTop, 0.0),
(bodyMid, 0.48),
(bodyBottom, 1.0)
)
self.bodyStrokeColor = tokens.surfaceBorderColor
self.underDishColor = tokens.surfaceBaseColor.lightened(by: 0.12)
self.dishGradient = NSGradient(colorsAndLocations:
(dishTop, 0.0),
(dishMid, 0.32),
(dishBottom, 1.0)
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
//
// KeycapAppearance+Retro.swift
// Keyty
//
// SPDX-FileCopyrightText: 2026 Serhii Bykov
// SPDX-License-Identifier: BSD-3-Clause
//

import AppKit

extension KeycapAppearance {
struct Retro: KeycapAppearanceProviding {
let shared: Shared
let lipGradient: NSGradient?
let bodyShadowColor: NSColor
let bodyGradient: NSGradient?
let bodyStrokeColor: NSColor
let faceShadowGradient: NSGradient?
let darkFaceGradient: NSGradient?
let darkFaceStrokeColor: NSColor
let lightFaceGradient: NSGradient?
let lightFaceStrokeColor: NSColor
let glossColor: NSColor

init(tokens: KeycapThemeTokens) {
let bodyEdge = tokens.surfaceShadowColor.darkened(by: 0.08)
let bodyCenter = tokens.surfaceBaseColor.darkened(by: 0.10)
let darkFaceTop = tokens.surfaceBaseColor.lightened(by: 0.03)
let darkFaceBottom = tokens.surfaceBaseColor.darkened(by: 0.01)
let lightFaceTop = tokens.surfaceHighlightColor.lightened(by: 0.08)
let lightFaceBottom = tokens.surfaceBaseColor
let groupBackgroundColor = tokens.recessColor.darkened(by: 0.40)
let groupStrokeColor = tokens.surfaceBaseColor.withAlphaComponent(0.70)
let lipEdge = tokens.recessColor.darkened(by: 0.34).withAlphaComponent(0.96)
let lipCenter = tokens.recessColor.darkened(by: 0.12).withAlphaComponent(0.86)
let bodyStrokeColor = tokens.surfaceBorderColor.withAlphaComponent(0.20)
let darkFaceStrokeColor = tokens.surfaceBorderColor.withAlphaComponent(0.20)
let faceShadowEdge = tokens.recessColor.darkened(by: 0.34).withAlphaComponent(0.46)
let faceShadowCenter = tokens.recessColor.darkened(by: 0.12).withAlphaComponent(0.26)

self.shared = Shared(
badgeFillColor: tokens.badgeFillColor,
badgeHighlightColor: tokens.badgeHighlightColor,
badgeStrokeColor: tokens.badgeStrokeColor,
badgeTextColor: tokens.badgeTextColor,
groupBackgroundColor: groupBackgroundColor,
groupStrokeColor: groupStrokeColor,
textColor: tokens.textColor
)
self.lipGradient = NSGradient(colorsAndLocations:
(lipEdge, 0.0),
(lipCenter, 0.5),
(lipEdge, 1.0)
)
self.bodyShadowColor = NSColor.black.withAlphaComponent(0.6)
self.bodyGradient = NSGradient(colorsAndLocations:
(bodyEdge, 0.0),
(bodyCenter, 0.32),
(bodyCenter, 0.68),
(bodyEdge, 1.0)
)
self.bodyStrokeColor = bodyStrokeColor
self.faceShadowGradient = NSGradient(colorsAndLocations:
(faceShadowEdge, 0.0),
(faceShadowCenter, 0.5),
(faceShadowEdge, 1.0)
)
self.darkFaceGradient = NSGradient(colorsAndLocations:
(darkFaceTop, 0.0),
(darkFaceBottom, 1.0)
)
self.darkFaceStrokeColor = darkFaceStrokeColor
self.lightFaceGradient = NSGradient(colorsAndLocations:
(lightFaceTop, 0.0),
(lightFaceBottom, 1.0)
)
self.lightFaceStrokeColor = tokens.surfaceBorderColor.withAlphaComponent(0.55)
self.glossColor = NSColor(white: 1, alpha: 0.08)
}
}
}
Loading
Loading