opencode-skills-antigravity 1.0.13 → 1.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundled-skills/app-store-changelog/SKILL.md +75 -0
- package/bundled-skills/app-store-changelog/agents/openai.yaml +4 -0
- package/bundled-skills/app-store-changelog/references/release-notes-guidelines.md +34 -0
- package/bundled-skills/app-store-changelog/scripts/collect_release_changes.sh +33 -0
- package/bundled-skills/docs/integrations/jetski-cortex.md +3 -3
- package/bundled-skills/docs/integrations/jetski-gemini-loader/README.md +1 -1
- package/bundled-skills/docs/maintainers/repo-growth-seo.md +14 -14
- package/bundled-skills/docs/maintainers/skills-update-guide.md +1 -1
- package/bundled-skills/docs/sources/sources.md +10 -0
- package/bundled-skills/docs/users/bundles.md +9 -1
- package/bundled-skills/docs/users/claude-code-skills.md +5 -1
- package/bundled-skills/docs/users/codex-cli-skills.md +8 -0
- package/bundled-skills/docs/users/cursor-skills.md +4 -0
- package/bundled-skills/docs/users/faq.md +45 -0
- package/bundled-skills/docs/users/gemini-cli-skills.md +5 -1
- package/bundled-skills/docs/users/getting-started.md +1 -1
- package/bundled-skills/docs/users/kiro-integration.md +1 -1
- package/bundled-skills/docs/users/skills-vs-mcp-tools.md +89 -0
- package/bundled-skills/docs/users/usage.md +14 -4
- package/bundled-skills/docs/users/visual-guide.md +4 -4
- package/bundled-skills/github/SKILL.md +76 -0
- package/bundled-skills/github/agents/openai.yaml +4 -0
- package/bundled-skills/ios-debugger-agent/SKILL.md +59 -0
- package/bundled-skills/ios-debugger-agent/agents/openai.yaml +4 -0
- package/bundled-skills/macos-menubar-tuist-app/SKILL.md +109 -0
- package/bundled-skills/macos-menubar-tuist-app/agents/openai.yaml +4 -0
- package/bundled-skills/macos-spm-app-packaging/SKILL.md +105 -0
- package/bundled-skills/macos-spm-app-packaging/agents/openai.yaml +4 -0
- package/bundled-skills/macos-spm-app-packaging/assets/templates/bootstrap/Package.swift +17 -0
- package/bundled-skills/macos-spm-app-packaging/assets/templates/bootstrap/Sources/MyApp/Resources/.keep +0 -0
- package/bundled-skills/macos-spm-app-packaging/assets/templates/bootstrap/Sources/MyApp/main.swift +11 -0
- package/bundled-skills/macos-spm-app-packaging/assets/templates/bootstrap/version.env +2 -0
- package/bundled-skills/macos-spm-app-packaging/assets/templates/build_icon.sh +49 -0
- package/bundled-skills/macos-spm-app-packaging/assets/templates/compile_and_run.sh +63 -0
- package/bundled-skills/macos-spm-app-packaging/assets/templates/launch.sh +28 -0
- package/bundled-skills/macos-spm-app-packaging/assets/templates/make_appcast.sh +82 -0
- package/bundled-skills/macos-spm-app-packaging/assets/templates/package_app.sh +206 -0
- package/bundled-skills/macos-spm-app-packaging/assets/templates/setup_dev_signing.sh +52 -0
- package/bundled-skills/macos-spm-app-packaging/assets/templates/sign-and-notarize.sh +52 -0
- package/bundled-skills/macos-spm-app-packaging/assets/templates/version.env +2 -0
- package/bundled-skills/macos-spm-app-packaging/references/packaging.md +17 -0
- package/bundled-skills/macos-spm-app-packaging/references/release.md +32 -0
- package/bundled-skills/macos-spm-app-packaging/references/scaffold.md +79 -0
- package/bundled-skills/orchestrate-batch-refactor/SKILL.md +97 -0
- package/bundled-skills/orchestrate-batch-refactor/agents/openai.yaml +4 -0
- package/bundled-skills/orchestrate-batch-refactor/references/agent-prompt-templates.md +53 -0
- package/bundled-skills/orchestrate-batch-refactor/references/work-packet-template.md +31 -0
- package/bundled-skills/project-skill-audit/SKILL.md +190 -0
- package/bundled-skills/project-skill-audit/agents/openai.yaml +4 -0
- package/bundled-skills/react-component-performance/SKILL.md +135 -0
- package/bundled-skills/react-component-performance/agents/openai.yaml +4 -0
- package/bundled-skills/react-component-performance/references/examples.md +88 -0
- package/bundled-skills/simplify-code/SKILL.md +179 -0
- package/bundled-skills/snowflake-development/SKILL.md +5 -0
- package/bundled-skills/swift-concurrency-expert/SKILL.md +113 -0
- package/bundled-skills/swift-concurrency-expert/agents/openai.yaml +4 -0
- package/bundled-skills/swift-concurrency-expert/references/approachable-concurrency.md +63 -0
- package/bundled-skills/swift-concurrency-expert/references/swift-6-2-concurrency.md +272 -0
- package/bundled-skills/swift-concurrency-expert/references/swiftui-concurrency-tour-wwdc.md +33 -0
- package/bundled-skills/swiftui-liquid-glass/SKILL.md +98 -0
- package/bundled-skills/swiftui-liquid-glass/agents/openai.yaml +4 -0
- package/bundled-skills/swiftui-liquid-glass/references/liquid-glass.md +280 -0
- package/bundled-skills/swiftui-performance-audit/SKILL.md +114 -0
- package/bundled-skills/swiftui-performance-audit/agents/openai.yaml +4 -0
- package/bundled-skills/swiftui-performance-audit/references/code-smells.md +150 -0
- package/bundled-skills/swiftui-performance-audit/references/demystify-swiftui-performance-wwdc23.md +46 -0
- package/bundled-skills/swiftui-performance-audit/references/optimizing-swiftui-performance-instruments.md +29 -0
- package/bundled-skills/swiftui-performance-audit/references/profiling-intake.md +44 -0
- package/bundled-skills/swiftui-performance-audit/references/report-template.md +47 -0
- package/bundled-skills/swiftui-performance-audit/references/understanding-hangs-in-your-app.md +33 -0
- package/bundled-skills/swiftui-performance-audit/references/understanding-improving-swiftui-performance.md +52 -0
- package/bundled-skills/swiftui-ui-patterns/SKILL.md +103 -0
- package/bundled-skills/swiftui-ui-patterns/agents/openai.yaml +4 -0
- package/bundled-skills/swiftui-ui-patterns/references/app-wiring.md +201 -0
- package/bundled-skills/swiftui-ui-patterns/references/async-state.md +96 -0
- package/bundled-skills/swiftui-ui-patterns/references/components-index.md +50 -0
- package/bundled-skills/swiftui-ui-patterns/references/controls.md +57 -0
- package/bundled-skills/swiftui-ui-patterns/references/deeplinks.md +66 -0
- package/bundled-skills/swiftui-ui-patterns/references/focus.md +90 -0
- package/bundled-skills/swiftui-ui-patterns/references/form.md +97 -0
- package/bundled-skills/swiftui-ui-patterns/references/grids.md +71 -0
- package/bundled-skills/swiftui-ui-patterns/references/haptics.md +71 -0
- package/bundled-skills/swiftui-ui-patterns/references/input-toolbar.md +51 -0
- package/bundled-skills/swiftui-ui-patterns/references/lightweight-clients.md +93 -0
- package/bundled-skills/swiftui-ui-patterns/references/list.md +86 -0
- package/bundled-skills/swiftui-ui-patterns/references/loading-placeholders.md +38 -0
- package/bundled-skills/swiftui-ui-patterns/references/macos-settings.md +71 -0
- package/bundled-skills/swiftui-ui-patterns/references/matched-transitions.md +59 -0
- package/bundled-skills/swiftui-ui-patterns/references/media.md +73 -0
- package/bundled-skills/swiftui-ui-patterns/references/menu-bar.md +101 -0
- package/bundled-skills/swiftui-ui-patterns/references/navigationstack.md +159 -0
- package/bundled-skills/swiftui-ui-patterns/references/overlay.md +45 -0
- package/bundled-skills/swiftui-ui-patterns/references/performance.md +62 -0
- package/bundled-skills/swiftui-ui-patterns/references/previews.md +48 -0
- package/bundled-skills/swiftui-ui-patterns/references/scroll-reveal.md +133 -0
- package/bundled-skills/swiftui-ui-patterns/references/scrollview.md +87 -0
- package/bundled-skills/swiftui-ui-patterns/references/searchable.md +71 -0
- package/bundled-skills/swiftui-ui-patterns/references/sheets.md +155 -0
- package/bundled-skills/swiftui-ui-patterns/references/split-views.md +72 -0
- package/bundled-skills/swiftui-ui-patterns/references/tabview.md +114 -0
- package/bundled-skills/swiftui-ui-patterns/references/theming.md +71 -0
- package/bundled-skills/swiftui-ui-patterns/references/title-menus.md +93 -0
- package/bundled-skills/swiftui-ui-patterns/references/top-bar.md +49 -0
- package/bundled-skills/swiftui-view-refactor/SKILL.md +210 -0
- package/bundled-skills/swiftui-view-refactor/agents/openai.yaml +4 -0
- package/bundled-skills/swiftui-view-refactor/references/mv-patterns.md +161 -0
- package/package.json +1 -1
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Scroll-reveal detail surfaces
|
|
2
|
+
|
|
3
|
+
## Intent
|
|
4
|
+
|
|
5
|
+
Use this pattern when a detail screen has a primary surface first and secondary content behind it, and you want the user to reveal that secondary layer by scrolling or swiping instead of tapping a separate button.
|
|
6
|
+
|
|
7
|
+
Typical fits:
|
|
8
|
+
|
|
9
|
+
- media detail screens that reveal actions or metadata
|
|
10
|
+
- maps, cards, or canvases that transition into structured detail
|
|
11
|
+
- full-screen viewers with a second "actions" or "insights" page
|
|
12
|
+
|
|
13
|
+
## Core pattern
|
|
14
|
+
|
|
15
|
+
Build the interaction as a paged vertical `ScrollView` with two sections:
|
|
16
|
+
|
|
17
|
+
1. a primary section sized to the viewport
|
|
18
|
+
2. a secondary section below it
|
|
19
|
+
|
|
20
|
+
Derive a normalized `progress` value from the vertical content offset and drive all visual changes from that one value.
|
|
21
|
+
|
|
22
|
+
Avoid treating the reveal as a separate gesture system unless scroll alone cannot express it.
|
|
23
|
+
|
|
24
|
+
## Minimal structure
|
|
25
|
+
|
|
26
|
+
```swift
|
|
27
|
+
private enum DetailSection: Hashable {
|
|
28
|
+
case primary
|
|
29
|
+
case secondary
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
struct DetailSurface: View {
|
|
33
|
+
@State private var revealProgress: CGFloat = 0
|
|
34
|
+
@State private var secondaryHeight: CGFloat = 1
|
|
35
|
+
|
|
36
|
+
var body: some View {
|
|
37
|
+
GeometryReader { geometry in
|
|
38
|
+
ScrollViewReader { proxy in
|
|
39
|
+
ScrollView(.vertical, showsIndicators: false) {
|
|
40
|
+
VStack(spacing: 0) {
|
|
41
|
+
PrimaryContent(progress: revealProgress)
|
|
42
|
+
.frame(height: geometry.size.height)
|
|
43
|
+
.id(DetailSection.primary)
|
|
44
|
+
|
|
45
|
+
SecondaryContent(progress: revealProgress)
|
|
46
|
+
.id(DetailSection.secondary)
|
|
47
|
+
.onGeometryChange(for: CGFloat.self) { geo in
|
|
48
|
+
geo.size.height
|
|
49
|
+
} action: { newHeight in
|
|
50
|
+
secondaryHeight = max(newHeight, 1)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
.scrollTargetLayout()
|
|
54
|
+
}
|
|
55
|
+
.scrollTargetBehavior(.paging)
|
|
56
|
+
.onScrollGeometryChange(for: CGFloat.self, of: { scroll in
|
|
57
|
+
scroll.contentOffset.y + scroll.contentInsets.top
|
|
58
|
+
}) { _, offset in
|
|
59
|
+
revealProgress = (offset / secondaryHeight).clamped(to: 0...1)
|
|
60
|
+
}
|
|
61
|
+
.safeAreaInset(edge: .bottom) {
|
|
62
|
+
ChevronAffordance(progress: revealProgress) {
|
|
63
|
+
withAnimation(.smooth) {
|
|
64
|
+
let target: DetailSection = revealProgress < 0.5 ? .secondary : .primary
|
|
65
|
+
proxy.scrollTo(target, anchor: .top)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Design choices to keep
|
|
76
|
+
|
|
77
|
+
- Make the primary section exactly viewport-sized when the interaction should feel like paging between states.
|
|
78
|
+
- Compute `progress` from real scroll offset, not from duplicated booleans like `isExpanded`, `isShowingSecondary`, and `isSnapped`.
|
|
79
|
+
- Use `progress` to drive `offset`, `opacity`, `blur`, `scaleEffect`, and toolbar state so the whole surface stays synchronized.
|
|
80
|
+
- Use `ScrollViewReader` for programmatic snapping from taps on the primary content or chevron affordances.
|
|
81
|
+
- Use `onScrollTargetVisibilityChange` when you need a settled section state for haptics, tooltip dismissal, analytics, or accessibility announcements.
|
|
82
|
+
|
|
83
|
+
## Morphing a shared control
|
|
84
|
+
|
|
85
|
+
If a control appears to move from the primary surface into the secondary content, do not render two fully visible copies.
|
|
86
|
+
|
|
87
|
+
Instead:
|
|
88
|
+
|
|
89
|
+
- expose a source anchor in the primary area
|
|
90
|
+
- expose a destination anchor in the secondary area
|
|
91
|
+
- render one overlay that interpolates position and size using `progress`
|
|
92
|
+
|
|
93
|
+
```swift
|
|
94
|
+
Color.clear
|
|
95
|
+
.anchorPreference(key: ControlAnchorKey.self, value: .bounds) { anchor in
|
|
96
|
+
["source": anchor]
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
Color.clear
|
|
100
|
+
.anchorPreference(key: ControlAnchorKey.self, value: .bounds) { anchor in
|
|
101
|
+
["destination": anchor]
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.overlayPreferenceValue(ControlAnchorKey.self) { anchors in
|
|
105
|
+
MorphingControlOverlay(anchors: anchors, progress: revealProgress)
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
This keeps the motion coherent and avoids duplicate-hit-target bugs.
|
|
110
|
+
|
|
111
|
+
## Haptics and affordances
|
|
112
|
+
|
|
113
|
+
- Use light threshold haptics when the reveal begins and stronger haptics near the committed state.
|
|
114
|
+
- Keep a visible affordance like a chevron or pill while `progress` is near zero.
|
|
115
|
+
- Flip, fade, or blur the affordance as the secondary section becomes active.
|
|
116
|
+
|
|
117
|
+
## Interaction guards
|
|
118
|
+
|
|
119
|
+
- Disable vertical scrolling when a conflicting mode is active, such as pinch-to-zoom, crop, or full-screen media manipulation.
|
|
120
|
+
- Disable hit testing on overlays that should disappear once the secondary content is revealed.
|
|
121
|
+
- Avoid same-axis nested scroll views unless the inner view is effectively static or disabled during the reveal.
|
|
122
|
+
|
|
123
|
+
## Pitfalls
|
|
124
|
+
|
|
125
|
+
- Do not hard-code the progress divisor. Measure the secondary section height or another real reveal distance.
|
|
126
|
+
- Do not mix multiple animation sources for the same property. If `progress` drives it, keep other animations off that property.
|
|
127
|
+
- Do not store derived state like `isSecondaryVisible` unless another API requires it. Prefer deriving it from `progress` or visible scroll targets.
|
|
128
|
+
- Beware of layout feedback loops when measuring heights. Clamp zero values and update only when the measured height actually changes.
|
|
129
|
+
|
|
130
|
+
## Concrete example
|
|
131
|
+
|
|
132
|
+
- Pool iOS tile detail reveal: `/Users/dimillian/Documents/Dev/Pool/pool-ios/Pool/Sources/Features/Tile/Detail/TileDetailView.swift`
|
|
133
|
+
- Secondary content anchor example: `/Users/dimillian/Documents/Dev/Pool/pool-ios/Pool/Sources/Features/Tile/Detail/TileDetailIntentListView.swift`
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# ScrollView and Lazy stacks
|
|
2
|
+
|
|
3
|
+
## Intent
|
|
4
|
+
|
|
5
|
+
Use `ScrollView` with `LazyVStack`, `LazyHStack`, or `LazyVGrid` when you need custom layout, mixed content, or horizontal/ grid-based scrolling.
|
|
6
|
+
|
|
7
|
+
## Core patterns
|
|
8
|
+
|
|
9
|
+
- Prefer `ScrollView` + `LazyVStack` for chat-like or custom feed layouts.
|
|
10
|
+
- Use `ScrollView(.horizontal)` + `LazyHStack` for chips, tags, avatars, and media strips.
|
|
11
|
+
- Use `LazyVGrid` for icon/media grids; prefer adaptive columns when possible.
|
|
12
|
+
- Use `ScrollViewReader` for scroll-to-top/bottom and anchor-based jumps.
|
|
13
|
+
- Use `safeAreaInset(edge:)` for input bars that should stick above the keyboard.
|
|
14
|
+
|
|
15
|
+
## Example: vertical custom feed
|
|
16
|
+
|
|
17
|
+
```swift
|
|
18
|
+
@MainActor
|
|
19
|
+
struct ConversationView: View {
|
|
20
|
+
private enum Constants { static let bottomAnchor = "bottom" }
|
|
21
|
+
@State private var scrollProxy: ScrollViewProxy?
|
|
22
|
+
|
|
23
|
+
var body: some View {
|
|
24
|
+
ScrollViewReader { proxy in
|
|
25
|
+
ScrollView {
|
|
26
|
+
LazyVStack {
|
|
27
|
+
ForEach(messages) { message in
|
|
28
|
+
MessageRow(message: message)
|
|
29
|
+
.id(message.id)
|
|
30
|
+
}
|
|
31
|
+
Color.clear.frame(height: 1).id(Constants.bottomAnchor)
|
|
32
|
+
}
|
|
33
|
+
.padding(.horizontal, .layoutPadding)
|
|
34
|
+
}
|
|
35
|
+
.safeAreaInset(edge: .bottom) {
|
|
36
|
+
MessageInputBar()
|
|
37
|
+
}
|
|
38
|
+
.onAppear {
|
|
39
|
+
scrollProxy = proxy
|
|
40
|
+
withAnimation {
|
|
41
|
+
proxy.scrollTo(Constants.bottomAnchor, anchor: .bottom)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Example: horizontal chips
|
|
50
|
+
|
|
51
|
+
```swift
|
|
52
|
+
ScrollView(.horizontal, showsIndicators: false) {
|
|
53
|
+
LazyHStack(spacing: 8) {
|
|
54
|
+
ForEach(chips) { chip in
|
|
55
|
+
ChipView(chip: chip)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Example: adaptive grid
|
|
62
|
+
|
|
63
|
+
```swift
|
|
64
|
+
let columns = [GridItem(.adaptive(minimum: 120))]
|
|
65
|
+
|
|
66
|
+
ScrollView {
|
|
67
|
+
LazyVGrid(columns: columns, spacing: 8) {
|
|
68
|
+
ForEach(items) { item in
|
|
69
|
+
GridItemView(item: item)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
.padding(8)
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Design choices to keep
|
|
77
|
+
|
|
78
|
+
- Use `Lazy*` stacks when item counts are large or unknown.
|
|
79
|
+
- Use non-lazy stacks for small, fixed-size content to avoid lazy overhead.
|
|
80
|
+
- Keep IDs stable when using `ScrollViewReader`.
|
|
81
|
+
- Prefer explicit animations (`withAnimation`) when scrolling to an ID.
|
|
82
|
+
|
|
83
|
+
## Pitfalls
|
|
84
|
+
|
|
85
|
+
- Avoid nesting scroll views of the same axis; it causes gesture conflicts.
|
|
86
|
+
- Don’t combine `List` and `ScrollView` in the same hierarchy without a clear reason.
|
|
87
|
+
- Overuse of `LazyVStack` for tiny content can add unnecessary complexity.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Searchable
|
|
2
|
+
|
|
3
|
+
## Intent
|
|
4
|
+
|
|
5
|
+
Use `searchable` to add native search UI with optional scopes and async results.
|
|
6
|
+
|
|
7
|
+
## Core patterns
|
|
8
|
+
|
|
9
|
+
- Bind `searchable(text:)` to local state.
|
|
10
|
+
- Use `.searchScopes` for multiple search modes.
|
|
11
|
+
- Use `.task(id: searchQuery)` or debounced tasks to avoid overfetching.
|
|
12
|
+
- Show placeholders or progress states while results load.
|
|
13
|
+
|
|
14
|
+
## Example: searchable with scopes
|
|
15
|
+
|
|
16
|
+
```swift
|
|
17
|
+
@MainActor
|
|
18
|
+
struct ExploreView: View {
|
|
19
|
+
@State private var searchQuery = ""
|
|
20
|
+
@State private var searchScope: SearchScope = .all
|
|
21
|
+
@State private var isSearching = false
|
|
22
|
+
@State private var results: [SearchResult] = []
|
|
23
|
+
|
|
24
|
+
var body: some View {
|
|
25
|
+
List {
|
|
26
|
+
if isSearching {
|
|
27
|
+
ProgressView()
|
|
28
|
+
} else {
|
|
29
|
+
ForEach(results) { result in
|
|
30
|
+
SearchRow(result: result)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
.searchable(
|
|
35
|
+
text: $searchQuery,
|
|
36
|
+
placement: .navigationBarDrawer(displayMode: .always),
|
|
37
|
+
prompt: Text("Search")
|
|
38
|
+
)
|
|
39
|
+
.searchScopes($searchScope) {
|
|
40
|
+
ForEach(SearchScope.allCases, id: \.self) { scope in
|
|
41
|
+
Text(scope.title)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
.task(id: searchQuery) {
|
|
45
|
+
await runSearch()
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private func runSearch() async {
|
|
50
|
+
guard !searchQuery.isEmpty else {
|
|
51
|
+
results = []
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
isSearching = true
|
|
55
|
+
defer { isSearching = false }
|
|
56
|
+
try? await Task.sleep(for: .milliseconds(250))
|
|
57
|
+
results = await fetchResults(query: searchQuery, scope: searchScope)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Design choices to keep
|
|
63
|
+
|
|
64
|
+
- Show a placeholder when search is empty or has no results.
|
|
65
|
+
- Debounce input to avoid spamming the network.
|
|
66
|
+
- Keep search state local to the view.
|
|
67
|
+
|
|
68
|
+
## Pitfalls
|
|
69
|
+
|
|
70
|
+
- Avoid running searches for empty strings.
|
|
71
|
+
- Don’t block the main thread during fetch.
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# Sheets
|
|
2
|
+
|
|
3
|
+
## Intent
|
|
4
|
+
|
|
5
|
+
Use a centralized sheet routing pattern so any view can present modals without prop-drilling. This keeps sheet state in one place and scales as the app grows.
|
|
6
|
+
|
|
7
|
+
## Core architecture
|
|
8
|
+
|
|
9
|
+
- Define a `SheetDestination` enum that describes every modal and is `Identifiable`.
|
|
10
|
+
- Store the current sheet in a router object (`presentedSheet: SheetDestination?`).
|
|
11
|
+
- Create a view modifier like `withSheetDestinations(...)` that maps the enum to concrete sheet views.
|
|
12
|
+
- Inject the router into the environment so child views can set `presentedSheet` directly.
|
|
13
|
+
|
|
14
|
+
## Example: item-driven local sheet
|
|
15
|
+
|
|
16
|
+
Use this when sheet state is local to one screen and does not need centralized routing.
|
|
17
|
+
|
|
18
|
+
```swift
|
|
19
|
+
@State private var selectedItem: Item?
|
|
20
|
+
|
|
21
|
+
.sheet(item: $selectedItem) { item in
|
|
22
|
+
EditItemSheet(item: item)
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Example: SheetDestination enum
|
|
27
|
+
|
|
28
|
+
```swift
|
|
29
|
+
enum SheetDestination: Identifiable, Hashable {
|
|
30
|
+
case composer
|
|
31
|
+
case editProfile
|
|
32
|
+
case settings
|
|
33
|
+
case report(itemID: String)
|
|
34
|
+
|
|
35
|
+
var id: String {
|
|
36
|
+
switch self {
|
|
37
|
+
case .composer, .editProfile:
|
|
38
|
+
// Use the same id to ensure only one editor-like sheet is active at a time.
|
|
39
|
+
return "editor"
|
|
40
|
+
case .settings:
|
|
41
|
+
return "settings"
|
|
42
|
+
case .report:
|
|
43
|
+
return "report"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Example: withSheetDestinations modifier
|
|
50
|
+
|
|
51
|
+
```swift
|
|
52
|
+
extension View {
|
|
53
|
+
func withSheetDestinations(
|
|
54
|
+
sheet: Binding<SheetDestination?>
|
|
55
|
+
) -> some View {
|
|
56
|
+
sheet(item: sheet) { destination in
|
|
57
|
+
Group {
|
|
58
|
+
switch destination {
|
|
59
|
+
case .composer:
|
|
60
|
+
ComposerView()
|
|
61
|
+
case .editProfile:
|
|
62
|
+
EditProfileView()
|
|
63
|
+
case .settings:
|
|
64
|
+
SettingsView()
|
|
65
|
+
case .report(let itemID):
|
|
66
|
+
ReportView(itemID: itemID)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Example: presenting from a child view
|
|
75
|
+
|
|
76
|
+
```swift
|
|
77
|
+
struct StatusRow: View {
|
|
78
|
+
@Environment(RouterPath.self) private var router
|
|
79
|
+
|
|
80
|
+
var body: some View {
|
|
81
|
+
Button("Report") {
|
|
82
|
+
router.presentedSheet = .report(itemID: "123")
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Required wiring
|
|
89
|
+
|
|
90
|
+
For the child view to work, a parent view must:
|
|
91
|
+
- own the router instance,
|
|
92
|
+
- attach `withSheetDestinations(sheet: $router.presentedSheet)` (or an equivalent `sheet(item:)` handler), and
|
|
93
|
+
- inject it with `.environment(router)` after the sheet modifier so the modal content inherits it.
|
|
94
|
+
|
|
95
|
+
This makes the child assignment to `router.presentedSheet` drive presentation at the root.
|
|
96
|
+
|
|
97
|
+
## Example: sheets that need their own navigation
|
|
98
|
+
|
|
99
|
+
Wrap sheet content in a `NavigationStack` so it can push within the modal.
|
|
100
|
+
|
|
101
|
+
```swift
|
|
102
|
+
struct NavigationSheet<Content: View>: View {
|
|
103
|
+
var content: () -> Content
|
|
104
|
+
|
|
105
|
+
var body: some View {
|
|
106
|
+
NavigationStack {
|
|
107
|
+
content()
|
|
108
|
+
.toolbar { CloseToolbarItem() }
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Example: sheet owns its actions
|
|
115
|
+
|
|
116
|
+
Keep dismissal and confirmation logic inside the sheet when the actions belong to the modal itself.
|
|
117
|
+
|
|
118
|
+
```swift
|
|
119
|
+
struct EditItemSheet: View {
|
|
120
|
+
@Environment(\.dismiss) private var dismiss
|
|
121
|
+
@Environment(Store.self) private var store
|
|
122
|
+
|
|
123
|
+
let item: Item
|
|
124
|
+
@State private var isSaving = false
|
|
125
|
+
|
|
126
|
+
var body: some View {
|
|
127
|
+
VStack {
|
|
128
|
+
Button(isSaving ? "Saving..." : "Save") {
|
|
129
|
+
Task { await save() }
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private func save() async {
|
|
135
|
+
isSaving = true
|
|
136
|
+
await store.save(item)
|
|
137
|
+
dismiss()
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Design choices to keep
|
|
143
|
+
|
|
144
|
+
- Centralize sheet routing so features can present modals without wiring bindings through many layers.
|
|
145
|
+
- Use `sheet(item:)` to guarantee a single sheet is active and to drive presentation from the enum.
|
|
146
|
+
- Group related sheets under the same `id` when they are mutually exclusive (e.g., editor flows).
|
|
147
|
+
- Keep sheet views lightweight and composed from smaller views; avoid large monoliths.
|
|
148
|
+
- Let sheets own their actions and call `dismiss()` internally instead of forwarding `onCancel` or `onConfirm` closures through many layers.
|
|
149
|
+
|
|
150
|
+
## Pitfalls
|
|
151
|
+
|
|
152
|
+
- Avoid mixing `sheet(isPresented:)` and `sheet(item:)` for the same concern; prefer a single enum.
|
|
153
|
+
- Avoid `if let` inside a sheet body when the presentation state already carries the selected model; prefer `sheet(item:)`.
|
|
154
|
+
- Do not store heavy state inside `SheetDestination`; pass lightweight identifiers or models.
|
|
155
|
+
- If multiple sheets can appear from the same screen, give them distinct `id` values.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Split views and columns
|
|
2
|
+
|
|
3
|
+
## Intent
|
|
4
|
+
|
|
5
|
+
Provide a lightweight, customizable multi-column layout for iPad/macOS without relying on `NavigationSplitView`.
|
|
6
|
+
|
|
7
|
+
## Custom split column pattern (manual HStack)
|
|
8
|
+
|
|
9
|
+
Use this when you want full control over column sizing, behavior, and environment tweaks.
|
|
10
|
+
|
|
11
|
+
```swift
|
|
12
|
+
@MainActor
|
|
13
|
+
struct AppView: View {
|
|
14
|
+
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
|
15
|
+
@AppStorage("showSecondaryColumn") private var showSecondaryColumn = true
|
|
16
|
+
|
|
17
|
+
var body: some View {
|
|
18
|
+
HStack(spacing: 0) {
|
|
19
|
+
primaryColumn
|
|
20
|
+
if shouldShowSecondaryColumn {
|
|
21
|
+
Divider().edgesIgnoringSafeArea(.all)
|
|
22
|
+
secondaryColumn
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private var shouldShowSecondaryColumn: Bool {
|
|
28
|
+
horizontalSizeClass == .regular
|
|
29
|
+
&& showSecondaryColumn
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private var primaryColumn: some View {
|
|
33
|
+
TabView { /* tabs */ }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private var secondaryColumn: some View {
|
|
37
|
+
NotificationsTab()
|
|
38
|
+
.environment(\.isSecondaryColumn, true)
|
|
39
|
+
.frame(maxWidth: .secondaryColumnWidth)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Notes on the custom approach
|
|
45
|
+
|
|
46
|
+
- Use a shared preference or setting to toggle the secondary column.
|
|
47
|
+
- Inject an environment flag (e.g., `isSecondaryColumn`) so child views can adapt behavior.
|
|
48
|
+
- Prefer a fixed or capped width for the secondary column to avoid layout thrash.
|
|
49
|
+
|
|
50
|
+
## Alternative: NavigationSplitView
|
|
51
|
+
|
|
52
|
+
`NavigationSplitView` can handle sidebar + detail + supplementary columns for you, but is harder to customize in cases like:\n- a dedicated notification column independent of selection,\n- custom sizing, or\n- different toolbar behaviors per column.
|
|
53
|
+
|
|
54
|
+
```swift
|
|
55
|
+
@MainActor
|
|
56
|
+
struct AppView: View {
|
|
57
|
+
var body: some View {
|
|
58
|
+
NavigationSplitView {
|
|
59
|
+
SidebarView()
|
|
60
|
+
} content: {
|
|
61
|
+
MainContentView()
|
|
62
|
+
} detail: {
|
|
63
|
+
NotificationsView()
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## When to choose which
|
|
70
|
+
|
|
71
|
+
- Use the manual HStack split when you need full control or a non-standard secondary column.
|
|
72
|
+
- Use `NavigationSplitView` when you want a standard system layout with minimal customization.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# TabView
|
|
2
|
+
|
|
3
|
+
## Intent
|
|
4
|
+
|
|
5
|
+
Use this pattern for a scalable, multi-platform tab architecture with:
|
|
6
|
+
- a single source of truth for tab identity and content,
|
|
7
|
+
- platform-specific tab sets and sidebar sections,
|
|
8
|
+
- dynamic tabs sourced from data,
|
|
9
|
+
- an interception hook for special tabs (e.g., compose).
|
|
10
|
+
|
|
11
|
+
## Core architecture
|
|
12
|
+
|
|
13
|
+
- `AppTab` enum defines identity, labels, icons, and content builder.
|
|
14
|
+
- `SidebarSections` enum groups tabs for sidebar sections.
|
|
15
|
+
- `AppView` owns the `TabView` and selection binding, and routes tab changes through `updateTab`.
|
|
16
|
+
|
|
17
|
+
## Example: custom binding with side effects
|
|
18
|
+
|
|
19
|
+
Use this when tab selection needs side effects, like intercepting a special tab to perform an action instead of changing selection.
|
|
20
|
+
|
|
21
|
+
```swift
|
|
22
|
+
@MainActor
|
|
23
|
+
struct AppView: View {
|
|
24
|
+
@Binding var selectedTab: AppTab
|
|
25
|
+
|
|
26
|
+
var body: some View {
|
|
27
|
+
TabView(selection: .init(
|
|
28
|
+
get: { selectedTab },
|
|
29
|
+
set: { updateTab(with: $0) }
|
|
30
|
+
)) {
|
|
31
|
+
ForEach(availableSections) { section in
|
|
32
|
+
TabSection(section.title) {
|
|
33
|
+
ForEach(section.tabs) { tab in
|
|
34
|
+
Tab(value: tab) {
|
|
35
|
+
tab.makeContentView(
|
|
36
|
+
homeTimeline: $timeline,
|
|
37
|
+
selectedTab: $selectedTab,
|
|
38
|
+
pinnedFilters: $pinnedFilters
|
|
39
|
+
)
|
|
40
|
+
} label: {
|
|
41
|
+
tab.label
|
|
42
|
+
}
|
|
43
|
+
.tabPlacement(tab.tabPlacement)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
.tabPlacement(.sidebarOnly)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private func updateTab(with newTab: AppTab) {
|
|
52
|
+
if newTab == .post {
|
|
53
|
+
// Intercept special tabs (compose) instead of changing selection.
|
|
54
|
+
presentComposer()
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
selectedTab = newTab
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Example: direct binding without side effects
|
|
63
|
+
|
|
64
|
+
Use this when selection is purely state-driven.
|
|
65
|
+
|
|
66
|
+
```swift
|
|
67
|
+
@MainActor
|
|
68
|
+
struct AppView: View {
|
|
69
|
+
@Binding var selectedTab: AppTab
|
|
70
|
+
|
|
71
|
+
var body: some View {
|
|
72
|
+
TabView(selection: $selectedTab) {
|
|
73
|
+
ForEach(availableSections) { section in
|
|
74
|
+
TabSection(section.title) {
|
|
75
|
+
ForEach(section.tabs) { tab in
|
|
76
|
+
Tab(value: tab) {
|
|
77
|
+
tab.makeContentView(
|
|
78
|
+
homeTimeline: $timeline,
|
|
79
|
+
selectedTab: $selectedTab,
|
|
80
|
+
pinnedFilters: $pinnedFilters
|
|
81
|
+
)
|
|
82
|
+
} label: {
|
|
83
|
+
tab.label
|
|
84
|
+
}
|
|
85
|
+
.tabPlacement(tab.tabPlacement)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
.tabPlacement(.sidebarOnly)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Design choices to keep
|
|
96
|
+
|
|
97
|
+
- Centralize tab identity and content in `AppTab` with `makeContentView(...)`.
|
|
98
|
+
- Use `Tab(value:)` with `selection` binding for state-driven tab selection.
|
|
99
|
+
- Route selection changes through `updateTab` to handle special tabs and scroll-to-top behavior.
|
|
100
|
+
- Use `TabSection` + `.tabPlacement(.sidebarOnly)` for sidebar structure.
|
|
101
|
+
- Use `.tabPlacement(.pinned)` in `AppTab.tabPlacement` for a single pinned tab; this is commonly used for iOS 26 `.searchable` tab content, but can be used for any tab.
|
|
102
|
+
|
|
103
|
+
## Dynamic tabs pattern
|
|
104
|
+
|
|
105
|
+
- `SidebarSections` handles dynamic data tabs.
|
|
106
|
+
- `AppTab.anyTimelineFilter(filter:)` wraps dynamic tabs in a single enum case.
|
|
107
|
+
- The enum provides label/icon/title for dynamic tabs via the filter type.
|
|
108
|
+
|
|
109
|
+
## Pitfalls
|
|
110
|
+
|
|
111
|
+
- Avoid adding ViewModels for tabs; keep state local or in `@Observable` services.
|
|
112
|
+
- Do not nest `@Observable` objects inside other `@Observable` objects.
|
|
113
|
+
- Ensure `AppTab.id` values are stable; dynamic cases should hash on stable IDs.
|
|
114
|
+
- Special tabs (compose) should not change selection.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Theming and dynamic type
|
|
2
|
+
|
|
3
|
+
## Intent
|
|
4
|
+
|
|
5
|
+
Provide a clean, scalable theming approach that keeps view code semantic and consistent.
|
|
6
|
+
|
|
7
|
+
## Core patterns
|
|
8
|
+
|
|
9
|
+
- Use a single `Theme` object as the source of truth (colors, fonts, spacing).
|
|
10
|
+
- Inject theme at the app root and read it via `@Environment(Theme.self)` in views.
|
|
11
|
+
- Prefer semantic colors (`primaryBackground`, `secondaryBackground`, `label`, `tint`) instead of raw colors.
|
|
12
|
+
- Keep user-facing theme controls in a dedicated settings screen.
|
|
13
|
+
- Apply Dynamic Type scaling through custom fonts or `.font(.scaled...)`.
|
|
14
|
+
|
|
15
|
+
## Example: Theme object
|
|
16
|
+
|
|
17
|
+
```swift
|
|
18
|
+
@MainActor
|
|
19
|
+
@Observable
|
|
20
|
+
final class Theme {
|
|
21
|
+
var tintColor: Color = .blue
|
|
22
|
+
var primaryBackground: Color = .white
|
|
23
|
+
var secondaryBackground: Color = .gray.opacity(0.1)
|
|
24
|
+
var labelColor: Color = .primary
|
|
25
|
+
var fontSizeScale: Double = 1.0
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Example: inject at app root
|
|
30
|
+
|
|
31
|
+
```swift
|
|
32
|
+
@main
|
|
33
|
+
struct MyApp: App {
|
|
34
|
+
@State private var theme = Theme()
|
|
35
|
+
|
|
36
|
+
var body: some Scene {
|
|
37
|
+
WindowGroup {
|
|
38
|
+
AppView()
|
|
39
|
+
.environment(theme)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Example: view usage
|
|
46
|
+
|
|
47
|
+
```swift
|
|
48
|
+
struct ProfileView: View {
|
|
49
|
+
@Environment(Theme.self) private var theme
|
|
50
|
+
|
|
51
|
+
var body: some View {
|
|
52
|
+
VStack {
|
|
53
|
+
Text("Profile")
|
|
54
|
+
.foregroundStyle(theme.labelColor)
|
|
55
|
+
}
|
|
56
|
+
.background(theme.primaryBackground)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Design choices to keep
|
|
62
|
+
|
|
63
|
+
- Keep theme values semantic and minimal; avoid duplicating system colors.
|
|
64
|
+
- Store user-selected theme values in persistent storage if needed.
|
|
65
|
+
- Ensure contrast between text and backgrounds.
|
|
66
|
+
|
|
67
|
+
## Pitfalls
|
|
68
|
+
|
|
69
|
+
- Avoid sprinkling raw `Color` values in views; it breaks consistency.
|
|
70
|
+
- Do not tie theme to a single view’s local state.
|
|
71
|
+
- Avoid using `@Environment(\\.colorScheme)` as the only theme control; it should complement your theme.
|