Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
1b22228
fix(server): scheduled tasks no longer jump to the top when toggled (…
t3dotgg Oct 21, 2026
22b89ef
fix(server): Forgejo and GitCafe pull requests link to their threads …
MarcoDotIO Oct 21, 2026
fb7f087
fix(server): pushes the remote refuses say why instead of a generic f…
t3dotgg Oct 21, 2026
1ee0241
fix(composer): typing a price no longer opens the skill menu (#21519)
MarcoDotIO Oct 21, 2026
a35066d
fix(web): thread details PR merge uses the last selected merge method…
tris203 Oct 21, 2026
bee9ce8
fix(web): a Usage toggle shortcut now goes back instead of staying on…
t3dotgg Oct 21, 2026
159f2b8
fix(web): quote comment box stays on screen for long code lines (#22149)
EtanHey Oct 21, 2026
86fa9e0
fix(web): right panel launcher letters open their surface instead of …
t3dotgg Oct 21, 2026
edce5f0
fix(web): ../ file links outside the workspace open as host files (#2…
t3dotgg Oct 21, 2026
6de7da5
fix(shared): recognize relative folder links with trailing slash (#22…
macodev00 Oct 21, 2026
1a44c40
fix(client): usage limits skip disabled connections (#22936)
M4NU5 Oct 21, 2026
7845898
fix(web): imported VS Code themes keep destructive buttons red (#21924)
lgollut Oct 21, 2026
855db96
fix(shared): Azure DevOps repositories with a space in the name list …
Tabonx Oct 21, 2026
2333f16
fix(opencode): cancelled todos no longer keep the todo list running (…
MarcoDotIO Oct 21, 2026
0f74ff8
fix(web): copy to machine clones the fork, not its upstream (#22492)
AKolenda Oct 21, 2026
1befc71
fix(server): JetBrains IDEs open file links at the right column (#22826)
levochkaa Oct 21, 2026
70e5e38
fix(server): sync pull request draft status changes (#22832)
Yash-Singh1 Oct 21, 2026
bd6940b
test(web): chat shortcut test types through the composer handle (#23126)
juliusmarminge Oct 21, 2026
a12d6ed
Merge commit from fork
juliusmarminge Oct 21, 2026
b5a226e
fix(source-control): merge messages drop Claude Code's current claude…
MarcoDotIO Oct 21, 2026
ed9b7ca
fix(build): pin Node 26.12.1 for development and standalone releases …
PixPMusic Oct 22, 2026
9cdd059
fix(mobile): keep Duo toolbar actions clear of workspace tabs (#22976)
juliusmarminge Oct 22, 2026
ec2fd4d
fix(mobile): respect Duo safe areas in overlays (#22977)
juliusmarminge Oct 22, 2026
eec07b6
fix(mobile): retain resting scroll offsets through Duo inset changes …
juliusmarminge Oct 22, 2026
61bdec0
fix(mobile): deliver the first tab selection after Duo folding (#22979)
juliusmarminge Oct 22, 2026
13eea3f
fix(mobile): use native full-width sheets on iPhone Duo (#22980)
juliusmarminge Oct 22, 2026
added62
fix(mobile): the pull requests tab's empty detail asks for a pull req…
juliusmarminge Oct 22, 2026
801701d
fix(mobile): the iPhone Duo keeps list titles through folds and unfol…
juliusmarminge Oct 22, 2026
9fa551a
fix(mobile): the iPhone Duo searches from a field at the bottom of it…
juliusmarminge Oct 22, 2026
37d6fce
fix(mobile): a closed search no longer risks freezing the app (#23146)
juliusmarminge Oct 22, 2026
5ed2ea2
fix(mobile): folding the Duo keeps the thread's end or the reader's l…
juliusmarminge Oct 22, 2026
e6c2aeb
fix(mobile): the iPhone Duo's search field and composer line up with …
juliusmarminge Oct 22, 2026
83a8501
fix(mobile): Screens 5.0.0-t3.36 stops an iPhone Duo unfold crash (#2…
juliusmarminge Oct 22, 2026
89fe509
fix(mobile): the iPhone Duo's inspector panes use the full column (#2…
juliusmarminge Oct 22, 2026
8acbefe
fix(gitcafe): show line counts on linked and listed pull requests (#2…
maria-rcks Oct 22, 2026
49e97d4
docs(import): correct conversation size and batch limits (#23226)
only21mil Oct 22, 2026
6f6f68a
fix(server): stop abandoned turns before continuing threads (#23169)
Yash-Singh1 Oct 22, 2026
f8e4834
feat(server): export how long runs take and how they end (#22094)
juliusmarminge Oct 22, 2026
6318bfc
fix(mobile): new-thread heading clears the iPad header (#23206)
juliusmarminge Oct 22, 2026
aa0158d
fix(pi): fork completed turns while the next turn is running (#23052)
juliusmarminge Oct 22, 2026
b295dd8
fix(mobile): drafts swipe to discard and every row move animates (#23…
juliusmarminge Oct 22, 2026
defb7e6
feat(server): webhook filters fan out over arrays with a [] path wild…
juliusmarminge Oct 22, 2026
3690c86
fix(web): Arabic and Hebrew text reads and navigates right to left (#…
yzn-h Oct 22, 2026
a98ebde
fix(client-runtime): refresh Git refs when the RPC session changes (#…
Yash-Singh1 Oct 22, 2026
05b697f
fix(web): cap list layout passes to prevent React error 185 (#23315)
Yash-Singh1 Oct 22, 2026
e075957
fix(mobile): align iPad composer without shifting on keyboard focus (…
juliusmarminge Oct 22, 2026
6e3c5e6
feat(gitcafe): request reviewers and apply labels on GitCafe pull req…
juliusmarminge Oct 22, 2026
a49d18c
fix(source-control): clones retry over HTTPS when the SSH key is refu…
juliusmarminge Oct 22, 2026
efa5381
fix(mobile): the iOS composer reopens where you left a long draft (#2…
shivamhwp Oct 22, 2026
002e941
fix(mobile): preserve new-thread drafts and native header layout (#23…
juliusmarminge Oct 22, 2026
6f75bcb
feat: ChatGPT Business usage limits offer a copyable admin request (#…
juliusmarminge Oct 22, 2026
3b95e78
fix(web): paused sidebar videos stop reloading after every command (#…
shivamhwp Oct 22, 2026
4040867
chore(typecheck): typescript is the tsc-rs drop-in, no effect-tsgo pa…
t3dotgg Oct 22, 2026
96bae54
fix(mobile): Dynamic Island status no longer wraps next to other Live…
t3dotgg Oct 22, 2026
83f118a
perf(web): streamed reply text no longer re-renders ChatView (#23117)
juliusmarminge Oct 22, 2026
ee33c4c
perf(web): visit marks during a reply no longer re-render ChatView (#…
juliusmarminge Oct 22, 2026
5ffe045
fix(server): a Claude agent stops when the server that started it die…
shivamhwp Oct 22, 2026
ff52547
fix(server): SSH hosts no longer keep every old 300 MB runtime after …
shivamhwp Oct 22, 2026
43879d9
fix(settings): older clients connect again after idle compaction mode…
juliusmarminge Oct 22, 2026
8d3b061
fix(mobile): unify draft and thread navigation and composer behavior …
juliusmarminge Oct 22, 2026
ed3f965
ci: setup survives npm registry outages (#23582)
t3dotgg Oct 22, 2026
f054d1b
feat(web,mobile): messages from other agents fold into one-line rows …
shivamhwp Oct 22, 2026
356f127
fix(fork): merge upstream f054d1b into patch/home
fireboltdude1357 Oct 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
22 changes: 19 additions & 3 deletions .github/actions/setup-ci-deps/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,38 @@ description: Install Vite+ and the workspace dependencies from cache.
runs:
using: composite
steps:
# setup-vp reuses an installed exact version, so restoring its home skips the
# installer download on every job.
# ~/.vite-plus holds vp, the Node.js runtime, and pnpm. A cache entry never
# updates, so the key names all three versions; otherwise a pnpm or Node.js
# bump makes every job download them from npm again.
- name: Resolve Vite+ version
id: vp
shell: bash
run: |
echo "version=$(sed -nE 's/^ vite-plus: ([0-9][^ ]*)$/\1/p' pnpm-workspace.yaml)" >> "$GITHUB_OUTPUT"
echo "node=$(jq -r '.engines.node' package.json)" >> "$GITHUB_OUTPUT"
echo "pnpm=$(jq -r '.packageManager' package.json)" >> "$GITHUB_OUTPUT"

- name: Cache Vite+
id: vp_cache
if: steps.vp.outputs.version != ''
uses: actions/cache@v6
with:
path: ~/.vite-plus
key: vite-plus-home-v1-${{ runner.os }}-${{ runner.arch }}-${{ steps.vp.outputs.version }}
key: vite-plus-home-v2-${{ runner.os }}-${{ runner.arch }}-${{ steps.vp.outputs.version }}-node${{ steps.vp.outputs.node }}-${{ steps.vp.outputs.pnpm }}

# setup-vp always runs the installer, which fetches vp from npm even when
# this exact version is installed. A cache hit already has everything, so
# skip setup-vp and do its remaining work here. This keeps npm outages from
# failing jobs.
- name: Use cached Vite+
if: steps.vp_cache.outputs.cache-hit == 'true'
shell: bash
run: |
echo "$HOME/.vite-plus/bin" >> "$GITHUB_PATH"
"$HOME/.vite-plus/bin/vp" env use "${{ steps.vp.outputs.node }}"

- name: Setup Vite+
if: steps.vp_cache.outputs.cache-hit != 'true'
uses: voidzero-dev/setup-vp@v1
with:
node-version-file: package.json
Expand Down
11 changes: 3 additions & 8 deletions .github/workflows/release-desktop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -253,18 +253,13 @@ jobs:
if [[ "${{ inputs.version }}" =~ -nightly\.[0-9]{8}\.[0-9]+$ ]]; then brand=nightly; fi
node scripts/apply-web-brand-assets.ts "$brand" apps/server/dist/client

# The single-executable is built with a Node that supports --build-sea
# (25.7+); the repo itself stays on the engines.node version. A native
# build injects into the runner's own Node; a Windows target downloads its
# node.exe.
# Setup Vite+ selects the exact engines.node version from package.json.
# Native builds embed that executable; cross-target builds download the
# same version for their target.
- name: Build CLI single-executable
if: inputs.cli_archive
shell: bash
env:
# The exact version, not a major: vp downloads it from nodejs.org/dist on
# the runner, and only exact versions have a dist directory. Keep in
# step with SEA_NODE_VERSION in apps/server/vite.config.ts.
VP_NODE_VERSION: "26.8.2"
# The executable bundles the server again, so it needs the same stamp
# as release.yml's JS bundle.
T3CODE_TELEMETRY_CHANNEL: ${{ github.repository == 'pingdotgg/t3code' && inputs.release_channel || 'dev' }}
Expand Down
9 changes: 9 additions & 0 deletions .pnpmfile.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@
// Release scripts stage their own installs with explicit architectures.
module.exports = {
hooks: {
// `typescript` is @tsc-rs/typescript (the catalog entry). Its npm version (0.8.x) is outside
// vite-plus-core's typescript peer range, so pnpm gave the vite-plus that @effect/vitest
// imports apps/marketing's typescript@6.0.3 instead, and tests loaded a second vitest.
readPackage(pkg) {
if (pkg.name === "@voidzero-dev/vite-plus-core" && pkg.peerDependencies?.typescript) {
pkg.peerDependencies.typescript = "*";
}
return pkg;
},
updateConfig(config) {
// oxlint-disable-next-line t3code/no-global-process-runtime -- pnpm loads this file before any Effect runtime exists.
if (process.platform !== "win32") return config;
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ An empty database is a bad test. Seed your worktree's `.t3` with a copy of real

- Before you push, run `vp run check:changed`. It runs CI's fast checks for changed files: format, lint, knip, typecheck of affected packages and dependents, and tests beside changes. CI still runs the full test suites.
- While you iterate, run `vp test run <files>`. For big files, add `-t "<name>"`.
- Do not use `npx tsc`. Use `vp run typecheck`; it uses tsc-rs, which is 3x faster.
- Use `vp run typecheck`, not `npx tsc`; it caches per package. Both run tsc-rs (`typescript` is the `@tsc-rs/typescript` drop-in).
- Run `node -e` snippets from `apps/server` or the owning package. `effect` does not resolve from the repo root.
- Test meaningful logic or observable behavior. Do not render components to static markup to assert props or attributes, or add tests that merely assert callback wiring or mirror the implementation.
- **Do not run repo-wide checks.** No `vp check`, no `vp run -r test`, no `vp run -r typecheck` unless I ask. CI owns the full suite.
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@ export default defineConfig({
dependsOn: ["t3#build"],
cache: false,
},
// tsc-rs rewrites the tsbuildinfo it reads, which keeps a task out of the cache.
// tsc (tsc-rs) rewrites the tsbuildinfo it reads, which keeps a task out of the cache.
typecheck: {
command: "tsc-rs --noEmit",
command: "tsc --noEmit",
cache: { input: [{ auto: true }, "!*.tsbuildinfo"] },
},
},
Expand Down
Binary file not shown.
Binary file not shown.
125 changes: 100 additions & 25 deletions apps/mobile/modules/t3-composer-editor/ios/T3ComposerEditorView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ private struct ComposerTokenPayload: Decodable {
let end: Int
}

private struct ComposerSelectionPayload: Decodable {
private struct ComposerSelectionPayload: Decodable, Equatable {
let start: Int
let end: Int
}
Expand Down Expand Up @@ -449,6 +449,9 @@ public final class T3ComposerEditorView: ExpoView, UITextViewDelegate, UITextDro
private var tokensNeedRebuild = false
private var chipsNeedMeasuredWidth = false
private var hasDeferredForcedRebuild = false
private var pendingContentOffset: CGPoint?
private var needsSelectionReveal = false
private var selectionBeforeFocusingTap: (selection: ComposerSelectionPayload, value: String)?

let onComposerChange = EventDispatcher()
let onComposerSelectionChange = EventDispatcher()
Expand All @@ -464,7 +467,8 @@ public final class T3ComposerEditorView: ExpoView, UITextViewDelegate, UITextDro
public required init(appContext: AppContext? = nil) {
super.init(appContext: appContext)

clipsToBounds = false
clipsToBounds = true
textView.clipsToBounds = true
textView.delegate = self
textView.textDropDelegate = self
textView.backgroundColor = .clear
Expand Down Expand Up @@ -520,6 +524,12 @@ public final class T3ComposerEditorView: ExpoView, UITextViewDelegate, UITextDro
@objc private func openContext(_ recognizer: UITapGestureRecognizer) {
guard let (index, attachment) = contextAttachment(at: recognizer.location(in: textView)) else {
textView.becomeFirstResponder()
if selectionBeforeFocusingTap != nil {
// UIKit places the caret from its own tap recognizer during this same touch.
DispatchQueue.main.async { [weak self] in
self?.restoreSelectionBeforeFocusingTap()
}
}
return
}
openContext(index: index, attachment: attachment)
Expand Down Expand Up @@ -558,7 +568,11 @@ public final class T3ComposerEditorView: ExpoView, UITextViewDelegate, UITextDro
}

public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
true
// The collapsed editor only shows the draft's first line, so a tap that expands an
// overflowing draft returns to the caret it was left at instead of moving it there.
let collapsedOverflow = !textView.isFirstResponder && !textView.isScrollEnabled && draftOverflowsBounds()
selectionBeforeFocusingTap = collapsedOverflow ? (sourceSelection(), value) : nil
return true
}

public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
Expand All @@ -583,11 +597,27 @@ public final class T3ComposerEditorView: ExpoView, UITextViewDelegate, UITextDro

public override func layoutSubviews() {
super.layoutSubviews()
let previousSize = textView.bounds.size
textView.frame = bounds
if chipsNeedMeasuredWidth, bounds.width > 0 {
chipsNeedMeasuredWidth = false
applyControlledDocument(force: true)
}
if textView.isScrollEnabled, bounds.width > 0, bounds.height > 0 {
textView.layoutIfNeeded()
if let offset = pendingContentOffset {
let minY = -textView.adjustedContentInset.top
let maxY = max(minY, textView.contentSize.height - bounds.height + textView.adjustedContentInset.bottom)
textView.setContentOffset(CGPoint(x: offset.x, y: min(maxY, max(minY, offset.y))), animated: false)
pendingContentOffset = nil
}
if needsSelectionReveal || (previousSize != bounds.size && textView.isFirstResponder) {
needsSelectionReveal = false
if let selection = textView.selectedTextRange {
textView.scrollRectToVisible(textView.caretRect(for: selection.end), animated: false)
}
}
}
let placeholderX = textView.textContainerInset.left + textView.textContainer.lineFragmentPadding
let placeholderY = textView.textContainerInset.top
let placeholderWidth = max(
Expand Down Expand Up @@ -632,10 +662,12 @@ public final class T3ComposerEditorView: ExpoView, UITextViewDelegate, UITextDro
tokens = decode([ComposerTokenPayload].self, from: document.tokensJson) ?? []
tokensNeedRebuild = true
}
let previousSelection = sourceSelection()
value = document.value
requestedSelection = document.selection
applyControlledDocument(force: tokensNeedRebuild)
applyRequestedSelection()
cancelFocusingTapRestoreIfSelectionMoved(from: previousSelection)
if tokensMatchCurrentValue() {
tokensNeedRebuild = false
}
Expand Down Expand Up @@ -694,7 +726,16 @@ public final class T3ComposerEditorView: ExpoView, UITextViewDelegate, UITextDro
}

func setScrollEnabled(_ scrollEnabled: Bool) {
guard textView.isScrollEnabled != scrollEnabled else { return }
if !scrollEnabled {
// Disabling scrolling discards UITextView's offset. Restore it after
// expansion has laid out the text, regardless of prop update order.
pendingContentOffset = pendingContentOffset ?? textView.contentOffset
}
textView.isScrollEnabled = scrollEnabled
if scrollEnabled {
revealSelectionAfterLayout()
}
}

func setAutoFocus(_ autoFocus: Bool) {
Expand Down Expand Up @@ -738,8 +779,10 @@ public final class T3ComposerEditorView: ExpoView, UITextViewDelegate, UITextDro
}

func setSelection(start: Int, end: Int) {
let previousSelection = sourceSelection()
requestedSelection = ComposerSelectionPayload(start: start, end: end)
applyRequestedSelection()
cancelFocusingTapRestoreIfSelectionMoved(from: previousSelection)
}

public func textViewDidChange(_ textView: UITextView) {
Expand Down Expand Up @@ -811,6 +854,7 @@ public final class T3ComposerEditorView: ExpoView, UITextViewDelegate, UITextDro
}

public func textViewDidBeginEditing(_ textView: UITextView) {
revealSelectionAfterLayout()
onComposerFocus()
}

Expand All @@ -836,13 +880,21 @@ public final class T3ComposerEditorView: ExpoView, UITextViewDelegate, UITextDro
hasDeferredForcedRebuild = false

let previousSelection = sourceSelection()
if textView.isScrollEnabled {
pendingContentOffset = pendingContentOffset ?? textView.contentOffset
}
isApplyingControlledValue = true
textView.attributedText = makeAttributedDocument()
let targetSelection = requestedSelection ?? previousSelection
requestedSelection = nil
textView.selectedRange = displayRange(for: targetSelection)
restoreBaseTypingAttributes()
isApplyingControlledValue = false
if currentSource != value || previousSelection.start != targetSelection.start || previousSelection.end != targetSelection.end {
revealSelectionAfterLayout()
} else {
setNeedsLayout()
}
updatePlaceholderVisibility()
emitContentSizeIfNeeded()
}
Expand Down Expand Up @@ -1053,6 +1105,7 @@ public final class T3ComposerEditorView: ExpoView, UITextViewDelegate, UITextDro
return
}
value = textView.serializedText()
revealSelectionAfterLayout()
let selection = sourceSelection()
nativeEventCount += 1
onComposerChange([
Expand Down Expand Up @@ -1122,35 +1175,57 @@ public final class T3ComposerEditorView: ExpoView, UITextViewDelegate, UITextDro
isApplyingControlledValue = true
textView.selectedRange = nextRange
isApplyingControlledValue = false
revealSelectionAfterLayout()
}

private func restoreSelectionBeforeFocusingTap() {
guard let snapshot = selectionBeforeFocusingTap else {
return
}
selectionBeforeFocusingTap = nil
// A draft that changed during the touch, such as a transcript landing, owns the caret now.
guard textView.isFirstResponder, textView.markedTextRange == nil, value == snapshot.value else {
return
}
let range = displayRange(for: snapshot.selection)
guard !NSEqualRanges(range, textView.selectedRange) else {
return
}
textView.selectedRange = range
revealSelectionAfterLayout()
}

private func cancelFocusingTapRestoreIfSelectionMoved(from previousSelection: ComposerSelectionPayload) {
if sourceSelection() != previousSelection {
selectionBeforeFocusingTap = nil
}
}

private func draftOverflowsBounds() -> Bool {
let size = textView.sizeThatFits(CGSize(width: textView.bounds.width, height: .greatestFiniteMagnitude))
return size.height > textView.bounds.height + 1
}

private func revealSelectionAfterLayout() {
needsSelectionReveal = true
setNeedsLayout()
}

private func updatePlaceholderVisibility() {
placeholderLabel.isHidden = !value.isEmpty
}

private func emitContentSizeIfNeeded() {
// The laid-out text without insets. A one-line draft reports its natural width, so
// JS can tell whether it fits a narrower row than the one it is laid out in; past
// one line only the height matters, so layout stops at the second line. The width
// comes from the line fragment because usedRect spans the container once the view
// scrolls. A trailing newline's empty line only exists as the extra fragment.
let layout = textView.layoutManager
let container = textView.textContainer
var lines = 0
var width: CGFloat = 0
var height: CGFloat = 0
let glyphs = NSRange(location: 0, length: layout.numberOfGlyphs)
layout.enumerateLineFragments(forGlyphRange: glyphs) { _, used, _, _, stop in
lines += 1
width = used.width
height = used.maxY
if lines > 1 { stop.pointee = true }
}
if lines < 2, layout.extraLineFragmentTextContainer === container {
lines += 1
height = max(height, layout.extraLineFragmentUsedRect.maxY)
}
let nextSize = CGSize(width: lines == 1 ? ceil(width) : 0, height: ceil(height))
guard bounds.width > 0 else { return }
// Fit independently of the animated viewport: when scrolling is disabled,
// its current height can clip TextKit's laid-out line fragments.
let fittedHeight = textView.sizeThatFits(CGSize(width: bounds.width, height: .greatestFiniteMagnitude)).height
- textView.textContainerInset.top - textView.textContainerInset.bottom
let measuredHeight = min(lineHeight * 4, fittedHeight)
let width = fittedHeight <= lineHeight + 0.5
? textView.layoutManager.usedRect(for: textView.textContainer).width
: 0
let nextSize = CGSize(width: ceil(width), height: ceil(measuredHeight))
guard abs(nextSize.width - lastContentSize.width) > 0.5 ||
abs(nextSize.height - lastContentSize.height) > 0.5 else {
return
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ final class T3LayoutMetricsView: ExpoView {
}
var verticalBarEdge = "none"
var regions: [[String: Any]] = []
var bottomBarInset: CGFloat?
#if canImport(UIKit, _version: 9127.0.85)
if #available(iOS 27.1, *) {
let rtl = effectiveUserInterfaceLayoutDirection == .rightToLeft
Expand All @@ -77,6 +78,8 @@ final class T3LayoutMetricsView: ExpoView {
case .trailing: verticalBarEdge = rtl ? "left" : "right"
default: break
}
// Where UIKit floats a bar as tall as the app's bottom controls, level with its tab bar.
bottomBarInset = edgeInsets(for: .bar(onEdge: UIRectEdge.bottom, extent: 48)).bottom
for kind in [UIView.ReservedRegion.Kind.division, .occlusion] {
for region in reservedRegions(kind: kind) where region.isActive {
let frame = region.frame.intersection(bounds)
Expand All @@ -90,7 +93,7 @@ final class T3LayoutMetricsView: ExpoView {
}
}
#endif
let metrics: [String: Any] = [
var metrics: [String: Any] = [
"width": bounds.width, "height": bounds.height,
"hasHinge": hasHinge,
"horizontalSizeClass": traitCollection.horizontalSizeClass == .regular ? "regular" : "compact",
Expand All @@ -101,6 +104,7 @@ final class T3LayoutMetricsView: ExpoView {
],
"reservedRegions": regions,
]
metrics["bottomBarInset"] = bottomBarInset
let next = metrics as NSDictionary
guard lastMetrics?.isEqual(next) != true else { return }
lastMetrics = next
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@ Pod::Spec.new do |s|
s.pod_target_xcconfig = {
'DEFINES_MODULE' => 'YES',
}
s.resource_bundles = { 'T3NativeControlIcons' => ['Resources/Icons/pull-request*.png'] }
s.source_files = '**/*.{h,m,mm,swift,hpp,cpp}'
end
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ public final class T3NativeControlsModule: Module {
Prop("appearance") { (view: T3SegmentedControlView, value: String) in view.appearance = value }
}

View(T3SidebarTabBarView.self) {
ViewName("SidebarTabBar")
Events("onSelectionChange")
Prop("selectedIndex") { (view: T3SidebarTabBarView, value: Int) in view.selectedIndex = value }
Prop("selectedColor") { (view: T3SidebarTabBarView, value: UIColor) in view.selectedColor = value }
Prop("appearance") { (view: T3SidebarTabBarView, value: String) in view.appearance = value }
}

View(T3LayoutMetricsView.self) {
ViewName("LayoutMetrics")
Events("onMetricsChange")
Expand Down
Loading