Skip to content

Add deterministic manifest-backed static assets - #13

Closed
crimson-knight wants to merge 370 commits into
mainfrom
agent/v2-static-asset-contract
Closed

crimson-knight wants to merge 370 commits into
mainfrom
agent/v2-static-asset-contract

Conversation

@crimson-knight

Copy link
Copy Markdown
Member

What changed

  • Adds a deterministic compiler for CSS, JavaScript, images, fonts, and other static files.
  • Fingerprints file contents, rewrites local CSS and JavaScript references, and records MIME type, SRI, byte size, and full digest in a manifest.
  • Verifies built output strictly and prunes only files owned by the previous manifest.
  • Preserves the SafeHTML release line and fixes raw HTML composition in card components.
  • Prepares asset_pipeline 0.37.0.

Why

Amber V2 applications need one documented production asset contract instead of unversioned public files, incomplete font/image handling, and destructive output cleanup.

Validation

  • 44 focused static-asset examples
  • 665 SafeHTML/theme examples
  • Full web suite: 2,518 examples, 0 failures, 66 pending

crimson-knight and others added 30 commits May 27, 2026 18:26
…dgets in todos + draft iOS WidgetKit phase

* brief-10-d-polish.md — combines ListView widget polish (corners /
  drag handle / removal animation / 16pt inset) with 5 catalog
  extensions to todos (Alert / ActionSheet / Sheet / DatePicker /
  Popover). One focused implementer pass.

* phase-11-home-screen-widgets.md — drafted per owner directive.
  iOS home-screen widgets via WidgetKit. Separate Xcode target,
  App Group plumbing, JSON snapshot publish model since Crystal
  runtime can't run in WidgetKit's restricted extension process.
  7 deliverables, ~8-10 hour estimate, queued behind 10D-polish.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…rch + preferences + glass

Owner scope expansion this session. Adds 6 deliverables on top of
10D-polish:
- D1 SearchField on todos list
- D2 Insights screen with ChartView + ActivityRings (seeded mock history)
- D3 Location field + MapView per todo with pin indicator
- D4 Preferences overhaul with Form / Picker / RadioGroup / Slider /
  Stepper for sort + weighting + goals
- D5 GlassBackground material on Sheet + Popover chrome
- D6 Build + screenshots + handtest guide

Sequenced AFTER 10D-polish to keep each implementer pass hand-testable.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…+ brief updates

Codex critique surfaced a foundational gap: briefs shipped widgets
with screenshots but no per-widget usage docs / API references /
catalog status updates / override-path documentation. Future agents
reading the catalog couldn't know HOW to use what we built.

Ships:
* NEW rubric/widget-demonstration-criteria.md — authoritative rule +
  per-widget usage doc template (6 sections) + catalog status fields
  + Voyager-as-canonical-demo policy.
* UPDATED brief-10-d-polish.md — Deliverable C0 + 3 new acceptance
  bullets requiring usage docs for the 7 widgets it exercises.
* UPDATED brief-10-d-extensions.md — Deliverable 0 + 3 new acceptance
  bullets for the 10 widgets it exercises.
* UPDATED phase-11-home-screen-widgets.md — D7 expanded with 4-doc
  requirement (usage doc, component-api entry, catalog status,
  architecture doc).
* Added .claude/worktrees/ to .gitignore (previous attempt accidentally
  tried to add agent worktrees as submodules).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add three new UI::ListView defaults driven by the owner's hand-test feedback:
- content_inset_horizontal : Float64? = 16.0 (Mail-style row inset)
- row_removal_duration_seconds : Float64 = 0.4 (collapse animation)
- shows_drag_handle : Bool = true (line.3.horizontal anchor when on_move set)

Plumbed through the swiftkit_overrides populator and the ListViewOverrides
Swift struct. Facade-side wiring lands in a follow-up commit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- A1: Use SwiftUI .tint(color) (not .buttonStyle) so swipe tiles render
  with full-bleed square-corner chrome. Label uses .titleAndIcon stack.
- A2: Render `line.3.horizontal` SF Symbol on trailing edge of each row
  when moveToken != nil. Tertiary system color, 24pt wide.
- A3: Wrap List in `.animation(.easeInOut(duration: 0.4), value: count)`
  and per-row .transition(.asymmetric(insertion: .opacity, removal:
  .opacity + .scale leading)) so deletes collapse smoothly.
- A4: Apply .listRowInsets(EdgeInsets(top:8, leading:16, bottom:8,
  trailing:16)) by default; nil-passthrough for explicit override.

Swift build green (libAssetPipelineSwiftKit.a archived).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…flags

State carries the transient flags for catalog modal demos:
- pending_delete_todo_id (Alert)
- pending_share_todo_id (ActionSheet)
- pending_editor_todo_id (Sheet)
- show_overflow_menu (Popover)

Controller adds the action methods that toggle these flags and
perform the underlying mutations on confirm/cancel. `:new_todo` and
`:edit_row` now route through the Sheet path; the slug-based fallback
is preserved as `:open_editor_sheet`.

State also gains `sort_by_deadline!` and `clear_completed_todos`
for B5 overflow menu actions.

Screen-side rendering of the new modals lands in the next commit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…et/DatePicker/Popover

- B1: Delete swipe sets pending_delete_todo_id; screen renders UI::Alert
  with Cancel + Delete (destructive). Confirm dispatches :confirm_delete.
- B2: Share swipe sets pending_share_todo_id; screen renders
  UI::ActionSheetWithWebFallback with Copy / Print / Cancel actions.
- B3: :new_todo and :edit_row now set pending_editor_todo_id; screen
  renders UI::Sheet wrapping the editor view (build_editor_content)
  with [:medium, :large] detents + drag indicator. Save dispatches
  :save_sheet which mutates state from FormState and closes the sheet.
- B4: Editor sheet uses UI::DatePicker for the deadline (native iOS
  wheel/calendar chrome). Clear-deadline button covers nilable state.
- B5: Header gains a "•••" overflow button that sets show_overflow_menu;
  screen renders UI::Popover under it with Sort by deadline / Hide
  completed / Clear all completed options.

Crystal --no-codegen check passes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Editing-checked strikethrough writer:
- before: editing && seed_completed → (Bool | Nil)
- after: !editing.nil? && seed_completed → Bool

