Persistent named palettes with tabbed drawer, export, and UX polish#200
Persistent named palettes with tabbed drawer, export, and UX polish#200superhighfives merged 16 commits intomainfrom
Conversation
Extends color history with named palettes that users can create, rename, delete, and export as JSON. The tab bar is always visible, showing auto-history and any named palettes. Keyboard navigation and active state work across all palettes. Selecting a palette color records it to history. Includes migration from the old colorHistory storage format. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix preview toggle (P) flicker by deferring state reset - Fix swap to only update palette swatch when selected - Fix palette color selection requiring double-click - Fix keyboard navigation selecting wrong item when nothing selected - Restore correct history selection when switching back from palette tab - Remove swap button visibility timer for smoother animation - Rotate swap icon on keyboard shortcut - Remove clipping that cut off preview pill stroke - Reduce palette drawer padding and increase swatch corner radius - Make action button fill entire area with centered icon - Add compliance toggle (C) to navigation and help - Add inline palette name field replacing system alert - Add palette export as JSON via NSSavePanel - Prevent keyboard shortcuts intercepting text field input Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR updates Pika’s palette/history UX by introducing persistent palettes (including JSON export), improving keyboard navigation and selection behavior, and polishing several UI interactions/animations (preview pill stroke, swap hover/rotation, drawer sizing), plus a new “compliance” toggle.
Changes:
- Add palette model + Defaults storage, migrate existing history, and update history drawer UI into a palette-tabbed drawer with inline naming/rename/delete and add-to-palette actions.
- Add JSON export for palettes/history via menu items/context menus and an export save panel flow.
- Add compliance visibility toggle (menu + keyboard shortcut) and multiple UI/animation refinements (preview pill strokeBorder, swap hover cleanup, window sizing).
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| Pika/Views/NavigationMenuItems.swift | Adds palette creation/export and compliance toggle actions to the menu. |
| Pika/Views/NavigationMenu.swift | Adds toolbar compliance toggle and keyboard shortcut registration. |
| Pika/Views/HelpView.swift | Documents the new compliance shortcut. |
| Pika/Views/ContentView.swift | Wires compliance visibility + palette export save panel handling. |
| Pika/Views/ColorPreview.swift | Fixes preview pill stroke clipping and adjusts swap preview animations/behavior. |
| Pika/Views/ColorPickers.swift | Removes clipping to prevent UI elements from being cut off. |
| Pika/Views/ColorHistoryDrawer.swift | Reworks history drawer into palette UI (tabs, inline naming, add/clear behavior). |
| Pika/Services/WindowCoordinator.swift | Adjusts window minimum/ideal height to fit updated UI. |
| Pika/Services/Eyedroppers.swift | Moves history storage to palettes, adds palette management, updates navigation/selection logic. |
| Pika/Services/Exporter.swift | Adds palette/history JSON export helper. |
| Pika/Services/ColorPair.swift | Introduces Palette model to support palette persistence. |
| Pika/Constants/Defaults.swift | Adds new defaults for palettes, active palette index, and compliance visibility. |
| Pika/Constants/Constants.swift | Adds notifications + localized text keys for palettes/compliance/export. |
| Pika/ButtonStyles/SwapButtonStyle.swift | Improves hover/task cleanup when swap UI hides. |
| Pika/Assets/en.lproj/Localizable.strings | Adds/updates strings for palettes, export, and compliance toggle. |
| Pika/AppDelegate.swift | Adds migration from colorHistory to palettes and compliance toggle action. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Restore .clipped() on ColorPickers and add clipShape on ContentView to prevent content bleeding past window rounded corners - Reorder menu: three toggles, divider, then new palette and export - Export defaults to color history when palette drawer is hidden Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix swap undo consistency, context menu labels, index clamping, filename sanitization, compliance icon differentiation, unused import, and migration guard. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Want to do a follow up review, @copilot? |
Sure! Here's the follow-up review on the current state of the code: ✅ All previous feedback addressed correctlyThe seven issues from the first pass are cleanly resolved. Here are three new observations: 1. After
Fix: add func savePalette(name: String) {
let current = ColorPair(id: UUID(), ...)
...
Defaults[.activePaletteIndex] = palettes.count - 1
activeHistoryID = current.id // ← add this
}2. When 3. If the user right-click-removes the currently highlighted chip from a named palette, func deleteChipFromPalette(paletteIndex: Int, pairID: UUID) {
var palettes = Defaults[.palettes]
guard paletteIndex > 0, paletteIndex < palettes.count else { return }
palettes[paletteIndex].pairs.removeAll { $0.id == pairID }
Defaults[.palettes] = palettes
if activeHistoryID == pairID { activeHistoryID = nil } // ← add this
}Items 1 and 3 are clear bugs worth fixing; item 2 is worth a deliberate decision either way. |
|
Thanks for the follow-up review! Fixed #1 and #3:
|
Show keyboard shortcuts in toolbar tooltips (P, C, H). Replace palette tab crossfade with directional vertical slide. Fix savePalette not selecting the new chip and deleteChipFromPalette leaving a stale activeHistoryID. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
One more time, @copilot! |
Here's the third-pass review: ✅ Previous items resolvedBoth fixes from the last round look correct — New observations1.
The practical effect: every transition shows the direction of the previous index change, not the current one. Fix: set // in paletteTab(palette:index:)
Button(action: {
slideDirection = index > activePaletteIndex ? .down : .up // ← set first
activePaletteIndex = index
if index == 0 { eyedroppers.restoreAutoHistorySelection() }
})2. When the active palette is deleted, Fix: after clamping func deletePalette(at index: Int) {
...
if Defaults[.activePaletteIndex] >= palettes.count {
Defaults[.activePaletteIndex] = 0
restoreAutoHistorySelection() // ← add this
}
}3. Inside let formatter = ISO8601DateFormatter()
let entries: [[String: String]] = pairs.map { pair in
var entry: [String: String] = [...]
if isHistory { entry["date"] = formatter.string(from: pair.date) }
return entry
}Items 1 and 2 are behavioral bugs worth fixing before merge; item 3 is a minor performance fix. |
|
All three addressed:
|
Set slideDirection before index change via binding instead of onChange. Restore auto-history selection after deletePalette clamps to index 0. Hoist ISO8601DateFormatter out of pairs.map loop. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Last one I promise, @copilot. |
Here's the fourth-pass review: ✅ All three previous items resolved cleanlyThe slide-direction binding, New observations1. Only the "out of bounds" case is handled (clamp to 0). If the user deletes palette at index 2 while index 3 is active, after removal the array is func deletePalette(at index: Int) {
...
let currentIndex = Defaults[.activePaletteIndex]
if currentIndex >= palettes.count {
Defaults[.activePaletteIndex] = 0
restoreAutoHistorySelection()
} else if currentIndex > index {
Defaults[.activePaletteIndex] = currentIndex - 1 // ← keep pointing at the same palette
}
}2.
3.
Item 1 is a behavioral bug (active palette jumps to the wrong one); items 2 and 3 are lower-priority polish. |
|
All three fixed:
|
Decrement activePaletteIndex when deleting a tab before the active one. Set slideDirection to .down before savePalette. Move VerticalDirection to file scope to decouple PaletteTabBar from ColorHistoryDrawer. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Restructure PreferencesView to match Help/About pattern: visualization header at top with gradient overlay, scrollable content below, resizable window (550x600, min 440x400). Fix palette slide transition to use explicit withAnimation and offset-based transitions so both old and new content move the same direction. Fix MetalShader freezing on window reopen by assigning a new id to force view recreation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…e-in shader - Extract shared VisualisationHeader component used by Preferences, About, and Help - Widen preferences window to 580px with fixed width, vertical scroll layout - Add overscroll purple background and gradient mask to visualization headers - Fade MetalShader in over purple background to avoid loading flicker - Widen app mode button SVGs (223→260 viewBox) to fill wider layout - Make app mode button images resizable with aspect fit - Widen ColorExampleRow max width from 460 to 530 - Remove "Show foreground color on background color" toggle from preferences - Add version info to preferences header Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Make export and color space pickers full-width using menu picker style - Fix Color Format section padding alignment - Add divider below help visualization header - Increase visualization header heights (preferences 180, help 180, about 240) - Make global shortcut section use horizontal layout Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Localize all hardcoded URL trigger group titles and descriptions in the help view across all 8 languages (en, de, es, fr, hr, pl, zh-Hans, zh-Hant). Move the fade gradient from the button HStack to an overlay on the scroll view to close the gap between palette chips and the action button. Tie dividers to their respective panels so they hide when compliance/history are toggled off. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…tory fix - Add pika://compliance and pika://preview URL triggers (show/hide/toggle) - Add pika://seed?file=PATH to load history and palettes from JSON - Fix system colour picker not updating the active history entry - Update handleSet to sync history entry after colour change - Add new triggers to help view and localisation strings Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replaces the flat auto-history drawer with a tabbed palette system: a persistent auto-history palette at index 0, plus user-created named palettes. Several behavioral bugs were caught and fixed across iterative review.
Data layer
Palettemodel (id,name?,pairs,createdAt); auto-history ispalettes[0]wherename == nilmigrateHistoryToPalettes()merges legacycolorHistoryintopalettes[0].pairsrather than overwritingDefaults[.activePaletteIndex]tracks the active tab; clamped to 0 on out-of-rangePalette management (
Eyedroppers)savePalette,renamePalette,deletePalette,addCurrentToPalette,deleteChipFromPaletteswap()only callspushUndo()when on auto-history (index 0) — named-palette swaps no longer corrupt the undo stacksavePalettesetsactiveHistoryID = current.idso the new chip is selected immediatelydeleteChipFromPalettenilsactiveHistoryIDwhen the removed pair was activedeletePalettecallsrestoreAutoHistorySelection()after clamping to index 0, and decrementsactivePaletteIndexwhen deleting a palette before the active oneDrawer UI
PaletteTabBarwith inline create/rename viaPaletteNameField; auto-history tab shows a clock iconslideDirectionis set via@Bindingin the button action beforeactivePaletteIndexchanges, avoiding the one-frame-behind issue thatonChangecausedisAutoHistoryderived fromactivePalette?.isAutoHistory ?? false;activePaletteIndexis clamped back to 0 async when out of rangetextHistoryRemoveandtextPaletteRemoveChipbased onisAutoHistoryExport
Exporter.paletteToJSONserialises pairs; auto-history entries include an ISO 8601datefieldISO8601DateFormatterhoisted out of thepairs.mapclosure (one allocation per export, not per pair)/,:,\beforeNSSavePanel; write errors surface viaNSAlertUI polish
checkmark.shield.fillwhen active vscheckmark.shieldwhen inactive