Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 55 additions & 12 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ on:
- "release/**"
tags:
- "v*"
- "[0-9]*.[0-9]*.[0-9]*"
workflow_dispatch:

permissions:
Expand All @@ -22,6 +23,7 @@ env:
DEMO_SCHEME: AdaptiveShellDemo
DEMO_DESTINATION: "generic/platform=iOS Simulator"
XCODEGEN_VERSION: "2.45.4"
SWIFTLINT_VERSION: "0.65.0"

jobs:
fast:
Expand All @@ -33,8 +35,18 @@ jobs:
DEVELOPER_DIR: /Applications/Xcode_26.5.app/Contents/Developer
steps:
- uses: actions/checkout@v4
- name: Ensure SwiftLint
run: command -v swiftlint >/dev/null || brew install swiftlint
- name: Install pinned tools
run: |
curl --fail --location --silent --show-error \
"https://github.com/realm/SwiftLint/releases/download/${SWIFTLINT_VERSION}/portable_swiftlint.zip" \
--output "$RUNNER_TEMP/swiftlint.zip"
unzip -q "$RUNNER_TEMP/swiftlint.zip" -d "$RUNNER_TEMP/swiftlint"
echo "$RUNNER_TEMP/swiftlint" >> "$GITHUB_PATH"
- name: Tool versions
run: |
swift --version
xcodebuild -version
test "$(swiftlint version)" = "$SWIFTLINT_VERSION"
- name: Test package
run: swift test
- name: Lint
Expand All @@ -47,6 +59,12 @@ jobs:
-destination "$DEMO_DESTINATION" \
CODE_SIGNING_ALLOWED=NO \
build
- name: Build separate public consumer
run: |
consumer="$RUNNER_TEMP/public-consumer"
python3 Scripts/prepare_consumer.py "$consumer"
swift package --package-path "$consumer" resolve
swift build --package-path "$consumer"