xcodebuild + Swift build + Crystal cross-compile all green on
iPhone 17 Pro / iOS 26.5 simulator.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Captures the todos screen on launch:
- line.3.horizontal drag handles visible on every row trailing edge
- 16pt row inset (rows align flush with content card)
- Header gains "•••" overflow button between Print + Settings
- Strikethrough + Secondary role on completed todos (Call dentist / Water plants)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
polish-01-delete-alert     — seeds pending_delete_todo_id
polish-02-share-actionsheet — seeds pending_share_todo_id
polish-03-editor-sheet-new  — seeds pending_editor_todo_id=0
polish-04-editor-sheet-edit — seeds pending_editor_todo_id=first.id
polish-05-overflow-popover  — seeds show_overflow_menu=true

Each maps to voyager-todos slug so the modal renders on first frame
via screen.build(ctx)'s pending_* checks.

iOS rebuild + xcodebuild green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The visit(UI::ActionSheet) method was calling apsk_overrides_set_int
on setConfirmToken: and setCancelToken:, which expect NSNumber? on
the Swift side. The raw-int path stuffs the value where an object
pointer is expected, then objc_retain segfaults.

Switched to apsk_overrides_set_uint64_boxed (existing trampoline,
boxes via NSNumber numberWithUnsignedLongLong:). This matches what
sender.set_uint64 would have done if used; visit() was hand-rolled
for these tokens.

Pre-existing bug from Phase 10D-refocus — unblocks B2 ActionSheet
share-flow demo.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
iOS class-init gap leaves Time::Location.local uninitialized →
segfault in find_zoneinfo_file when build_editor_content seeds the
DatePicker. Use Time.utc + Time.parse_utc for the seed path; format
the on_change time via t.to_utc.to_s("%Y-%m-%d"). Date-only picker
mode doesn't care about timezone.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- 03_alert_delete_confirm.png  — UI::Alert (B1)
- 04_actionsheet_share.png     — UI::ActionSheet (B2)
- 05_sheet_editor_new.png      — UI::Sheet new-todo path (B3)
- 06_datepicker_deadline.png   — UI::Sheet edit + UI::DatePicker (B3+B4)
- 07_popover_overflow.png      — UI::Popover overflow menu (B5)

Plus 01_drag_handle_visible.png (A1+A2+A4) from earlier.

DatePicker year display shows "May 27, 3995" — pre-existing
Crystal-Swift epoch-conversion gap, NOT introduced by this brief.
File as backlog: timeIntervalSince1970 vs Crystal Time.to_unix.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
.claude/skills/apple-platform-guide/usage/
  ├── list-view.md      — UI::ListView (with Phase 10D-polish defaults)
  ├── swipe-action.md   — UI::SwipeAction tile contract
  ├── alert.md          — UI::Alert (B1)
  ├── action-sheet.md   — UI::ActionSheet / WithWebFallback (B2)
  ├── sheet.md          — UI::Sheet reactive modal (B3)
  ├── date-picker.md    — UI::DatePicker (B4)
  └── popover.md        — UI::Popover (B5)

Per rubric: six sections each — Default experience / Crystal API /
Behavior contract / Customization knobs / Override path / Evidence.

Every override path documented EITHER as a public knob OR with an
explicit backlog item name. No "future consumer can override" hand-
waving.

Backlog items referenced:
- B-LIST-MACOS-CHROME (drag handle + animation port)
- B-LIST-ANDROID-CALLBACKS
- B-LIST-SWIPE-TINT (per-action tint property)
- B-LIST-SWIPE-LABEL-STYLE
- B-ALERT-CUSTOM-CHROME
- B-ACTIONSHEET-MULTI-ACTION
- B-SHEET-CUSTOM-DETENT-HEIGHT
- B-SHEET-INTERACTIVE-DISMISS-DISABLED
- B-DATEPICKER-STYLE-PROPERTY
- B-DATEPICKER-EPOCH-CONVERSION
- B-POPOVER-ANCHOR-VIEW
- B-POPOVER-COMPACT-ADAPTATION
- B-POPOVER-REACTIVE-PRESENTED

intent-backlog.md additions land in the next commit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
intent-catalog.md — add four new fields to 6 widget entries:
- :list (UI::ListView)
- :swipe_actions (UI::SwipeAction)
- :alert (UI::Alert)
- :confirmation_dialog (UI::ActionSheet via ConfirmationDialogFacade)
- :sheet (UI::Sheet)
- :popover (UI::Popover)
- :compact_date_picker_style (UI::DatePicker)

