@maccesar/titools 4.0.0 → 4.1.0

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/README.md CHANGED
@@ -342,7 +342,7 @@ Example prompts:
342
342
 
343
343
  Key features:
344
344
  - Quick decision matrix for common questions
345
- - 18 reference guides for deep dives
345
+ - 21 reference guides for deep dives
346
346
  - Delegates to specialized skills when needed
347
347
 
348
348
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maccesar/titools",
3
- "version": "4.0.0",
3
+ "version": "4.1.0",
4
4
  "description": "Titanium SDK skills and agents for AI coding assistants (Claude Code, Gemini CLI, Codex CLI)",
5
5
  "main": "lib/index.js",
6
6
  "type": "module",
@@ -149,7 +149,7 @@ For the complete reference with examples, see [Alloy builtins and globals](refer
149
149
  - [Contracts & Communication](references/contracts.md)
150
150
  - [State Management](references/state-management.md)
151
151
  - [Anti-patterns to Avoid](references/anti-patterns.md)
152
- - [Adaptive Layouts](references/adaptive-layouts.md) (responsive breakpoints, tablet/foldable/desktop support, Android 17 resizability compliance)
152
+ - [Adaptive Layouts](references/adaptive-layouts.md) (responsive breakpoints, tablet/foldable/desktop support, Android 16/17 resizability compliance, orientation locks and which Android activities they must cover)
153
153
 
154
154
  ### Implementation & API
155
155
  - [Alloy Builtins & Globals](references/alloy-builtins.md)
@@ -19,6 +19,26 @@ Scope: Only affects screens >= 600dp. Phones (< 600dp) and games (`android:appCa
19
19
 
20
20
  What this means: portrait-only apps will be forced into landscape/resized on large screens. If the UI doesn't support it, it will break.
21
21
 
22
+ ### It already started in Android 16 (API 36)
23
+
24
+ This is usually described as an API 37 change, but it lands one release earlier: an app
25
+ targeting **API 36** already has `android:screenOrientation` ignored on >= 600dp displays
26
+ running Android 16. The difference is that API 36 still ships an opt-out:
27
+
28
+ ```xml
29
+ <!-- inside <application>, or on an individual <activity> -->
30
+ <property
31
+ android:name="android.window.PROPERTY_COMPAT_ALLOW_RESTRICTED_RESIZABILITY"
32
+ android:value="true"/>
33
+ ```
34
+
35
+ The opt-out stops working the moment you target API 37, so treat it as a bridge while the
36
+ layout is made adaptive — not as a fix. Source: [Android 16 behavior changes](https://developer.android.com/about/versions/16/behavior-changes-16).
37
+
38
+ Worth knowing when debugging: this only kicks in at >= 600dp. If a **phone** is rotating
39
+ despite a portrait lock, the cause is in the app, not in Android — see the orientation
40
+ debugging recipe in section 7.
41
+
22
42
  ### Related: edge-to-edge (API 36)
23
43
 
24
44
  Before the resizability requirement (API 37), Android SDK 36 removes the opt-out for edge-to-edge rendering, causing Titanium layouts to overlap system navigation bars. The Titanium core team has a PR to handle this at the SDK level: tidev/titanium-sdk#14399 — adds an `EdgeToEdgeHelper` with correct padding so existing apps work without code changes.
@@ -343,6 +363,24 @@ const dpWidth = $.win.size.width / density // WRONG: halves the value
343
363
  Ti.Gesture.isPortrait() // TypeError!
344
364
  Ti.Gesture.portrait // correct — boolean property
345
365
 
366
+ // ❌ Setting orientation imperatively — silently overrides the manifest
367
+ $.win.activity.requestedOrientation = Ti.Android.SCREEN_ORIENTATION_PORTRAIT
368
+ // The manifest value is applied in onCreate; this runs afterwards and always wins.
369
+ // Nothing in tiapp.xml can beat it, which makes it invisible when debugging.
370
+ // Declare `orientationModes` on the window instead (see section 7).
371
+
372
+ // ❌ Detecting tablets with a pixel threshold
373
+ Math.min(displayCaps.platformWidth, displayCaps.platformHeight) >= 600
374
+ // platformWidth returns PIXELS on Android. A 720x1604 @xhdpi phone is 360dp wide,
375
+ // but 720 >= 600 reports "tablet". Use Alloy.isTablet / the formFactor query, which
376
+ // read Ti.Platform.Android.physicalSizeCategory instead of raw pixels.
377
+
378
+ // ❌ Passing an empty orientationModes array to mean "no preference"
379
+ orientationModes: []
380
+ // An empty array maps to SCREEN_ORIENTATION_SENSOR (TiWindowProxy.setOrientationModes),
381
+ // so it doesn't clear the setting — it actively unlocks rotation, overriding the manifest.
382
+ // To leave orientation alone, don't set the property at all.
383
+
346
384
  // ❌ Using gap property — does NOT exist in Titanium
347
385
  { layout: 'horizontal', gap: 10 }
348
386
 
@@ -373,7 +411,129 @@ Titanium generates the main activity name as `<app-id>.<Appname>Activity`. For e
373
411
  </android>
374
412
  ```
375
413
 
376
- To verify the exact generated activity name, check `build/android/AndroidManifest.xml` after a build.
414
+ To verify the exact generated activity name, check `build/android/app/src/main/AndroidManifest.xml` after a build.
415
+
416
+ ### Which activities a lock has to cover
417
+
418
+ `android:screenOrientation` is per-activity — it is not inherited from `<application>`. Every
419
+ window the app opens runs inside one of a small, fixed set of activities declared by the
420
+ Titanium AAR, so read the canonical list from the SDK instead of memorising it:
421
+
422
+ ```bash
423
+ cat ~/.gradle/caches/*/transforms/*/transformed/jetified-titanium-*/AndroidManifest.xml
424
+ ```
425
+
426
+ As of 13.4.0 there are exactly six, and `android:name` takes the fully-qualified form:
427
+
428
+ ```
429
+ org.appcelerator.titanium.TiActivity
430
+ org.appcelerator.titanium.TiTranslucentActivity
431
+ ti.modules.titanium.media.TiCameraActivity
432
+ ti.modules.titanium.media.TiCameraXActivity
433
+ ti.modules.titanium.media.TiVideoActivity
434
+ ti.modules.titanium.ui.android.TiPreferencesActivity
435
+ ```
436
+
437
+ The seventh is the app's own root activity, generated from the `<name>` in tiapp.xml (see the
438
+ derivation rule above). Which one hosts what:
439
+
440
+ | What the app does | Activity |
441
+ |---|---|
442
+ | `Window.open()`, Tab, TabGroup, NavigationWindow | `org.appcelerator.titanium.TiActivity` |
443
+ | Window with `modal:true`, `opacity`, or a `backgroundColor` with alpha < 255 | `org.appcelerator.titanium.TiTranslucentActivity` |
444
+ | `showCamera({ overlay })` | `ti.modules.titanium.media.TiCameraActivity` (`TiCameraXActivity` when `useCameraX:true`) |
445
+ | `showCamera()` with no overlay | none of yours — Titanium launches the system camera app |
446
+ | VideoPlayer added to a regular Window | `org.appcelerator.titanium.TiActivity` |
447
+
448
+ Two things are better left undeclared:
449
+
450
+ - **`android:configChanges`.** The CLI strips it before the merge even happens — `_build.js`
451
+ keeps a list of the `TiBaseActivity`-derived activities and calls
452
+ `removeActivityAttribute(name, 'android:configChanges')` on each, with the reason spelled out
453
+ in the source: *"Most devs don't set this right, causing UI to disappear when a config change
454
+ occurs for a missing setting."* So a hand-written subset is not dangerous, just dead weight —
455
+ Titanium's full list always wins.
456
+ - **A lock on `TiTranslucentActivity`.** Android 8–11 throws `IllegalStateException` ("Only
457
+ fullscreen opaque activities can request orientation") when a translucent activity requests a
458
+ fixed orientation. Declaring `orientationModes` on those windows is safer: the SDK applies it
459
+ inside a try/catch (`TiWindowProxy.setOrientationModes`), so the worst case is a modal that
460
+ rotates instead of an app that crashes.
461
+
462
+ ### Prefer orientationModes over the manifest
463
+
464
+ `orientationModes` is defined on `TiWindowProxy`, so the same declaration works on `Window`,
465
+ `TabGroup` and `NavigationWindow`. Being per-window it also composes with form-factor queries,
466
+ which is what you usually want:
467
+
468
+ ```xml
469
+ <!-- PurgeTSS -->
470
+ <TabGroup class="handheld:portrait tablet:landscape">
471
+ ```
472
+
473
+ ```tss
474
+ /* plain TSS equivalent */
475
+ '#tabs[formFactor=handheld]': { orientationModes: [ Ti.UI.PORTRAIT ] }
476
+ ```
477
+
478
+ Alloy resolves `formFactor` from `Ti.Platform.Android.physicalSizeCategory`, which is why this
479
+ is more reliable than any width comparison you could write yourself.
480
+
481
+ Keep the manifest for what Alloy cannot reach: the root activity, which shows the splash before
482
+ any JS runs, and optionally `TiActivity` as a blanket fallback for windows that carry no class.
483
+
484
+ A single Ti Element rule covers every window in the app without touching a single view, which
485
+ is usually all an Alloy app needs:
486
+
487
+ ```javascript
488
+ // purgetss/config.cjs
489
+ module.exports = {
490
+ theme: { extend: {} },
491
+ 'Window': { handheld: { apply: 'portrait' } }
492
+ }
493
+ ```
494
+
495
+ ```tss
496
+ /* generated — applies to every Window, including modal ones */
497
+ 'Window[formFactor=handheld]': { orientationModes: [ Ti.UI.PORTRAIT ] }
498
+ ```
499
+
500
+ Note it matches `Window` only, so a `TabGroup` still needs its own class. And because the
501
+ declaration is per-window, it does nothing for the splash — that one stays in the manifest.
502
+
503
+ ### Debugging a rotation that shouldn't happen
504
+
505
+ Ask the system for the effective value instead of re-reading the manifest:
506
+
507
+ ```bash
508
+ adb shell dumpsys activity activities | grep -E "topResumedActivity|requestedOrientation"
509
+ ```
510
+
511
+ If `requestedOrientation` disagrees with what tiapp.xml declares, something overrode it at
512
+ runtime — grep the app for `requestedOrientation` and `orientationModes` before touching the
513
+ manifest again. Note that an *existing* `orientationModes` can be the bug rather than the fix:
514
+ an empty array unlocks rotation (see the anti-patterns above).
515
+
516
+ Before blaming the OS, let the SDK tell you. Titanium logs a warning when Android drops the
517
+ request on a large screen, so this either hits or rules that cause out immediately:
518
+
519
+ ```bash
520
+ adb logcat | grep "Orientation request will be ignored"
521
+ # Fixed orientation is not supported on large screens (smallest width: <N>dp).
522
+ # Orientation request will be ignored on Android 16+.
523
+ ```
524
+
525
+ And to see the size bucket the device actually reports — `normal` means a phone, so the >= 600dp
526
+ rule does not apply and the cause is in the app:
527
+
528
+ ```bash
529
+ adb shell am get-config # e.g. ...-sw360dp-w360dp-h802dp-normal-...
530
+ ```
531
+
532
+ To confirm what actually shipped in the binary:
533
+
534
+ ```bash
535
+ aapt2 dump xmltree --file AndroidManifest.xml <apk> | grep -A2 "E: activity"
536
+ ```
377
537
 
378
538
  ### iOS: support all orientations on iPad
379
539
 
@@ -405,6 +565,9 @@ To verify the exact generated activity name, check `build/android/AndroidManifes
405
565
  | Device screen size | `Ti.Platform.displayCaps.platformWidth` — pixels on Android, needs `/logicalDensityFactor` |
406
566
  | Orientation (boolean) | `Ti.Gesture.portrait`, `Ti.Gesture.landscape` — properties, not methods |
407
567
  | Orientation event | `Ti.Gesture.addEventListener('orientationchange', handler)` |
568
+ | Lock orientation | `orientationModes` on the window — not `activity.requestedOrientation` |
569
+ | Tablet detection | `Alloy.isTablet` / `[formFactor=tablet]` — never a pixel threshold |
570
+ | Effective orientation at runtime | `adb shell dumpsys activity activities \| grep requestedOrientation` |
408
571
  | Window resize event | `$.win.addEventListener('postlayout', handler)` — fires on rotation, split-screen, resize |
409
572
  | Auto-size | `Ti.UI.SIZE` (fit content), `Ti.UI.FILL` (fill remaining) |
410
573
  | Percentages | `width: '50%'` relative to parent |