full:
name: Full · ${{ matrix.name }}
Expand All @@ -62,13 +80,29 @@ jobs:
runner: macos-26
developer_dir: /Applications/Xcode_26.5.app/Contents/Developer
runs-on: ${{ matrix.runner }}
timeout-minutes: 30
timeout-minutes: 45
env:
DEVELOPER_DIR: ${{ matrix.developer_dir }}
steps:
- uses: actions/checkout@v4
- name: Ensure SwiftLint
run: command -v swiftlint >/dev/null || brew install swiftlint
- name: Install pinned tools
run: |
curl --fail --location --silent --show-error \
"https://github.com/realm/SwiftLint/releases/download/${SWIFTLINT_VERSION}/portable_swiftlint.zip" \
--output "$RUNNER_TEMP/swiftlint.zip"
unzip -q "$RUNNER_TEMP/swiftlint.zip" -d "$RUNNER_TEMP/swiftlint"
echo "$RUNNER_TEMP/swiftlint" >> "$GITHUB_PATH"
curl --fail --location --silent --show-error \
"https://github.com/yonaskolb/XcodeGen/releases/download/${XCODEGEN_VERSION}/xcodegen.zip" \
--output "$RUNNER_TEMP/xcodegen.zip"
unzip -q "$RUNNER_TEMP/xcodegen.zip" -d "$RUNNER_TEMP/xcodegen"
echo "$RUNNER_TEMP/xcodegen/xcodegen/bin" >> "$GITHUB_PATH"
- name: Tool versions
run: |
swift --version
xcodebuild -version
test "$(swiftlint version)" = "$SWIFTLINT_VERSION"
test "$(xcodegen --version | awk '{print $2}')" = "$XCODEGEN_VERSION"
- name: Test package
run: swift test
- name: Lint
Expand All @@ -81,14 +115,23 @@ jobs:
-destination "$DEMO_DESTINATION" \
CODE_SIGNING_ALLOWED=NO \
build
- name: Install pinned XcodeGen
if: matrix.name == 'current'
- name: Hosted demo and public consumer · iPhone
run: Scripts/run_ui_tests.sh iphone
- name: Hosted demo and public consumer · iPad
run: Scripts/run_ui_tests.sh ipad
- name: Upload native test results
if: always()
uses: actions/upload-artifact@v4
with:
name: native-results-${{ matrix.name }}
path: DerivedData-qa-*/**/results/*.xcresult
if-no-files-found: warn
- name: Build released public consumer
run: |
curl --fail --location --silent --show-error \
"https://github.com/yonaskolb/XcodeGen/releases/download/${XCODEGEN_VERSION}/xcodegen.zip" \
--output "$RUNNER_TEMP/xcodegen.zip"
unzip -q "$RUNNER_TEMP/xcodegen.zip" -d "$RUNNER_TEMP/xcodegen"
echo "$RUNNER_TEMP/xcodegen/xcodegen/bin" >> "$GITHUB_PATH"
consumer="$RUNNER_TEMP/released-consumer"
python3 Scripts/prepare_consumer.py "$consumer" --version 1.0.3
swift package --package-path "$consumer" resolve
swift build --package-path "$consumer"
- name: Reproduce checked-in project
if: matrix.name == 'current'
run: |
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ xcuserdata/
Assets/GIFs/*.mov
Assets/GIFs/*.mp4
Assets/Screenshots/qa-*.png
__pycache__/
3 changes: 3 additions & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
excluded:
- .build
- DerivedData
- DerivedData-showcase
- DerivedData-qa-iphone
- DerivedData-qa-ipad

line_length:
warning: 120
Expand Down
4 changes: 3 additions & 1 deletion Assets/GIFs/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# GIF Assets

Run `Scripts/record_showcase.sh` to rebuild `adaptive-app-shell.gif` from the iPhone simulator.
Run `Scripts/record_showcase.sh` to rebuild `adaptive-app-shell.gif` from an installed iPhone simulator runtime. It checks Xcode/simctl, Python 3 and ffmpeg, creates its own device and removes that device and temporary recording directory on exit. Its trap stops only its own recorder PID. Set `ADAPTIVE_SHELL_RUNTIME=26.5` to choose that installed runtime explicitly.

The source MOV is ignored by Git.

The current GIF was captured on 14 October 2026: iPhone 17 Pro, iOS 26.5, built with Xcode 27.0; 400×870, 20 fps, 243 frames, 12.15 seconds. It records the real demo's `--showcase` sequence across its preset themes and tabs. Temporary recording avoids requiring Simulator to write directly into a protected Documents directory.
Binary file modified Assets/GIFs/adaptive-app-shell.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 4 additions & 2 deletions Assets/Screenshots/README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# Screenshot Assets

These images are direct iOS 26.5 Simulator captures.
These are actual iOS 26.5 Simulator captures of the 1.0.4 candidate, refreshed on 14 October 2026 using Xcode 27.0. The devices were owned, temporary iPhone 17 Pro and iPad Pro 13-inch (M5) simulators. Status-bar time/battery were set with Simulator's own override. App content and pixels were captured directly; no design mockups or generated imagery are included here.

- `iphone-classic.png`
- `iphone-graphite-search.png`
- `ipad-classic.png`
- `ipad-stone.png`
- `ipad-inspector.png`
- `ipad-landscape-inspector.png`

Landscape interaction is covered by hosted tests. The local XCTest landscape attachment has a rotated/cropped framebuffer and is excluded from the current gallery. A new landscape image should be added only after its actual capture has been visually verified.
Binary file modified Assets/Screenshots/ipad-classic.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified Assets/Screenshots/ipad-inspector.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed Assets/Screenshots/ipad-landscape-inspector.png
Binary file not shown.
Binary file modified Assets/Screenshots/ipad-stone.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified Assets/Screenshots/iphone-classic.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified Assets/Screenshots/iphone-graphite-search.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# Changelog

## Unreleased · 1.0.4 candidate

- Reconcile dynamic sections and compact placement, with deterministic first-occurrence duplicate handling and retained app-owned histories.
- Give each regular detail stack its tab identity to preserve the correct visible destination when switching tabs.
- Open Studio projects consistently from Overview, Projects, Search, creation and validated links, regardless of inspector visibility.
- Add session-only creation, shared task/progress data, effective settings and scoped search with retained query/scope.
- Provide an accessible close action inside Studio's inspector for narrow native modal presentations.
- Reject unknown and malformed links explicitly, without fixture fallback or navigation mutations.
- Capture theme transitions from their hosting window and cancel pending transition work on disappearance.
- Add hosted demo tests and a separate public consumer; align version-tag triggers, pinned tools and owned-simulator capture.

No public initializer or typed navigation model changes. See [candidate notes](Docs/ReleaseNotes-1.0.4.md) and [migration rules](Docs/Migration-1.0.4.md).

## 1.0.3

- Added coverage for invalid selection, compact/regular transitions, deep-link path replacement, inspector state, and per-tab navigation.
Expand Down
7 changes: 5 additions & 2 deletions Docs/Accessibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,17 @@ The package and demo are designed around system behavior instead of fixed visual
- Reduce Transparency replaces custom glass actions with opaque controls
- Native navigation keeps standard VoiceOver and keyboard behavior

## Verified states
## Simulator and automated checks

- Standard Dynamic Type on iPhone and iPad
- Accessibility Extra Large on iPhone
- Increase Contrast on iPhone
- Classic, Graphite, and Stone contrast
- Compact tab presentation
- Sidebar with and without inspector
- Accessibility XXXL project/task/create control reachability
- Project labels, task values and empty/recovery messages in the accessibility hierarchy

These are simulator/UI assertions and visual checks. Physical VoiceOver activation, hardware keyboard focus, Increase Contrast, Reduce Transparency/Motion and Stage Manager are recorded separately in the [validation matrix](Validation.md); system controls do not by themselves prove those device gates.

## Consumer checklist

Expand Down
14 changes: 14 additions & 0 deletions Docs/Architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,24 @@ Compact width renders a system `TabView`. Every compact item wraps its feature r

Regular width renders a `NavigationSplitView`. Sidebar buttons use the same destination IDs as compact tabs. The currently selected path is attached to the detail column, and an optional inspector reads contextual feature selection.

The inspector belongs to the split-view container and its toggle belongs to the detail stack, so pushing a route preserves both panel and control. Inspector eligibility still comes from the consumer's predicate.

## Adaptation

Compact and regular presentations never own separate selection or route values. Changing size class swaps presentation while keeping the same state object. If a regular-only collection becomes unavailable in compact width, the shell selects the first valid compact tab and preserves the hidden collection path for later use.

Configuration changes are observed independently of selection and size class. The first occurrence of each section ID and item ID wins. A valid selection survives reordering; an unavailable selection resolves to the first eligible item. With no eligible items, the shell shows an empty state and retains the current selection and all app-owned paths. The regular detail stack has the selected tab's identity, preventing native stack presentation from leaking between tabs.

Typed paths preserve navigation context. They do not preserve arbitrary child `@State`. The demo therefore owns its `StudioStore` and search query/scope above the adaptive presentation. This follows Apple's [navigation state](https://developer.apple.com/documentation/swiftui/understanding-the-navigation-stack) and [Observation ownership](https://developer.apple.com/documentation/swiftui/migrating-from-the-observable-object-protocol-to-the-observable-macro) guidance.

## Demo boundary

`StudioCoordinator` validates project identity and owns one opening rule for Overview, Projects, Search, creation and URLs. Both presentations push the source tab's typed path. Known project links replace only the Projects path. Parsing and identity validation precede mutations; failures show a message and preserve history.

`StudioStore` owns session-only projects, task completion, progress settings and search context. Progress derives from completed tasks. The list, details, task screen and inspector read the same project. Search builds immutable scoped results once per render, and the empty condition uses those visible results.

Theme transitions capture the view's hosting window through a weak reference. The transition task is cancelled and its snapshot cleared when the root disappears; no connected-scene search is used.

## Why the inspector starts closed

An open inspector can force a portrait iPad to hide its sidebar. Defaulting to closed prioritizes primary navigation. Wide layouts can show it with the toolbar action, and consumers can opt into another initial value.
Expand Down
27 changes: 27 additions & 0 deletions Docs/Migration-1.0.4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Configuration and migration

The 1.0.4 candidate is a patch to the reusable shell. Public generic types, initializer signatures, deployment floors and navigation methods remain source compatible. There are no runtime dependencies.

## Selection and identity

Supply stable, globally unique item IDs and unique section IDs. If IDs repeat, the first section with that ID and the first item occurrence in accepted sections win. Later duplicates do not create duplicate native tabs or rows; compact placement also comes from the first item.

Every section change is reconciled, including additions, removals, reordering and `.tab`/`.hidden` changes. A still eligible selection remains selected. Otherwise the first eligible item in configuration order becomes selected. Compact candidates use `.tab`; regular candidates include all accepted items.

An empty configuration displays **No destinations** and retains the previous selection. When items return, a valid retained selection is reused; an invalid one falls back to the first eligible item.

## History ownership

Removing or temporarily hiding an item does not delete its path. Reinserting the same ID restores that history. Permanently retired IDs can be cleaned by the application with `state.paths.removeValue(forKey:)`; do this only when its own domain policy permits it.

The regular stack is keyed by tab ID, so a tab switch presents that tab's actual route array. Compact/regular replacement preserves typed routes, not arbitrary child `@State`. Hoist feature state such as queries, drafts or filters above the shell when it must survive replacement.

## Domain validation

The package cannot decide whether an app-owned route ID still exists. Validate restored or URL-derived routes before applying them, and provide an honest unavailable destination when a record disappears. The Studio example demonstrates this without adding storage or URL semantics to the package.

Theme 1.x IDs continue to identify the documented preset family. Application persistence remains optional and app-owned.

## Inspector presentation

The native inspector can adapt to a modal presentation in a narrow window. Provide a close action inside custom inspector content so it remains reachable when the main toolbar is covered; setting `state.isInspectorPresented = false` dismisses it. The Studio example keeps this action separate from its workspace preference to reopen the inspector for later project navigation.
8 changes: 8 additions & 0 deletions Docs/QualityChecklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- Stable generic destination IDs
- Independent per-destination route paths
- Empty configuration handled without a crash
- Dynamic section reconciliation and deterministic duplicate handling
- Sidebar-only items normalize safely in compact width
- Inspector is opt-in per destination
- No feature or service dependencies in the package
Expand Down Expand Up @@ -35,4 +36,11 @@
- Classic, Graphite, and Stone screenshots
- Accessibility Extra Large screenshot
- SwiftLint
- Hosted demo model, window lifecycle and UI tests
- Separate public-API consumer, released baseline and local candidate
- Plain SemVer and v-prefixed CI triggers
- Two-SDK CI and second-generation XcodeGen equality
- Owned simulator / recorder cleanup in capture and test scripts
- No unintended code comments or debug output

See [Validation](Validation.md) for concrete contracts and remaining physical-device/adoption gates.
15 changes: 15 additions & 0 deletions Docs/ReleaseNotes-1.0.4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# 1.0.4 candidate release notes

Status: review candidate; no 1.0.4 tag or release is implied by this document.

When a selected section disappears, the shell now presents the remaining valid destination without requiring a size-class change. Reordering preserves a valid selection, hidden or removed histories remain app-owned, and regular tab switches present the correct independent detail stack. Duplicate IDs use the first occurrence.

The Studio demo now opens the selected project from Overview, Projects and Search in both presentations. Creating a named project inserts a real session-only record into list/search and opens it. Task changes update shared progress and inspector data. Both workspace settings have visible effects. Scoped search trims the query, ignores case, respects empty-query scopes and displays **No results** when its visible result set is empty.

The inspector includes an accessible close control inside the panel, so it can be dismissed when a narrow native modal presentation covers the main toolbar. Closing does not disable the workspace preference to reopen it for a later project.

Known project links replace one Projects path. Unknown IDs, extra path segments, credentials, queries and fragments produce explicit errors before navigation changes. A missing project is never replaced by the first fixture.

Hosted demo and external-consumer checks supplement package tests. CI retains compatibility/current SDK gates and accepts both plain SemVer and `v`-prefixed tags. Capture creates and cleans its own simulator and recorder. Theme snapshots use the hosting window and cancelled transitions release their image.

The public initializer and typed model remain source compatible: expected package release category **patch**. See [migration rules](Migration-1.0.4.md), [validation evidence](Validation.md) and the [demo walkthrough](../Examples/AdaptiveShellDemo/README.md). Physical-device checks and an independent adoption pilot remain separate release evidence.
Loading
Loading