Each gains:
- demo_status: documented-with-default-experience
- usage_doc: relative path to .claude/skills/apple-platform-guide/usage/*.md
- canonical_example: file:line in Voyager source
- evidence: relative path to phase-10-d-polish screenshot
- override_path_status: one of public-knobs | facade-extension-required
  | no-override-yet-tracked-in-backlog

intent-backlog.md — appended Phase 10D-polish section with 13 new
backlog items (B-LIST-* / B-ALERT-* / B-ACTIONSHEET-* / B-SHEET-* /
B-DATEPICKER-* / B-POPOVER-*) covering every "no override knob today"
surface mentioned in the usage docs. No evasive language remains.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Full walkthrough of:
- Initial launch (A1+A2+A4 — drag handles, square tiles, 16pt inset)
- Delete swipe → Alert (B1)
- Share swipe → ActionSheet (B2)
- Add/Edit → Sheet (B3) with native DatePicker (B4)
- Overflow → Popover (B5)
- Drag-reorder confirmation
- Known issues + their backlog item references
- Capture-scenario reference for automated screenshots

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…pover chrome on iPhone

DatePicker bug root cause: iOS class-init gap leaves
Crystal Time::UNIX_EPOCH (62_135_596_800) uninitialized → Time#to_unix
returns the raw year-1-rooted @seconds instead of Unix epoch. SwiftUI
interprets that as seconds-since-1970 → year 3995.

Fix in UIKit renderer's visit(UI::DatePicker): new safe_time_to_unix
helper detects values > year 2100 in seconds and manually subtracts
the year-1-to-1970 literal offset. Closes B-DATEPICKER-EPOCH-CONVERSION.

Popover compact adaptation: SwiftUI's default on iPhone size class is
.popover → .sheet fallback. PopoverFacade.swift now calls
.presentationCompactAdaptation(.popover) (iOS 16.4+ / macOS 13.3+)
inside an #available guard so iPhone gets actual popover chrome.
Closes B-POPOVER-COMPACT-ADAPTATION.

Both fixes are widget-default level — every consumer of UI::DatePicker
and UI::Popover gets the corrected behavior for free.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…nfirmationDialogFacade

Extends ConfirmationDialogOverrides with parallel actionLabels /
actionStyles / actionTokens arrays. When non-empty, the SwiftUI facade
switches from binary confirm/cancel to a ForEach so iOS ActionSheet
renders Copy / Print / Cancel (or any number of actions) instead of
dropping additional buttons.

Crystal-side visit(UI::ActionSheet) now emits every action in
view.actions to the new arrays; cancel-role buttons land naturally
because SwiftUI's .confirmationDialog pins role:.cancel buttons at the
bottom.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…to-dismiss

Adds interactive_dismiss_disabled : Bool = false to UI::Sheet. When
true, SheetFacade applies .interactiveDismissDisabled() so the user
cannot swipe-down to close the sheet. Programmatic dismissal via
is_presented = false still fires the on_dismiss callback as before.

Use this for editors mid-edit, multi-step wizards, and forms with
unsaved changes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…eels styles

Adds UI::DatePickerStyle enum (Automatic / Compact / Graphical / Wheels)
and UI::DatePicker#style = DatePickerStyle::Automatic.

DatePickerFacade switches on overrides.datePickerStyle and applies
.datePickerStyle(.compact|.graphical|.wheel). Default Automatic skips
the modifier so the platform's HIG default still applies.

Voyager's editor-sheet picker now uses :compact so the deadline shows
as an inline button with a popover calendar instead of a full-page
graphical calendar.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…-action tint + label-style

Adds tint : Symbol? and label_style : Symbol = :auto properties to
UI::SwipeAction. When set:

- tint: overrides the role-derived default (red destructive, blue/green
  default) with one of :blue/:green/:orange/:red/:purple/:yellow/:pink/
  :gray. Populator passes the symbol name to SwiftKit which maps to the
  matching SwiftUI Color.

- label_style: :auto/:icon/:title/:title_and_icon. Default :auto retains
  the old auto-inference; explicit values force the tile contents.

Both SwipeActionRowFacade and ListViewFacade now accept parallel
*LabelStyles arrays. ListView's per-row callback path threads through
SwipeAction#tint / #label_style so the per-row swipe API supports the
same knobs.

Voyager's todo list now demos: Archive orange (leading), Done green,
Share gray, Edit blue, Delete role-derived red.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…iew by test_id

Adds anchor_view_id : String? to UI::Popover. When set, the iOS visit
method looks up the source view's ObjC pointer in a new per-renderer
test_id → UIView registry and threads it to PopoverOverrides.anchorSourceView.

PopoverFacade switches on the presence of the anchor view: when set,
it returns a hidden APSKAnchoredPopoverHost UIView that owns a
UIPopoverPresentationController-backed presentation anchored to the
source view's frame. Adaptive presentation is forced to .none so the
popover stays as a popover on iPhone compact (no auto-fallback to
sheet). Dismissal forwards through dismissToken.

When anchor_view_id is nil, falls back to the existing SwiftUI
.popover(isPresented:) path with .presentationCompactAdaptation(.popover).

Adds apsk_overrides_set_object_ptr C trampoline for setting AnyObject?
properties from raw ObjC pointers.

Voyager's overflow popover now anchors to the "•••" button so the
bubble's arrow points at it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ActionSheet multi-action, Sheet interactive_dismiss_disabled, DatePicker
style enum, SwipeAction tint+label_style, and Popover anchor_view_id
all compile through the SwiftKit facade + Crystal bridge.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…backlog

Per the widget-demonstration-criteria rubric (non-negotiable social
contract), every backlog item closure in this iter ships with:

- Usage doc updated for new knobs:
  - action-sheet.md: multi-action support resolved
  - sheet.md: interactive_dismiss_disabled knob documented
  - date-picker.md: style enum (Automatic/Compact/Graphical/Wheels) documented
  - popover.md: anchor_view_id knob documented
  - swipe-action.md: tint + label_style knobs documented

- intent-catalog.md: coverage_today flipped from missing/partial to full
  for :interactive_dismiss_disabled, :compact_date_picker_style,
  :graphical_date_picker_style, :wheel_date_picker_style.

- intent-backlog.md: B-ACTIONSHEET-MULTI-ACTION, B-SHEET-INTERACTIVE-
  DISMISS-DISABLED, B-DATEPICKER-STYLE-PROPERTY, B-POPOVER-ANCHOR-VIEW,
  B-LIST-SWIPE-TINT, B-LIST-SWIPE-LABEL-STYLE all marked RESOLVED with
  commit SHAs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- 01_home_state.png: app launches, todos render correctly
- 02_actionsheet_multi_action.png: PROOF that ActionSheet now renders
  "Copy to Clipboard" AND "Print This Todo" simultaneously, not just one.
- 03_datepicker_compact.png: PROOF that DatePicker shows as inline
  "May 27, 2026" button (compact style) instead of full-page calendar.
  BONUS: the year-offset bug is also resolved on the compact path
  (was showing 3995 in prior screenshots).
- 04_popover_anchored.png: Popover uses new UIPopoverPresentationController
  path with proper popover chrome. Anchor positioning works but iPhone
  compact size class places the bubble where it has room.
- 05_alert_still_works.png: Alert chrome unaffected by iter-2 changes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
B-ACTIONSHEET-MULTI-ACTION resolution flipped the intent-catalog
override path from "facade-extension-required" to "public-knobs".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…+ Codex review

- merge-readiness-gate.md v2 — addresses 5 Codex BLOCKERs: widget/intent
  scoreboard split, machine-readable catalog-coverage.yml schema, scoreboard
  starts red (V1+V2 filed), usage-doc CI check requirement, harness must be
  executable not design-only. Adds widget-class taxonomy (10 classes,
  per-class evidence requirements), default-experience checklist, iOS-icon
  gate separated from web-fallback route manifest, override-path restrictions,
  source-hash freshness, cross-widget feature stories as merge blockers.

- presentation-lifecycle-contract.md — formal C1-C5 invariants for modal-
  presentation widgets. V1 (ConfirmationDialog auto-close on row tap) + V2
  (Voyager todos header sort button crash) filed as P1 violations.

- interaction-contracts-harness.md — Crystal-driven simctl + cliclick +
  log-stream harness design. Marker convention ([APIC:<widget>:<event>]).
  Phase 12.A/B/C rollout. New accessibility_identifier property on UI::View
  named as precondition.

- handoff/2026-05-28-codex-catalog-coverage-review.md — Codex antagonist
  review of gate v1, full 76-widget coverage table (13 examples / 0 usage
  docs in main checkout), 5-app demo-app ladder proposal.

Strategic path confirmed by owner: Path A — one merge to main when full
revised gate is met across all Tier 1+2 widgets. No partial ships.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…pp ladder

Three artifacts that the gate v2 doc named as preconditions but didn't yet
exist. Together they materialize the gate's scoreboards from a single
machine-checked source of truth.

- catalog-coverage.yml — 76 widgets (17 Tier 1 + 59 Tier 2) and 92 intents
  (1 Class A + 18 Class B + 9 Class C + 64 Class D) initialized in RED
  state. 14 widgets carry pre-populated canonical_example paths from
  Codex's audit (HStack, Label, Spacer, VStack, Button, Checkbox,
  FullScreenCover, Inspector, SecureField, Snackbar, TextField, Toggle,
  ToolbarItemGroup, ToolbarSpacer). V1 (ConfirmationDialog auto-close) and
  V2 (Voyager toolbar crash) filed in blocking_p1_violations with
  not-reproduced-by-spec status.

- scripts/validate_catalog_coverage.cr — lint enforcer. Cross-references
  widgets section against tier-matrix.md, intents section against
  intent-catalog.md, validates widget_class taxonomy (10 classes),
  override_path_status enum, demo_status enum, intent class A/B/C/D,
  owning_widget references, file path existence for canonical_examples
  and usage_docs, and source-hash freshness when set. Lint passes on
  initial manifest (0 errors, 2 expected warnings for V1/V2 specs not
  yet authored).

- demo-app-ladder.md — formalizes Codex's 6-app proposal. Per-app:
  elevator pitch, canonical widget cluster, mirroring app, screens,
  cross-widget feature stories (gate H), reuse plan, complexity
  estimate. Home-screen ladder order: Voyager → Notes → Mailbox →
  Health-log → Photos → Freeform-board, with rationale (most-broadly-
  demonstrative first). Authoring sequence as Phase 12.A (harness) →
  12.B (V1+V2 fixes) → 12.C (Voyager usage docs) → 13-17 (one phase
  per remaining demo app).

Closes Codex BLOCKER 5 (canonical_example and catalog status fields
do not exist) and the precondition behind BLOCKERs 1, 3, 4.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Wires the marker-emission machinery, the simulator-driven spec runner,
and the first C1+C3 spec for UI::ConfirmationDialog (reproduces V1 once
tap coordinates are captured).

- src/ui/native/interaction_contracts.cr — UI::InteractionContracts.emit
  + emit_for(view_id:). No-op unless ENV["APIC_ENABLED"]=="1". Writes
  [APIC:<Widget>:<event>] key=value markers to STDERR for harness
  capture. Smoke-tested standalone.

- swift/.../InteractionContracts.swift — Swift mirror that emits the
  same marker convention via NSLog so SwiftUI facade events land in
  the simulator's unified log alongside Crystal-side STDERR.

- spec/native_ios/ui_interaction/support/simulator_harness.cr — Crystal-
  driven simctl harness. Harness.with_voyager(route:) boots the named
  iPhone 17 Pro / iOS 26.5 simulator, installs the .app bundle, launches
  with SIMCTL_CHILD_APIC_ENABLED=1, subscribes log stream filtered for
  [APIC: markers into a thread-safe buffer, yields a Simulator driver,
  terminates on block exit. Driver: tap_at_point, tap_accessibility_id
  (coordinate-resolved via scenario YAML), wait_for_marker,
  assert_no_marker, assert_marker_order, snapshot_markers, clear_markers.

- spec/native_ios/ui_interaction/scenarios/voyager.yml — 9 placeholder
  tap coordinates for the V1+V2 scenarios. Marked captured: false until
  real coordinates are recorded.

- spec/native_ios/ui_interaction/confirmation_dialog_spec.cr — first
  spec. C1: action-sheet stays open across unrelated Rerender; C3:
  dismissal flows through dismissToken; C2 corollary: no binding-read
  during Rerender. {% if flag?(:ios) %} guarded. Currently pends with
  clear reason ("PLACEHOLDERS — run scripts/capture_tap_coordinates.sh")
  on `crystal spec` until coordinates are captured. Verified pending
  output: 2 examples, 0 failures, 0 errors, 2 pending.

- src/ui.cr — require the new interaction_contracts module.

- merge-readiness-gate.md, interaction-contracts-harness.md, catalog-
  coverage.yml — updated all spec path references from
  spec/ui_interaction/ to spec/native_ios/ui_interaction/ to comply
  with the family_5_partial/spec_platform_directory lint rule. (V1+V2
  reproduction_spec paths in the manifest updated.)

Architectural note: accessibility_identifier was already on UI::View
at src/ui/view.cr:327 and threaded through UIKit/AppKit/Web renderers —
the harness doc's claim that it was a NEW property was incorrect. No
work required on that front.

Lint clean: 501 files, 19 rules, 0 diagnostics.
Catalog validator: 76 widgets, 92 intents, 0 errors, 1 expected warning
(V2 reproduction_spec doesn't exist yet — V2 is not-reproduced-by-spec).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Wires the first concrete marker-emit sites so the V1 reproduction spec
(spec/native_ios/ui_interaction/confirmation_dialog_spec.cr) has live
markers to assert against once tap coordinates are captured.

- BoolStorage gains optional markerWidget + viewID properties. Setter
  emits dismiss-token-fire on true→false transitions; setProgrammatically
  emits present on false→true and platform-dismissed on true→false; init
  is a no-op (markerWidget is set by the facade after construction).
  Non-presentation BoolStorage users (Toggle, Checkbox, etc.) leave both
  properties nil and pay zero overhead.

- ConfirmationDialogFacade now tags its BoolStorage with widget=
  "ConfirmationDialog" + viewID=overrides.accessibilityIdentifier and
  emits the initial present marker when the dialog is constructed
  already-presented.

Emission is gated by InteractionContracts.enabled (ENV APIC_ENABLED=="1")
so production runs are unaffected. swift build: clean.

Next sites for follow-up: SheetFacade, PopoverFacade, AlertFacade,
FullScreenCoverFacade, InspectorFacade — all use the same BoolStorage
pattern so extension is mechanical.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
crimson-knight and others added 27 commits June 4, 2026 01:13
A UI::ScrollView always painted the opaque NSScrollView/NSClipView system
background, so a scroll view placed over other content in a ZStack (e.g. a
full-bleed hero image) hid it behind a black/system fill. Consumers couldn't
build a "scroll content over a fixed hero" screen — surfaced by Happy Coach's
AffirmationReview, which must scroll many affirmation cards over the onboarding
hero photo.

Fix: add nsscrollview_set_draws_background (sets drawsBackground=NO on the
scroll view AND its clip view — each paints independently). The appkit
ScrollView visit calls it whenever the view's background is unset or fully
clear (alpha 0), so a transparent ScrollView lets content behind it show
through; an opaque background still paints as before.

Native AppKit behavior (no web-spec coverage); verified in the consumer: Happy
Coach's AffirmationReview now scrolls its cards over the visible hero photo.
Full suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
UI::IconButton always rendered the platform button chrome — a bezel /
touch-target box around the glyph (visible as a faint box on a small icon over
a flat background). There was no way to get a BARE tappable icon, so consumers
fell back to a plain Image and lost the tap. Surfaced by Happy Coach's
thought-detail icon+text action rows (checkcircle / pluscircle).

Add `UI::IconButton#bordered` (default true = standard chrome, no regression).
Set false → the facade applies `.buttonStyle(.plain)` for a flush, chrome-free
tappable icon. populate_icon_button emits setBordered only when opting out.

Spec: skips setBordered at the default, emits false when bare. Full suite green
(2144 examples); Swift package builds clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A fill_horizontal Button with a background rendered the background as a narrow
pill hugging the label, left-aligned in an otherwise-empty full-width frame —
because the background/clip were applied to the label-sized content and the
maxWidth:.infinity frame was applied to the whole Button AFTERWARD. Surfaced by
Happy Coach's full-width "Submit feedback" CTA (sage box only around the text).

Fix: stretch the LABEL inside the Button (frame maxWidth:.infinity) when
fill_horizontal — so the Button itself is full-width BEFORE its background/clip
apply, and the background fills the row. fill_horizontal leading-aligns its
label; stretched-prominent centers. Removed the now-redundant
frame-the-whole-Button step. Transparent fill_horizontal buttons (e.g. tappable
text-row cards) are unaffected (no background to fill; still fill + leading).

Swift-only composition fix (the populator already emits setFillHorizontal,
spec'd); full suite green (2144), package builds clean. Verified in the consumer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…gned children)

The AppKit renderer's visit(UI::ZStack) pinned EVERY child to all four
superview edges at required priority, ignoring ZStack#alignment. A fixed-size
child was therefore over-constrained (both edges required + width@999): the
solver kept the width but broke an edge, so the child landed at an arbitrary
horizontal position — there was no way to place an aligned, fixed-size overlay
child (slide-in drawer panel, toast, badge, FAB).

Fix (backward-compatible, zero-regression for existing screens):
- Center / Fill / default: KEEP the legacy all-4-edges-required pin
  (objc_pin_child_to_superview_edges). Every existing full-bleed hero is an
  unconstrained Center child that relies on this — untouched.
- Leading/Trailing/Top/Bottom: new objc_pin_child_aligned pins the aligned edge
  + both cross-axis edges at required, adds a REQUIRED containment inequality on
  the opposite edge (never overflow the parent), and a SOFT equality (priority
  499) on the opposite edge. An unconstrained child still fills (499 beats
  content-hugging 250); a fixed-size child keeps its size and aligns (width@999
  beats 499). 499 (not 500) avoids a tie with minimum_width's @500 constraint.
  (Codex-reviewed: containment inequality + 499 priority were its suggestions.)

The 1-D Alignment enum can't express compound alignment, so directional cases
still fill the cross axis — sufficient for the drawer/edge-overlay use cases.

Adds objc_get_frame + objc_layout_now bridge helpers and a native macOS
geometry spec (spec/native_macos/native/zstack_alignment_spec.cr) that builds a
real NSView hierarchy, runs a layout pass, and asserts resolved frames:
Leading/Trailing fixed-width children align (360pt @ x=0 / x=120), unconstrained
children fill (480), Center unconstrained fills (zero-regression). 4/4 green in
isolation; web suite 2144/0/0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… gap)

src/ui/native/objc_collections.cr declared lib LibCollectionBridge (NSString /
NSArray / NSMutableArray / NSDictionary helpers, batch view ops, autorelease
pool, retain/release — ~28 symbols) but the implementing C/Obj-C source was
never in the repo. The whole native macOS spec lane (make test-macos) failed at
link with ~40 undefined symbols, so it was 100% dark — documented as a deferred
blocker in docs/initiative-cross-platform-ui/native-compile-matrix.md (macOS
row, blocker 2: 'locate or stub the C trampolines').

Adds src/ui/native/collection_bridge.m implementing all the trampolines (MRC /
-fno-objc-arc, matching the +0 autoreleased-factory / borrowed-accessor
ownership conventions documented in objc_collections.cr) and wires it into the
Makefile (COL_BRIDGE_OBJ: compile rule, MACOS_LINK_FLAGS, test-macos dep,
clean-bridges).

Result: the native lane LINKS. spec/native_macos/native/ (collections_spec —
which exercises NSString/NSArray/NSMutableArray/NSDictionary/autorelease — plus
the ZStack-alignment geometry spec) now runs 35 examples, 0 failures. Full
spec/native_macos/ binary links clean. (The AX/voyager specs still have separate
pre-existing GUI/TCC blockers — out of scope here.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… geometry

The "reads a window position, size, and composed frame" example reads a Finder
window via the AX API and asserts width/height > 0. In a headless / no-desktop-
session context (CI, ssh, the validation loop) the app reports a window with
0x0 geometry — there is no on-screen frame — so it hard-FAILED
("Expected 0.0 to be GreaterThan 0.0") instead of pending like the sibling
Voyager AX specs, which already detect "no on-screen AXWindow" and pend.

Guard the same way: pend when the window size is nil or 0x0. In a real desktop
session Finder has a real window (width/height > 0) and the assertions run as
before. Verified: the file now reports 3 examples, 0 failures, 1 pending headless
(was 1 failure).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n trees)

App#find / find_all / find_by_id / find_by_id! delegated to Element#find but did
NOT forward max_depth, so an App-level search could not be bounded — it always
used the default depth-10. Against a large foreign app (e.g. Finder) an UNMATCHED
query then walks depth-10 of a very wide tree (millions of cross-process AX
reads), which effectively hangs.

Surfaced by the native_macos lane: ax_identifier_spec searched Finder for a
synthetic (never-matching) id and hung. Plumbed max_depth through the App-level
helpers (default 10, backward compatible) so callers probing a big foreign tree
can cap the walk, and capped the smoke tests at depth 2 (a bogus id is absent at
any depth, so a shallow walk still correctly returns nil / raises).

Verified: ax_identifier_spec now runs 5 examples, 0 failures in 205ms (was hanging).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…g_showcase

AXTestPatterns::SHARD_ROOT used File.expand_path("../..", __DIR__) from
spec/native_macos/support, which lands at spec/ — not the repo root (three
levels up: support -> native_macos -> spec -> root). So the probe computed
SHOWCASE_BIN = <root>/spec/samples/.../hig_showcase, which never exists, while
macos_visual_spec.cr is own guard checks the CORRECT <root>/samples/.../hig_showcase
(present). The guard therefore passed (file exists) and every visual example then
raised File::NotFoundError launching the wrong path — 126 errors.

This was masked until now: the lane used to hang in ax_test (the unbounded find,
fixed in 8ce6427), so it never reached hig_validation.

Fixed to "../../..". Verified: hig_showcase now launches (real RECT_PX window
geometry printed) and the visual examples render + pass instead of erroring.

(Separate, pre-existing residual now unmasked: the visual lane terminates
abnormally partway through — investigating independently.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…arity)

Two related Auto Layout fixes that bring the UIKit renderer to parity with the
already-correct AppKit renderer, fixing layouts that depend on fill + flex space.

1. ZStack children now fill the container via Auto Layout edge constraints.
   The visit set an autoresizing MASK (FlexibleWidth|FlexibleHeight) to fill the
   parent, but every rendered child sets translatesAutoresizingMaskIntoConstraints
   = NO, so the mask was a dead no-op: a child with no explicit size collapsed to
   its intrinsic height/position instead of filling. Symptoms: a content VStack's
   bottom Spacer couldn't expand (a "push to the bottom" tray floated mid-screen),
   and full-bleed backgrounds only covered part of the frame. Now pin each child's
   edges to the ZStack exactly as appkit_renderer does — and honor ZStack#alignment
   (Center/Fill keep the all-edges fill; Leading/Trailing/Top/Bottom pin the aligned
   edge and soft-fill the opposite via objc_pin_child_aligned, so a drawer panel /
   toast / badge sits aligned while unconstrained children still fill).

2. Spacer now drops content-hugging to 1 on BOTH axes. The visit's comment always
   claimed it set a low hugging priority, but the code never did — a plain UIView
   sat at the default 250, tying with ordinary content under UIStackView .fill, so
   slack was distributed arbitrarily (a [Spacer, card, Spacer] column could pin the
   card to the top instead of centering). New objc_set_flex_spacer_priority makes
   the Spacer unambiguously the flexible element.

Verified end-to-end in the happy_coach consumer (iPhone 17 Pro simulator): player
transport tray pins to the bottom, auth CTAs sit at the bottom, the mixer bottom
sheet anchors correctly, the nav drawer left-anchors, and full-bleed heroes fill —
with no regression to macOS (appkit unchanged) and asset_pipeline web specs green
(2144 examples, 0 failures).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t parity)

In a vertical UIStackView, fill_horizontal was a no-op under any non-Fill
alignment: it only lowers content-hugging, which does nothing on the cross
axis when alignment is Center/Leading/Trailing — each arranged subview sizes
to its intrinsic width. Symptom in the happy_coach consumer: the Sound Levels
mixer title (Leading + fill_horizontal inside a Center-aligned sheet column)
rendered centered at text width instead of spanning the column.

Mirror the appkit fix (34fb2b9): pin each fill_horizontal child's width to
the stack at required priority, minus the stack's horizontal layoutMargins so
padded containers keep their gutters. Children with explicit width intent
(min/max) are skipped, as is the whole loop when fluid_width already pinned
every child. Also adds the objc_constrain_equal_width_offset fun binding to
the UIKit LibObjCBridge block (the C implementation in objc_bridge.m is
shared and already used by appkit).

Verified: web suite GREEN (2149 examples, 0 failures, 0 errors); happy_coach
consumer on the iPhone 15 simulator — mixer title now left-aligned full-width,
player/mixer interaction UITests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…I::View

Adds UI::SwipeDirection enum and two gesture APIs to the base View:
  - on_swipe(direction, &block) backed by Hash(SwipeDirection, Proc(Nil))
  - on_long_press property + block convenience setter

UIKit: UISwipeGestureRecognizer per direction (cancelsTouchesInView default
YES); UILongPressGestureRecognizer (0.5 s, cancelsTouchesInView = NO so
child taps still fire). AppKit: NSPanGestureRecognizer with direction-aware
CrystalPanSwipeTarget (classifies dominant translation at .ended, 30 pt
threshold); NSPressGestureRecognizer for long-press. Web: no-op.

ObjC targets are pinned to the host view via objc_setAssociatedObject so
Crystal Procs outlive GC scans for the view's lifetime. Continuous pan/drag
is explicitly out of scope.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e re-render (Phase 12.D)

A UI::Sheet whose presentation_identity persists across a host's
DESTRUCTIVE re-render (build fresh tree → swap root) now carries its
native hosting view + SwiftKit state handle into the fresh tree, so
SwiftUI never observes a dismissal. This closes the Phase 12.D residual
logged in native_view.cr#dismiss_reactive_presentations! ("the visual
transition is a re-present rather than a dismiss ... proper view
reconciliation would eliminate"). It is why the happy_coach consumer
hand-rolled ZStack overlays instead of UI::Sheet and lost native sheet
UX (grabber, detents, swipe-to-dismiss).

The renderer half (UIKit/AppKit try_reuse state-handle adoption +
reuse_from:/retire_prior!) already landed in 2c0648c; this commit adds:

  - NativeView.build_reuse_registry(prior) — the canonical,
    platform-agnostic identity→NativeView registry builder. The
    renderers' reuse_from: entry and both Voyager hosts now build the
    registry ONE way (DRY); the iOS bridge + macOS host are refactored
    onto it.
  - native_view.cr design commentary updated: the re-present residual
    is RESOLVED; the cross-render sweep now only flips ORPHANED handles.
  - Web-lane specs (fake-bridge shape) for the platform-agnostic logic:
    registry construction, identity matching, nil-identity / non-reactive
    / torn-down pruning, last-render-wins, detach_reused! + reused? +
    walk_reactive_views; plus a UI::Sheet state-handle-adoption spec.
  - Native macOS spec (spec/native_macos/native/sheet_reuse_spec.cr)
    driving the REAL AppKit renderer + linked SwiftKit: a continuing
    sheet reuses the prior NativeView (same handle), the fresh UI::Sheet
    adopts the surviving state_handle, the
    [APIC:Sheet:continuing-presentation-reused] marker fires, an orphaned
    sheet is NOT reused, and retire_prior! detaches without
    double-release.

v1 content policy: the reuse path keeps the prior shell AND its prior
content subtree (marker content=prior-shell-retained); re-rendering
changed content into a reused shell needs child reconciliation (Stage 3
of the in-place reconciliation design) and is out of scope for v1.

Host-facing API: UI::UIKit::Renderer.new(reuse_from: prior_root) builds
the registry internally; after swapping the fresh root the host calls
renderer.retire_prior!(fresh_root) (detach reused subtree + orphan
sweep). A plain host like happy_coach's render_current no longer needs
the hand-rolled registry/detach/sweep ceremony.

Verified: crystal-alpha spec spec/web/ GREEN (2181 examples, 0 failures,
0 errors, 66 pending). Native macOS sheet_reuse_spec GREEN (4 examples,
marker observed) + zstack/collections regression GREEN (35) — run
individually since test-macos is pre-broken at link. Convention linter:
no new diagnostics (the 2 remaining are pre-existing Complication views).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add `icon_width` and `icon_height` properties to UI::IconButton. When both
are set, the Swift facade renders the icon into an exact W×H frame using
.fill contentMode + .clipped() (cover-crop), matching Expo's contentFit:"cover"
+ overflow:"hidden" on sized images. Closes the framework gap required for
happy_coach Batch 1 design parity (hamburger 22x18, fixed-box tab icons 24x24).

- src/ui/views/icon_button.cr: icon_width/icon_height Float64? properties
- src/ui/native/swiftkit_overrides.cr: emit setIconWidth/setIconHeight selectors
- IconButtonOverrides.swift: @objc iconWidth/iconHeight NSNumber? properties
- IconButtonFacade.swift: useNonSquare branch — .fill + .clipped() into W×H frame
- group1_overrides_spec.cr: +2 tests for nil-skip and non-square emission

Web suite: 2183 examples, 0 failures. Lint: no new diagnostics.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nside fill (B2.1/B2.5)

THE BUG (B2.1, reported "more than a dozen times"): a row built as
`HStack[Button(fill_horizontal), IconButton]` split the width ~50/50 on iOS
instead of label-hugs-left / icon-hugs-right — "Change Password" wrapped to two
lines while the chevron sat far right with a huge gap; affirmation ✓/+ and
track play-circle rows were spaced the same way.

MECHANISM: UIStackView defaults to .fill distribution, which stretches EVERY
arranged subview that lacks an explicit width and sits at the same content-hugging
priority. A hosted control (UIHostingController-backed Button/IconButton/Image)
reports no intrinsic width to the stack's fitting pass, so the no-fill IconButton
host stretched alongside the fill Button while the fill Button compressed below
its label's intrinsic width and wrapped. NSStackView's default GravityAreas
distribution hugs no-width children automatically (appkit reference); UIKit has
no equivalent.

FIX (framework):
- uikit_renderer visit(HStack): after visiting children, pin each non-fill,
  no-explicit-width child's horizontal hugging + compression resistance to
  Required so it hugs its intrinsic width (recreating NSStackView's GravityAreas
  hug). Spacers and fill_horizontal children keep their flex priority.
- objc_set_horizontal_fill_priority: also RAISE compression resistance to
  Required so a fill child grows to absorb slack but never compresses below its
  intrinsic width (the wrapping "Change Password" root cause).
- IconButton visit: pin the host UIView width to the icon's effective box width
  for a BARE icon (bordered=false), so the chevron/play-circle host hugs its
  24/32pt box instead of stretching. Adds UI::IconButton#effective_icon_box_width.

FIX (B2.5 ButtonFacade.swift): apply the button's padding to the label content
BEFORE the background/clipShape layers (canonical SwiftUI order: content →
padding → background → clip). CommonModifiers layered padding OUTSIDE the
background, so a filled CTA's pill hugged the text and the padding became dead
outer margin (Home "Listen now": "wrapping right to the text with no margin").
Padding is nulled on the shadowed overrides so CommonModifiers does not
double-apply it.

Web suite: 2186 examples, 0 failures (baseline 2183 + 3 new IconButton specs).
Lint: no new diagnostics. The HStack/host-pin constraint calls are exercised only
under -Dios (sim); verified on-device via account_settings AX frame dump (label
hugs intrinsic 219pt single line, chevron right-pinned at 32pt).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
render_document emitted no base reset, so every web render inherited the browser
default 8px body margin and a serif (Times) fallback for any element without its
own font — making web renders deviate from the native platforms for ALL apps.
inject_theme_css now emits html,body{margin:0;padding:0} and a sans-serif base
font. Proven via tag-for-tag DOM diff: a Crystal screen that deviated (11 nodes
on Times) now matches its golden 100%. App typography is still set per-view.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…native/golden

These are renderer-wide correctness fixes (benefit every consuming app),
discovered while pixel-matching a screen against its react-native-web golden
and the AppKit render (which were already correct).

1. fill_horizontal now picks the right CSS for the parent's flex axis.
   It unconditionally emitted `flex: 1 1 0%`, which only grows the MAIN
   axis. In a VStack (column) the main axis is vertical, so a
   fill_horizontal child never stretched to full WIDTH and — with the
   stack's default `align-items: center` — shrink-wrapped and centered
   instead of left-aligning full-width. Now: row parent keeps
   `flex: 1 1 0%`; column parent gets `align-self: stretch`; unknown
   parents keep the historical behavior (no regression). Parent axis is
   read from the parent element's own emitted style, so there is no
   parallel stack to desync. This is why headings/cards rendered centered
   on web but left-aligned on AppKit.

2. An explicit pure-black Label color is no longer swapped for a theme
   token. color_css treats Color(0,0,0) as "unset" and substitutes
   var(--ap-color-text-primary) when a default_token is supplied. But a
   Label whose text_color_role is nil has EXPLICITLY opted into a raw RGBA
   (the text_color= setter nulls the role), so the Label visit now honors
   it verbatim. Previously deliberate black body text vanished to the
   near-white primary on dark themes.

3. font-family is quoted. Unquoted multi-word names (e.g. "Helvetica Neue",
   "Alegreya Sans_medium") are invalid CSS and silently fall back to the
   base sans-serif, changing glyph metrics and text bounds. Quotes are
   valid around single-token names too, so this is lossless for existing
   hyphenated families.

Note: the gitignored lib/ build copy in consuming apps is synced separately;
this commit covers the asset_pipeline source of truth only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…button text-align)

Found while driving the Expo OnboardingAffirmationSelec golden to pixel-parity;
all three are generic renderer correctness fixes that benefit every app.

1. box-sizing: border-box global reset. Padding/border now live INSIDE an
   element's declared width/height, matching the native layout engines and
   react-native-web (the golden). Without it, a view with explicit width +
   padding overflowed by the padding and centered children landed off-center
   (the affirmation column rendered +10px right of the golden everywhere).

2. enforce_touch_target no longer SHRINKS a larger explicit min. The 44px
   floor ran after apply_common_styles and, via CSS last-declaration-wins,
   clobbered an author's explicit min-width/height (a 334px CTA collapsed to
   ~89px content width). Now honors max(floor, explicit). Stale TextArea spec
   (asserted the clobbered 44 over its real 80px min-height) updated.

3. UI::Button gains text_alignment (default Center = the browser <button>
   default, so existing output is byte-identical). Content buttons whose label
   is body text (wrapping affirmation/thought cards) can now read left-aligned,
   matching the golden's left-aligned Pressable text.

Full web spec suite green (2186 examples, 0 failures).
macOS Forms default to .columns, which right-aligns labels into a
separate column that clips at narrow window widths and drops grouped
section chrome entirely. .grouped is the System Settings look and
matches Form's default rendering on iOS.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EkCaEC49RRReKtSTXgcppT
…d view

apply_common_properties already applied a View's background to its CALayer but
ignored corner_radius / border_width / border_color, so a plain VStack/HStack
surface could not be rounded or rimmed without reaching for a shape widget.
Add the missing layer calls, gated on non-default values so views that set
neither are byte-for-byte unchanged. Enables token-driven glass panes, rounded
chat bubbles, and neutral rims for the LocalChat Glasswork skin.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UGEzDn8wq6CvXvCYceoX3L
Adds the shared primitives a scrolling, resizable content surface needs:
- objc_bridge.m: objc_set_vertical_fill_priority (claim a stack's leftover
  vertical space, reflow on resize), nsscrollview_scroll_to_end, and
  nsscrollview_is_at_bottom (stick-to-bottom while streaming, release on scroll-up).
- ScrollView: fill_vertical property, native_handle, and scroll_to_end /
  at_bottom? helpers driving the new C fns after render (no tree rebuild).
- appkit_renderer: honor fill_vertical (skip fixed height, low vertical hugging)
  and store the NSScrollView pointer on the view.

These are exactly the primitives the marketplace catalog scroll + search need.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UGEzDn8wq6CvXvCYceoX3L
Mirrors SearchField#on_submit. Adds on_submit to UI::TextField, a submitToken
NSNumber on TextFieldOverrides, and wires SwiftUI .onSubmit in TextFieldFacade
to fire the string-valued trampoline with the current text on bare Return. The
renderer registers the handler on the string channel and threads the token
through the overrides (no C-selector change). Backward compatible: nil on_submit
keeps the prior behavior (Return does nothing). Swift static lib rebuilt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UGEzDn8wq6CvXvCYceoX3L
…illEqually

Harvested from the happy_coach demo-shell consumer (2026-07-23 dead-tap hunt):
- View#touch_passthrough — informational overlays opt out of hit-testing
  (UIKit userInteractionEnabled=false). A full-screen overlay otherwise
  swallows every touch natively even though clicks pass through on web.
- AsyncImage#preloaded_data + UIKit visit: no async URL loader exists on
  the native path, so pre-fetched bytes decode synchronously (base64 ->
  NSData -> UIImage), aspect-fill, clipped, frame pinned via constraints
  (a UIImageView's intrinsic size is the bitmap's).
- UIKit HStack: honor fill_equally (UIStackViewDistribution.fillEqually) —
  was appkit-only; iOS tab bars crammed cells to intrinsic width.
- Web AsyncImage: size the <img> to its container (width/height 100%) so
  object-fit governs the crop; a raw <img> rendered at natural size.
…taps

A Card rendered via apsk_make_card hosts its content inside a SwiftUI
VStack (APSKHostedChild / UIViewRepresentable) within a UIHostingController.
Any interactive child (Button, IconButton, ...) is itself a
UIHostingController, so it becomes a hosting controller NESTED inside the
Card's. Across that second SwiftUI hosting boundary the child's action
never fires: the child VC parents correctly (Path A) and the button is
hit-testable, but the outer hosting layer's gesture arbitration swallows
the touch. A button INSIDE a Card was therefore dead on iOS.

Route visit(UI::Card) to a raw-UIKit body (rename of the retained
_legacy_card -> render_uikit_card) that adds the content through the normal
visit path. The interactive child is then a direct UIStackView arranged
subview whose own hosting controller's responder chain reaches the root
view controller, so taps fire exactly like a button in a plain HStack. The
old SwiftUI body is retained as _swiftui_card for reference / non-iOS.

Also renders the Card's explicit background/corner/border verbatim via
apply_common_properties instead of layering .regularMaterial over the
requested colour.

Proven by HappyCoach's testAudioPlayerPlayPauseSmoke (play-circle inside a
tracks Card was dead; now fires and opens the player).
# Conflicts:
#	spec/test_js/some_js.js
#	spec/web/components/phase2_verification_spec.cr
#	spec/web/ui/views_spec.cr
@crimson-knight

Copy link
Copy Markdown
Member Author

Superseded by #14, rebuilt from current main so the 0.37.0 release contains only the static-asset compiler and its CI coverage.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant