From 2678ae337251da88ffd9596d1f4b42026257525f Mon Sep 17 00:00:00 2001 From: Tomoki Kobayashi Date: Tue, 11 Aug 2026 08:31:22 +0900 Subject: [PATCH 1/4] Give every row one menu, and let the label win the row MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ⋯ replaces the always-visible ✕ (#21) on every block row, and holds うえへ / したへ / けす plus whatever the row adds — the if block's そうでなければ, which used to be a button of its own on the header, and the else divider's own "remove", so けす means the same thing wherever it is found. Long-press still opens the same menu, so the macOS right-click is unchanged. Deleting had three ways in and this was the redundant one. Moving a row had none a child would find: the menu it lived in only opens on long-press, and nobody presses a block hoping for a hidden menu. A visible ⋯ costs けす one tap and buys the reorder commands their first real affordance — children press what looks pressable. That is also 52pt of the widest row in the app back, which is where #44 started: an if header nested one level deep broke onto two lines on iPad, and a recursive block puts it there by construction. The wrapping itself turned out not to be about width at all. Widening the column changed nothing — measured, the content stayed at 236–564pt, because the canvas column's own ideal outranks it — and the reason is visible in the screenshot: 60pt of empty space sat beside the label, waiting for the menu button. An HStack hands space to its flexible children together, and a wrapping Text loses to a growing Spacer. So the label takes layoutPriority(1), and the chips are pinned with fixedSize, because serving the label first made 0.6 break as "0." over "6" instead. A row now compresses in a fixed order: the spacer, then the label, never a chip. Two levels deep with a long name the label still wraps, and that is accepted — each level of nesting costs 18pt, and no column width wins that race. The column keeps #23's widths. なら / then stays: it reads as the block was written, and the row fits with it. Fixes #44 --- App/Localizable.xcstrings | 10 +++ App/Views/BlockLabelStyle.swift | 10 +++ App/Views/CLAUDE.md | 35 ++++++++- App/Views/ValueEditors.swift | 6 ++ App/Views/WorkspaceView.swift | 123 +++++++++++++++++++++++--------- 5 files changed, 147 insertions(+), 37 deletions(-) diff --git a/App/Localizable.xcstrings b/App/Localizable.xcstrings index d2afd5d..640644d 100644 --- a/App/Localizable.xcstrings +++ b/App/Localizable.xcstrings @@ -514,6 +514,16 @@ } } }, + "More" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メニュー" + } + } + } + }, "Motion" : { "localizations" : { "ja" : { diff --git a/App/Views/BlockLabelStyle.swift b/App/Views/BlockLabelStyle.swift index c6f06fe..38a69aa 100644 --- a/App/Views/BlockLabelStyle.swift +++ b/App/Views/BlockLabelStyle.swift @@ -44,5 +44,15 @@ private struct BlockLabel: View { } configuration.title } + // First claim on the row's width (#44). A row is `label + chips + + // Spacer + menu`, and an `HStack` hands space to its flexible children + // together: `Text` can wrap and `Spacer` can grow, so the label was + // breaking onto two lines — 「も」/「し」 — while 60pt of empty space sat + // beside it waiting for the menu button. The wrapping was never a + // shortage of width, which is why widening the column didn't fix it. + // + // Priority, not `fixedSize`: when the room genuinely isn't there the + // label still wraps, rather than overflowing its block. + .layoutPriority(1) } } diff --git a/App/Views/CLAUDE.md b/App/Views/CLAUDE.md index 93d788d..705a988 100644 --- a/App/Views/CLAUDE.md +++ b/App/Views/CLAUDE.md @@ -71,9 +71,9 @@ Swiping a program should say "まえへ、かず 100、じっこうちゅう" on not stop three times, so a simple row is `.accessibilityElement(children: .combine)`: the kind, its value chips and the running state fuse into one sentence while the chips keep their own actions, which is what leaves editing a -value reachable. The ✕ is `accessibilityHidden` and comes back as a named -action — as a child it would be both an extra stop and the word "けす" tacked -onto every block's sentence. Container headers are deliberately left alone: +value reachable. The ⋯ is `accessibilityHidden` and its entries come back as +named actions — as a child it would be both an extra stop and the word +"メニュー" tacked onto every block's sentence. Container headers are deliberately left alone: combining them would fold in the "Add Here" toggle, and that toggle *is* the accessible alternative to dragging. Order matters twice here. Accessibility actions belong *after* the combine, where they attach to the element it built @@ -87,6 +87,35 @@ and an empty mouth's "Drop Here" reads as something to do when it isn't. Icon-only row buttons wear `touchTarget()`, which is 44pt of hit area on iPadOS and nothing on macOS, where a pointer never needed it. +**One visible control per row, and it is the menu** (#44). The ⋯ replaced the +always-visible ✕ that #21 had put there, and it holds うえへ / したへ / けす plus +whatever the row adds — the if block's そうでなければ, the else divider's own +"remove". Deleting had three ways in and this was the redundant one; moving a +row had *none* a child would find, because the menu it lived in only opens on +long-press. A visible ⋯ costs けす one tap and buys the reorder commands their +first real affordance: children press what looks pressable, and nobody presses a +block hoping for a hidden menu. Long-press still opens the same menu, so the +macOS right-click keeps working. Note the icon is `ellipsis`, not +`ellipsis.circle` — the row already carries two circled glyphs (the chips and +the ⊕), and a third circle read as another control of the same kind. + +**A row compresses in a fixed order: the spacer, then the label, never a chip.** +Getting there took two modifiers, and neither is the one the symptom suggests. +An if header nested one level deep broke its label onto two lines — 「も」/「し」 — +on iPad, and *widening the column did nothing*, because the row was not short of +width: 60pt of empty space sat beside the label waiting for the menu button. An +`HStack` hands space to its flexible children together, and a wrapping `Text` +and a growing `Spacer` are both flexible, so the label lost to the gap. +`BlockLabelStyle` therefore carries `.layoutPriority(1)`. That alone moves the +damage rather than fixing it: with the label served first, `0.6` in the next row +broke as "0." over "6", which is worse — a chip holds a number, a name or a +colour, all atomic. So `WorkspaceChipButtonStyle` pins its label with +`.fixedSize(horizontal: true, vertical: false)`. Priority rather than +`fixedSize` on the *label*, deliberately: a row two levels deep with a long name +(「はこにかける」) genuinely runs out of room, and there the label should still +wrap instead of overflowing its block. Each level of nesting costs 18pt, so no +column width wins that race — the wrap is accepted there. + **Drop model**: a `DropGap` between rows carries `(BodyAddress, index)`, so insertion semantics need no y-coordinate math and every mouth — an if's else included — is a target. Tap-to-add, with the "Add Here" toggle diff --git a/App/Views/ValueEditors.swift b/App/Views/ValueEditors.swift index 9dd86d5..59cb885 100644 --- a/App/Views/ValueEditors.swift +++ b/App/Views/ValueEditors.swift @@ -32,6 +32,12 @@ let variableNameMaxLength = 10 struct WorkspaceChipButtonStyle: ButtonStyle { func makeBody(configuration: Configuration) -> some View { configuration.label + // A chip never wraps (#44). It holds a number, a name or a colour — + // atomic things, where a second line reads as damage: 0.6 broke as + // "0." over "6" once the label beside it was given first claim on + // the row. So the row compresses in a fixed order — the spacer, + // then the label, and the chips never. + .fixedSize(horizontal: true, vertical: false) .foregroundStyle(BlockCategory.ink) .padding(.vertical, 6) .padding(.horizontal, 10) diff --git a/App/Views/WorkspaceView.swift b/App/Views/WorkspaceView.swift index cb3262c..2ccc962 100644 --- a/App/Views/WorkspaceView.swift +++ b/App/Views/WorkspaceView.swift @@ -113,10 +113,10 @@ struct WorkspaceView: View { } /// Somewhere to put a block you have picked up and thought better of — the one -/// thing dragging was missing (#30). Deleting was never hidden: every row -/// carries a ✕ and a context-menu entry. What there was no answer for was -/// "I'm holding this and I don't want it", where the only way out was to put -/// it back exactly where it came from. +/// thing dragging was missing (#30). Deleting was never hidden: every row's +/// menu carries it (a ✕ of its own, when this was written — #44). What there +/// was no answer for was "I'm holding this and I don't want it", where the only +/// way out was to put it back exactly where it came from. /// /// It is always visible rather than appearing mid-drag because SwiftUI has no /// cross-platform signal for "a drag started" — `onDragSessionUpdated` is @@ -315,7 +315,18 @@ struct BlockRowView: View { block: block, childBlocks: body, workspace: workspace, highlightedID: highlightedID, usedVariableNames: usedVariableNames, usedFunctionNames: usedFunctionNames, - elseBlocks: elseBody + elseBlocks: elseBody, + // そうでなければ lives in the row's menu (#44) — the header is + // the widest row in the app, and this is 52pt of it. Offered + // only while there is no else mouth yet, exactly as the button + // it replaced was (#24). + addElseAction: elseBody == nil + ? { + workspace.updateKind( + of: block.id, + to: .ifBlock(condition: condition, body: body, elseBody: [])) + } + : nil ) { Label("If", systemImage: "questionmark.diamond") .labelStyle(BlockLabelStyle()) @@ -327,23 +338,6 @@ struct BlockRowView: View { to: .ifBlock(condition: new, body: body, elseBody: elseBody)) } Text("then") - // Omitted (not just faded) once there's already an else - // mouth, so it stops reserving header width for nothing - // (#24). - if elseBody == nil { - Button("Add Otherwise", systemImage: "arrow.triangle.branch") { - workspace.updateKind( - of: block.id, - to: .ifBlock(condition: condition, body: body, elseBody: [])) - } - .labelStyle(.iconOnly) - .buttonStyle(.borderless) - .controlSize(.large) - .tint(BlockCategory.ink) - .touchTarget() - .accessibilityHint( - "Adds an otherwise mouth that runs when the condition fails") - } } case .defineBlock(let name, let body): ContainerBlockRow( @@ -433,6 +427,9 @@ struct ContainerBlockRow: View { /// instead, which is the whole visual difference between "this runs here" /// and "this is a block being named" (#14). var headerCorners: RowCorners = .containerHeader + /// An extra entry for this header's menu — the if block's そうでなければ, + /// which used to be a button of its own on the header (#44). + var addElseAction: (() -> Void)? = nil @ViewBuilder let header: Header @State private var isDropTargeted = false @@ -458,7 +455,8 @@ struct ContainerBlockRow: View { InsertionTargetButton( address: BodyAddress(containerID: block.id), workspace: workspace) Spacer(minLength: 0) - RowControls(blockID: block.id, workspace: workspace) + RowControls( + blockID: block.id, workspace: workspace, addElseAction: addElseAction) } .blockChrome( block.kind.category.color, corners: headerCorners, @@ -466,6 +464,13 @@ struct ContainerBlockRow: View { ) .draggable(block) .rowContextMenu(blockID: block.id, workspace: workspace) + // The menu holds it, and a menu is reachable — but an action says + // it out loud, the way Move Up / Move Down do. + .accessibilityActions { + if let addElseAction { + Button("Add Otherwise", action: addElseAction) + } + } // Dropping onto the header appends into this container's body. .dropDestination(for: Block.self) { items, _ in guard let dropped = items.first else { return false } @@ -542,22 +547,26 @@ struct ElseDividerRow: View { .labelStyle(BlockLabelStyle()) InsertionTargetButton(address: address, workspace: workspace) Spacer(minLength: 0) - Button("Remove Otherwise", systemImage: "xmark.circle", role: .destructive) { - guard let block = BlockTree.block(withID: blockID, in: workspace.blocks), - case .ifBlock(let condition, let body, _) = block.kind - else { return } - workspace.updateKind( - of: blockID, to: .ifBlock(condition: condition, body: body, elseBody: nil)) + // The same control every other row now carries: one menu, opened + // either by the ⋯ or by long-press. The mouth is removed from + // inside it, so "けす" means the same thing wherever it is found. + Menu { + removeButton + } label: { + Label("More", systemImage: "ellipsis") } + .menuIndicator(.hidden) .labelStyle(.iconOnly) .buttonStyle(.borderless) .controlSize(.large) + .tint(BlockCategory.ink) .touchTarget() } .blockChrome( BlockCategory.control.color, corners: .containerDivider, isDropTargeted: isDropTargeted ) + .contextMenu { removeButton } // Dropping onto the divider appends into the else mouth. .dropDestination(for: Block.self) { items, _ in guard let dropped = items.first else { return false } @@ -565,6 +574,23 @@ struct ElseDividerRow: View { } isTargeted: { isDropTargeted = $0 } + // Reachable without opening the menu, the way every row's Delete is. + .accessibilityAction(named: Text("Remove Otherwise"), removeElse) + } + + private var removeButton: some View { + Button( + "Remove Otherwise", systemImage: "xmark.circle", role: .destructive, action: removeElse) + } + + /// Drops the else mouth, contents included — tree-swap undo makes that + /// safe. + private func removeElse() { + guard let block = BlockTree.block(withID: blockID, in: workspace.blocks), + case .ifBlock(let condition, let body, _) = block.kind + else { return } + workspace.updateKind( + of: blockID, to: .ifBlock(condition: condition, body: body, elseBody: nil)) } } @@ -702,20 +728,49 @@ struct InsertionTargetButton: View { } } -/// Delete for one row — the only row operation that stays always visible -/// (#21); move up/down live in the row's context menu instead (see -/// `rowContextMenu`), with an explicit VoiceOver custom action alongside it. +/// The row's one visible control: the menu, not the ✕ (#44). +/// +/// Everything a row can do lives in one place — うえへ / したへ / けす, plus +/// whatever the row itself adds (the if block's そうでなければ). It replaced the +/// always-visible ✕ #21 had put there, and the trade is deliberate. Deleting +/// had three ways in (the ✕, the long-press menu, the trash can) and this was +/// the redundant one; moving a row had *none* a child would find, because the +/// menu it lived in only opens on long-press. A visible ⋯ costs けす one tap +/// and buys うえへ / したへ their first real affordance — children press what +/// looks pressable, and nobody presses a block hoping for a hidden menu. +/// +/// It also buys the widest row its width back: the if header's そうでなければ +/// button moved in here, which is what stopped the condition chip wrapping at +/// one level of nesting on iPad. struct RowControls: View { let blockID: UUID let workspace: WorkspaceEditor + /// The if block's "add an otherwise mouth", when this row has one. + var addElseAction: (() -> Void)? = nil var body: some View { - Button("Delete", systemImage: "xmark.circle", role: .destructive) { - workspace.delete(blockID) + Menu { + Button("Move Up", systemImage: "chevron.up") { + workspace.move(blockID, by: -1) + } + Button("Move Down", systemImage: "chevron.down") { + workspace.move(blockID, by: 1) + } + if let addElseAction { + Button("Add Otherwise", systemImage: "arrow.triangle.branch", action: addElseAction) + } + Divider() + Button("Delete", systemImage: "xmark.circle", role: .destructive) { + workspace.delete(blockID) + } + } label: { + Label("More", systemImage: "ellipsis") } + .menuIndicator(.hidden) .labelStyle(.iconOnly) .buttonStyle(.borderless) .controlSize(.large) + .tint(BlockCategory.ink) .touchTarget() } } From 1df32b949d3d02c0894ef96b7fe47c2f7c2ee5e3 Mon Sep 17 00:00:00 2001 From: Tomoki Kobayashi Date: Tue, 11 Aug 2026 09:02:38 +0900 Subject: [PATCH 2/4] Put the finger target inside the menu's label MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A `Menu` hit-tests what it was handed to draw, so `touchTarget()` wrapped around the outside of one grows the layout and leaves the tappable area the size of the glyph. Every `Button` on a row wore it that way and was fine — a filled ✕ circle is a large shape by itself — and the ⋯ that replaced them is three dots on a thin band, which is how it came out as a control that misses taps. Painting the target in and measuring it does not catch this: the 44pt square is there either way, and only the touch is different. The glyph also takes imageScale(.large). It is the one control on the row now, so it can afford to be the one thing easiest to hit. --- App/Localizable.xcstrings | 9 +++++---- App/Views/CLAUDE.md | 10 ++++++++++ App/Views/PlatformModifiers.swift | 9 ++++++++- App/Views/WorkspaceView.swift | 19 +++++++++++++++---- 4 files changed, 38 insertions(+), 9 deletions(-) diff --git a/App/Localizable.xcstrings b/App/Localizable.xcstrings index 640644d..58d27dd 100644 --- a/App/Localizable.xcstrings +++ b/App/Localizable.xcstrings @@ -4,22 +4,22 @@ "" : { }, - "🎲" : { + "%@ to %@" : { "localizations" : { "ja" : { "stringUnit" : { "state" : "translated", - "value" : "🎲" + "value" : "%@ 〜 %@" } } } }, - "%@ to %@" : { + "🎲" : { "localizations" : { "ja" : { "stringUnit" : { "state" : "translated", - "value" : "%@ 〜 %@" + "value" : "🎲" } } } @@ -85,6 +85,7 @@ } }, "Adds an otherwise mouth that runs when the condition fails" : { + "extractionState" : "stale", "localizations" : { "ja" : { "stringUnit" : { diff --git a/App/Views/CLAUDE.md b/App/Views/CLAUDE.md index 705a988..9c23a79 100644 --- a/App/Views/CLAUDE.md +++ b/App/Views/CLAUDE.md @@ -99,6 +99,16 @@ macOS right-click keeps working. Note the icon is `ellipsis`, not `ellipsis.circle` — the row already carries two circled glyphs (the chips and the ⊕), and a third circle read as another control of the same kind. +**A `Menu` hit-tests its label, so `touchTarget()` goes inside it.** Wrapped +around the outside — where it sat for every `Button` before this — the 44pt +frame grows the layout and leaves the tappable area the size of the glyph. It +looks identical either way, including with the target painted in for a +screenshot, and only shows up as a control that misses half the taps. The ✕ hid +this for as long as it was there, because a filled circle is a large shape on +its own; three dots on a thin band are not. The ⋯ also takes +`.imageScale(.large)`, bigger than the row's own icons: it is the one control on +the row, so it can afford to be the one thing easiest to hit. + **A row compresses in a fixed order: the spacer, then the label, never a chip.** Getting there took two modifiers, and neither is the one the symptom suggests. An if header nested one level deep broke its label onto two lines — 「も」/「し」 — diff --git a/App/Views/PlatformModifiers.swift b/App/Views/PlatformModifiers.swift index 38f42a1..7bd8fe1 100644 --- a/App/Views/PlatformModifiers.swift +++ b/App/Views/PlatformModifiers.swift @@ -27,9 +27,16 @@ extension View { /// Holds an icon-only control to the 44pt finger minimum on iPadOS. /// A borderless SF Symbol button is only as tappable as the glyph is big — - /// around 24pt at body size — so the ✕ on a block row is a small target on + /// around 24pt at body size — so the ⋯ on a block row is a small target on /// a touch screen even though the row around it is not. /// + /// On a `Menu`, apply this to the *label* rather than to the menu: a menu + /// hit-tests what it was handed to draw, so wrapped around the outside this + /// grows the layout and leaves the tappable area the size of the glyph. The + /// symptom is a control that looks right and misses half the taps, and it + /// only showed up when the row's ✕ became a ⋯ — a filled circle is a large + /// shape by itself, three dots on a thin band are not. + /// /// Only the *hit* area grows, and only where fingers are: the glyph is /// unchanged, and macOS keeps its own (smaller, pointer-sized) metrics /// rather than growing controls a mouse never needed. Rows are already diff --git a/App/Views/WorkspaceView.swift b/App/Views/WorkspaceView.swift index 2ccc962..6ce5e3c 100644 --- a/App/Views/WorkspaceView.swift +++ b/App/Views/WorkspaceView.swift @@ -553,14 +553,16 @@ struct ElseDividerRow: View { Menu { removeButton } label: { + // Inside the label, for the reason `RowControls` gives. Label("More", systemImage: "ellipsis") + .labelStyle(.iconOnly) + .imageScale(.large) + .touchTarget() } .menuIndicator(.hidden) - .labelStyle(.iconOnly) .buttonStyle(.borderless) .controlSize(.large) .tint(BlockCategory.ink) - .touchTarget() } .blockChrome( BlockCategory.control.color, corners: .containerDivider, @@ -764,14 +766,23 @@ struct RowControls: View { workspace.delete(blockID) } } label: { + // The finger target belongs *inside* the label: a `Menu` hit-tests + // what it was handed to draw, so a `frame` wrapped around the menu + // grows the layout and not the tappable area. The ✕ hid this — a + // filled circle is a big shape on its own — and three dots on a + // thin band are not, which is exactly how it came out on iPad. Label("More", systemImage: "ellipsis") + .labelStyle(.iconOnly) + // Bigger than a row's own icons on purpose: this is the one + // control on the row, and a larger glyph is both easier to see + // and a larger shape to hit. + .imageScale(.large) + .touchTarget() } .menuIndicator(.hidden) - .labelStyle(.iconOnly) .buttonStyle(.borderless) .controlSize(.large) .tint(BlockCategory.ink) - .touchTarget() } } From e9c7ede4fe1452398ff6d12db32f6f295e7b08f8 Mon Sep 17 00:00:00 2001 From: Tomoki Kobayashi Date: Tue, 11 Aug 2026 09:05:19 +0900 Subject: [PATCH 3/4] Keep the else mouth's hint, on the menu entry that offers it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ⑂ button carried a VoiceOver hint explaining what an "otherwise" mouth is, and moving the action into the menu left the string with no call site — Xcode marked it stale on the next build. What it explains is still worth saying, so the menu entry carries it now. Xcode goes on marking it stale even so, through a clean build and whether the hint is written as a literal or as a Text; the string is referenced, so the flag is bookkeeping rather than truth, and it is cleared here by hand. --- App/Localizable.xcstrings | 1 - App/Views/WorkspaceView.swift | 5 +++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/App/Localizable.xcstrings b/App/Localizable.xcstrings index 58d27dd..dc1ded9 100644 --- a/App/Localizable.xcstrings +++ b/App/Localizable.xcstrings @@ -85,7 +85,6 @@ } }, "Adds an otherwise mouth that runs when the condition fails" : { - "extractionState" : "stale", "localizations" : { "ja" : { "stringUnit" : { diff --git a/App/Views/WorkspaceView.swift b/App/Views/WorkspaceView.swift index 6ce5e3c..0931d28 100644 --- a/App/Views/WorkspaceView.swift +++ b/App/Views/WorkspaceView.swift @@ -760,6 +760,11 @@ struct RowControls: View { } if let addElseAction { Button("Add Otherwise", systemImage: "arrow.triangle.branch", action: addElseAction) + // The hint the button on the header used to carry: what an + // else mouth *is* still needs saying, and a menu entry is + // where it is now read. + .accessibilityHint( + Text("Adds an otherwise mouth that runs when the condition fails")) } Divider() Button("Delete", systemImage: "xmark.circle", role: .destructive) { From 1fd7b684d5eb74bfed2413ca903d9e127c03126d Mon Sep 17 00:00:00 2001 From: Tomoki Kobayashi Date: Tue, 11 Aug 2026 09:12:43 +0900 Subject: [PATCH 4/4] Leave the menu glyph at its own size MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ⋯ was enlarged along with the touch-target fix, on the theory that a bigger shape would be easier to hit. The 44pt label turned out to be enough on device, so the glyph goes back to the default: at `.large` the dots sat heavier than the words beside them, and a control that is easy to hit does not also need to be the loudest thing on the row. --- App/Views/CLAUDE.md | 8 +++++--- App/Views/WorkspaceView.swift | 5 ----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/App/Views/CLAUDE.md b/App/Views/CLAUDE.md index 9c23a79..6f83c8f 100644 --- a/App/Views/CLAUDE.md +++ b/App/Views/CLAUDE.md @@ -105,9 +105,11 @@ frame grows the layout and leaves the tappable area the size of the glyph. It looks identical either way, including with the target painted in for a screenshot, and only shows up as a control that misses half the taps. The ✕ hid this for as long as it was there, because a filled circle is a large shape on -its own; three dots on a thin band are not. The ⋯ also takes -`.imageScale(.large)`, bigger than the row's own icons: it is the one control on -the row, so it can afford to be the one thing easiest to hit. +its own; three dots on a thin band are not. Note the fix is the target and not +the glyph: enlarging the ⋯ was tried alongside it and taken back out once the +44pt label proved enough on device — at `.large` the dots sat heavier than the +words beside them, and a control that is merely easy to hit does not need to be +the loudest thing on the row. **A row compresses in a fixed order: the spacer, then the label, never a chip.** Getting there took two modifiers, and neither is the one the symptom suggests. diff --git a/App/Views/WorkspaceView.swift b/App/Views/WorkspaceView.swift index 0931d28..88239d3 100644 --- a/App/Views/WorkspaceView.swift +++ b/App/Views/WorkspaceView.swift @@ -556,7 +556,6 @@ struct ElseDividerRow: View { // Inside the label, for the reason `RowControls` gives. Label("More", systemImage: "ellipsis") .labelStyle(.iconOnly) - .imageScale(.large) .touchTarget() } .menuIndicator(.hidden) @@ -778,10 +777,6 @@ struct RowControls: View { // thin band are not, which is exactly how it came out on iPad. Label("More", systemImage: "ellipsis") .labelStyle(.iconOnly) - // Bigger than a row's own icons on purpose: this is the one - // control on the row, and a larger glyph is both easier to see - // and a larger shape to hit. - .imageScale(.large) .touchTarget() } .menuIndicator(.hidden)