From e2af8003accf2779ec6d895c203bb07500505f61 Mon Sep 17 00:00:00 2001 From: Jiun Bae Date: Mon, 26 Oct 2026 22:13:32 +0900 Subject: [PATCH] design: the Dock draws from the same tokens Widget tiles use the card radius and fills, hover labels and folder stacks the spacing scale, the glance meters the track fill, and the profile list's Active tag is a Pill. Hover magnification and reordering use Motion (and respect Reduce Motion as before). Folder-tile artwork keeps its own values. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../MenubucketApp/Dock/DockFolderStack.swift | 10 +++---- .../MenubucketApp/Dock/DockSettingsPage.swift | 8 ++---- Sources/MenubucketApp/Dock/DockView.swift | 26 +++++++++---------- .../MenubucketApp/Dock/DockWidgetGlance.swift | 6 ++--- .../Fixtures/design-literal-baseline.json | 5 +--- 5 files changed, 24 insertions(+), 31 deletions(-) diff --git a/Sources/MenubucketApp/Dock/DockFolderStack.swift b/Sources/MenubucketApp/Dock/DockFolderStack.swift index 80d4ef9..50b502c 100644 --- a/Sources/MenubucketApp/Dock/DockFolderStack.swift +++ b/Sources/MenubucketApp/Dock/DockFolderStack.swift @@ -53,11 +53,11 @@ struct DockFolderStack: View { .frame(maxWidth: .infinity, minHeight: 120) } else { ScrollView { - LazyVGrid(columns: Array(repeating: GridItem(.fixed(Self.cellWidth), spacing: 8), count: Self.columns), + LazyVGrid(columns: Array(repeating: GridItem(.fixed(Self.cellWidth), spacing: Spacing.xs), count: Self.columns), spacing: Self.rowSpacing) { ForEach(entries) { entry in cell(entry) } } - .padding(12) + .padding(Spacing.s) } // A scroll view has no height of its own, and the popover // took the smallest it could: one row. Rows decide it, up to @@ -91,8 +91,8 @@ struct DockFolderStack: View { } .controlSize(.small) } - .padding(.horizontal, 12) - .padding(.vertical, 8) + .padding(.horizontal, Spacing.s) + .padding(.vertical, Spacing.xs) } private func cell(_ entry: Entry) -> some View { @@ -104,7 +104,7 @@ struct DockFolderStack: View { onClose() } } label: { - VStack(spacing: 4) { + VStack(spacing: Spacing.xxs) { Image(nsImage: DockActions.fileIcon(at: entry.url.path)) .resizable() .interpolation(.high) diff --git a/Sources/MenubucketApp/Dock/DockSettingsPage.swift b/Sources/MenubucketApp/Dock/DockSettingsPage.swift index 7937849..6953fec 100644 --- a/Sources/MenubucketApp/Dock/DockSettingsPage.swift +++ b/Sources/MenubucketApp/Dock/DockSettingsPage.swift @@ -276,11 +276,7 @@ struct DockSettingsPage: View { Text(profile.name) .fontWeight(isSelected ? .semibold : .regular) if isActive { - Text("Active") - .font(.caption) - .padding(.horizontal, 6) - .padding(.vertical, 1) - .background(Color.accentColor.opacity(0.15), in: Capsule()) + Pill(text: String(localized: "Active"), color: .accentColor) } Spacer() if config.profileHotkeysEnabled, let label = DockHotkeys.label(forPosition: position) { @@ -527,7 +523,7 @@ struct DockSettingsPage: View { Toggle("Switch the Apple Dock's apps with the profile", isOn: binding(\.appleDockLayouts)) if let layout = profile.appleDock { LabeledContent("Saved in \(profile.name)") { - VStack(alignment: .trailing, spacing: 2) { + VStack(alignment: .trailing, spacing: Spacing.hair) { Text(Self.summary(layout.appNames + layout.otherNames)) .lineLimit(2) .multilineTextAlignment(.trailing) diff --git a/Sources/MenubucketApp/Dock/DockView.swift b/Sources/MenubucketApp/Dock/DockView.swift index d2c833b..46ec537 100644 --- a/Sources/MenubucketApp/Dock/DockView.swift +++ b/Sources/MenubucketApp/Dock/DockView.swift @@ -235,8 +235,8 @@ struct DockView: View { .overlay(alignment: edge == .bottom ? .top : .center) { profileBanner } .fixedSize() .onChange(of: hoveredID == nil) { _, resting in onHoverChange(!resting) } - .animation(reduceMotion ? nil : .spring(response: 0.22, dampingFraction: 0.82), value: hoveredID) - .animation(reduceMotion ? nil : .easeInOut(duration: 0.2), value: tiles.map(\.id)) + .animation(Motion.respecting(reduceMotion, Motion.quick), value: hoveredID) + .animation(Motion.respecting(reduceMotion, Motion.arrange), value: tiles.map(\.id)) .onChange(of: tiles.map(\.id)) { _, ids in DockMenuRouter.shared.keep(only: Set(ids)) if let hoveredID, !ids.contains(hoveredID) { self.hoveredID = nil } @@ -280,7 +280,7 @@ struct DockView: View { } else { shape .fill(isClassic ? AnyShapeStyle(.ultraThinMaterial) : AnyShapeStyle(.regularMaterial)) - .overlay(shape.strokeBorder(Color.primary.opacity(0.12), lineWidth: 0.5)) + .overlay(shape.strokeBorder(Fill.hairline, lineWidth: 0.5)) .shadow(color: .black.opacity(0.18), radius: 10, y: 2) } } @@ -298,8 +298,8 @@ struct DockView: View { if let profile = store.announcedProfile { Label(profile.name, systemImage: profile.symbol) .font(.headline) - .padding(.horizontal, 14) - .padding(.vertical, 7) + .padding(.horizontal, Spacing.s + Spacing.hair) + .padding(.vertical, Spacing.stack) .background(.regularMaterial, in: Capsule()) .offset(y: edge == .bottom ? -44 : 0) .transition(.opacity.combined(with: .scale(scale: 0.9))) @@ -593,7 +593,7 @@ struct DockView: View { @ViewBuilder private func widgetTile(item: DockItem, widgetID: String) -> some View { let frame = widgetFrame(for: widgetID) - let shape = RoundedRectangle(cornerRadius: 12, style: .continuous) + let shape = RoundedRectangle(cornerRadius: Radius.card, style: .continuous) if isClassic, let widget = runtime.widgets.first(where: { $0.id == widgetID }) { // Classic keeps the Apple Dock's height: a glance, not a card. // A button, so keyboard and VoiceOver users can reach and open it. @@ -609,7 +609,7 @@ struct DockView: View { WidgetCardView(widget: widget, runtime: runtime, compactHeight: frame.height, placement: .single) .frame(width: frame.width, height: frame.height) .clipShape(shape) - .overlay(shape.strokeBorder(Color.primary.opacity(0.08), lineWidth: 0.5)) + .overlay(shape.strokeBorder(Fill.hairline, lineWidth: 0.5)) } else { VStack(spacing: Spacing.xxs) { Image(systemName: "questionmark.square.dashed").font(.title2) @@ -617,7 +617,7 @@ struct DockView: View { } .foregroundStyle(.secondary) .frame(width: frame.width, height: frame.height) - .background(Color.primary.opacity(0.05), in: shape) + .background(Fill.card, in: shape) } } @@ -784,7 +784,7 @@ struct DockOverflowStack: View { ) .onChange(of: focusedID) { _, id in guard let id else { return } - withAnimation(reduceMotion ? nil : .easeOut(duration: 0.2)) { proxy.scrollTo(id) } + withAnimation(Motion.respecting(reduceMotion, Motion.quick)) { proxy.scrollTo(id) } } } } else { @@ -809,11 +809,11 @@ private struct HoverLabel: View { var body: some View { Text(title) - .font(.system(size: 13)) - .padding(.horizontal, 10) - .padding(.vertical, 4) + .font(.body) + .padding(.horizontal, Spacing.s) + .padding(.vertical, Spacing.xxs) .background(.regularMaterial, in: Capsule()) - .overlay(Capsule().strokeBorder(Color.primary.opacity(0.1), lineWidth: 0.5)) + .overlay(Capsule().strokeBorder(Fill.hairline, lineWidth: 0.5)) } } diff --git a/Sources/MenubucketApp/Dock/DockWidgetGlance.swift b/Sources/MenubucketApp/Dock/DockWidgetGlance.swift index d835f4b..c311889 100644 --- a/Sources/MenubucketApp/Dock/DockWidgetGlance.swift +++ b/Sources/MenubucketApp/Dock/DockWidgetGlance.swift @@ -33,7 +33,7 @@ struct DockWidgetGlance: View { let summary = self.summary let metrics = Array((summary?.metrics ?? []).prefix(2)) VStack(alignment: .leading, spacing: height * 0.06) { - HStack(spacing: 4) { + HStack(spacing: Spacing.xxs) { if let symbol = summary?.symbol { Image(systemName: symbol).imageScale(.small) } @@ -69,7 +69,7 @@ struct DockWidgetGlance: View { private func meter(_ metric: SharedShelf.Metric) -> some View { let barHeight = max(3, height * 0.08) - return HStack(spacing: 4) { + return HStack(spacing: Spacing.xxs) { if let label = metric.label { Text(label) .font(.system(size: max(8, height * 0.18))) @@ -79,7 +79,7 @@ struct DockWidgetGlance: View { } GeometryReader { proxy in ZStack(alignment: .leading) { - Capsule().fill(Color.primary.opacity(0.12)) + Capsule().fill(Fill.track) Capsule() .fill(nodeColor(metric.tone) ?? .accentColor) .frame(width: proxy.size.width * CGFloat(min(max(metric.fraction ?? 0, 0), 1))) diff --git a/Tests/MenubucketAppTests/Fixtures/design-literal-baseline.json b/Tests/MenubucketAppTests/Fixtures/design-literal-baseline.json index 1e2c7fb..55cfd45 100644 --- a/Tests/MenubucketAppTests/Fixtures/design-literal-baseline.json +++ b/Tests/MenubucketAppTests/Fixtures/design-literal-baseline.json @@ -1,9 +1,6 @@ { "Sources\/MenubucketApp\/Builder\/WidgetBuilderView.swift" : 107, - "Sources\/MenubucketApp\/Dock\/DockFolderStack.swift" : 5, - "Sources\/MenubucketApp\/Dock\/DockSettingsPage.swift" : 4, - "Sources\/MenubucketApp\/Dock\/DockView.swift" : 21, - "Sources\/MenubucketApp\/Dock\/DockWidgetGlance.swift" : 3, + "Sources\/MenubucketApp\/Dock\/DockView.swift" : 8, "Sources\/MenubucketApp\/Hub\/ShelfView.swift" : 1, "Sources\/MenubucketApp\/Popover\/RootView.swift" : 2, "Sources\/MenubucketApp\/Renderer\/ViewTreeRenderer.swift" : 1,