@maccesar/titools 4.20.0 → 4.21.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 +11 -6
- package/agents/ti-pro.md +3 -0
- package/commands/ti-audit.md +11 -10
- package/commands/ti-new-screen.md +1 -0
- package/lib/config.js +1 -0
- package/package.json +1 -1
- package/skills/ti-expert/SKILL.md +5 -2
- package/skills/ti-expert/references/patterns.md +2 -0
- package/skills/ti-reuse-first/SKILL.md +88 -0
- package/skills/ti-reuse-first/references/alloy-reuse.md +127 -0
- package/skills/ti-reuse-first/references/diagnostics.md +137 -0
- package/skills/ti-reuse-first/references/indirection.md +89 -0
- package/skills/ti-reuse-first/references/purgetss-reuse.md +84 -0
- package/skills/ti-reuse-first/references/the-ladder.md +123 -0
- package/skills/ti-ui/SKILL.md +2 -0
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
</div>
|
|
8
8
|
|
|
9
|
-
TiTools is a Titanium SDK toolkit for AI coding assistants. It ships
|
|
9
|
+
TiTools is a Titanium SDK toolkit for AI coding assistants. It ships 11 skills — 6 specialist skills (`ti-reuse-first`, `ti-expert`, `purgetss`, `ti-ui`, `ti-game`, `ti-synthengine`) plus 5 documentation-mirror skills (`ti-api`, `ti-guides`, `ti-howtos`, `alloy-guides`, `alloy-howtos`) — a research agent, and reference files covering reuse and simplification, Titanium architecture, API reference, native how-tos, Alloy MVC, PurgeTSS styling and resource automation, UI/UX patterns, 2D games, and native sound synthesis.
|
|
10
10
|
|
|
11
11
|
The reference files are maintained against official documentation whenever an official source exists, so the assistant can retrieve current framework behavior instead of guessing from generic training data.
|
|
12
12
|
|
|
@@ -36,7 +36,7 @@ The fastest way to get started with Claude Code. One command to add the marketpl
|
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
What you get:
|
|
39
|
-
- All
|
|
39
|
+
- All 11 TiTools skills (`ti-reuse-first`, `ti-expert`, `purgetss`, `ti-ui`, `ti-game`, `ti-synthengine`, `ti-api`, `ti-guides`, `ti-howtos`, `alloy-guides`, `alloy-howtos`)
|
|
40
40
|
- ti-pro research agent
|
|
41
41
|
- Session hook that auto-detects Titanium projects (`tiapp.xml`, Alloy, PurgeTSS, `ti.game`, `ti.synthengine`)
|
|
42
42
|
- Slash commands: `/ti-check`, `/ti-new-screen`, `/ti-audit`
|
|
@@ -174,6 +174,7 @@ The knowledge index is based on the latest Titanium SDK documentation. If your p
|
|
|
174
174
|
|
|
175
175
|
| Skill | Purpose | Best For |
|
|
176
176
|
| ------------ | ----------------------------------- | --------------------------------------- |
|
|
177
|
+
| ti-reuse-first | Reuse before writing new code | Whether it should exist and what already does it; duplication sweeps |
|
|
177
178
|
| ti-expert | Architecture and implementation | Starting point for most tasks |
|
|
178
179
|
| purgetss | Alloy styling + asset automation | Utility classes; Alloy/Classic branding, images, colors, fonts and modules |
|
|
179
180
|
| ti-ui | UI/UX patterns | Complex layouts, ListViews, platform UI |
|
|
@@ -186,7 +187,8 @@ The knowledge index is based on the latest Titanium SDK documentation. If your p
|
|
|
186
187
|
| alloy-howtos | Alloy CLI, configuration, debugging | alloy.jmk, config.json, custom XML tags |
|
|
187
188
|
|
|
188
189
|
Notes:
|
|
189
|
-
- The first
|
|
190
|
+
- The first six (`ti-reuse-first`, `ti-expert`, `purgetss`, `ti-ui`, `ti-game`, `ti-synthengine`) are specialist workflow skills reflecting TiTools' Titanium conventions and native-module contracts.
|
|
191
|
+
- `ti-reuse-first` runs before the others. It asks whether a thing needs to exist and what already does it — a search order that stops at the first rung that holds — and sweeps an existing `app/` for near-identical controllers, single-listener events, wrappers around one call, and layout recomputed in JavaScript. It is deliberately two-sided: it says when adding a module or a Widget is the right answer, so the bias does not simply invert.
|
|
190
192
|
- `ti-game` documents the `ti.game` native module (2D sprite engine on OpenGL ES 2.0, Android and iOS). Its API reference was verified against the module source and tracks upstream `main` (the module ships features ahead of its manifest version); it applies to Alloy and Classic projects alike and does not depend on PurgeTSS.
|
|
191
193
|
- `ti-synthengine` translates aesthetic sound requests into strict, production-ready JavaScript for the cross-platform `ti.synthengine` 1.0.0 module. Seven references preserve the official documentation and an eighth adds curated retro, xylophone, memory-pad, polyphony and timer-ownership recipes; the API contract was cross-checked against both native implementations.
|
|
192
194
|
- The latter five are documentation-mirror skills sourced from the official Titanium SDK docs (`tidev/titanium-docs`) and audited against them via the internal `titools-skill-auditor`.
|
|
@@ -249,7 +251,7 @@ TiTools provides slash commands for common tasks. Both channels ship them: the p
|
|
|
249
251
|
These commands automatically invoke the relevant skills:
|
|
250
252
|
- `/ti-check` reads `tiapp.xml`, validates declared native modules, and reports project health
|
|
251
253
|
- `/ti-new-screen` uses `ti-expert` + `purgetss` to create properly structured files
|
|
252
|
-
- `/ti-audit` routes all
|
|
254
|
+
- `/ti-audit` routes all 11 skills: the reuse sweep first, core Titanium checks always, Alloy/PurgeTSS checks when detected, integration how-tos when used, and `ti-game` / `ti-synthengine` checks when those modules are declared
|
|
253
255
|
|
|
254
256
|
---
|
|
255
257
|
|
|
@@ -289,6 +291,7 @@ All skills include automatic project detection to ensure compatibility:
|
|
|
289
291
|
| Skill | What It Detects | How It Works |
|
|
290
292
|
| --------- | --------------------- | ------------------------------------------------------------- |
|
|
291
293
|
| purgetss | PurgeTSS mode and project type | Checks PurgeTSS markers, then separates Alloy utilities from Classic standalone commands |
|
|
294
|
+
| ti-reuse-first | Existing app structure | Searches `app/lib/`, `app/controllers/`, `app/widgets/`, `purgetss/config.cjs` before proposing anything new |
|
|
292
295
|
| ti-expert | Alloy vs Classic | Checks for `app/` (Alloy) vs `Resources/` (Classic) structure |
|
|
293
296
|
| ti-ui | Titanium projects | Checks for `tiapp.xml` (both Alloy and Classic) |
|
|
294
297
|
| ti-game | ti.game module | Checks for `<module>ti.game</module>` in `tiapp.xml` or `require('ti.game')` |
|
|
@@ -301,16 +304,17 @@ Why this matters:
|
|
|
301
304
|
|
|
302
305
|
### Skill hierarchy
|
|
303
306
|
|
|
304
|
-
`ti-expert` is the architectural starting point
|
|
307
|
+
`ti-reuse-first` runs first — it decides whether there is anything to build — and `ti-expert` is the architectural starting point for what survives that question. Together they coordinate the rest of the bundled catalog:
|
|
305
308
|
|
|
306
309
|
| Scope | Skills |
|
|
307
310
|
| --- | --- |
|
|
311
|
+
| Whether to build it at all | `ti-reuse-first`, first |
|
|
308
312
|
| Architecture and UI | `ti-expert`, `ti-ui`, `purgetss` when configured |
|
|
309
313
|
| Native modules | `ti-game`, `ti-synthengine` when declared |
|
|
310
314
|
| Exact SDK/API guidance | `ti-api`, `ti-guides`, `ti-howtos` |
|
|
311
315
|
| Alloy MVC and tooling | `alloy-guides`, `alloy-howtos` for Alloy projects |
|
|
312
316
|
|
|
313
|
-
All
|
|
317
|
+
All eleven ship with TiTools; no second skill package is required.
|
|
314
318
|
|
|
315
319
|
---
|
|
316
320
|
|
|
@@ -804,6 +808,7 @@ This pattern is documented across four bundled TiTools skills: `ti-ui`, `ti-expe
|
|
|
804
808
|
|
|
805
809
|
| Skill | SKILL.md | References |
|
|
806
810
|
| --------- | ----------------------------- | ----------------------------------------------------- |
|
|
811
|
+
| ti-reuse-first | Reuse Before You Write | 5 files (the ladder, Alloy reuse, indirection, PurgeTSS reuse, diagnostics) |
|
|
807
812
|
| ti-expert | Architecture + Implementation | 26 files (patterns, feedback surfaces, file type association, sharing, testing, security, etc.) |
|
|
808
813
|
| purgetss | Setup + Critical Rules | 34 files (Classic compatibility, grid, animations, icons, class-index, SVG pipeline, etc.) |
|
|
809
814
|
| ti-ui | UI Rules + Platform Diffs | 14 files (layouts, lists, gestures, etc.) |
|
package/agents/ti-pro.md
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
name: ti-pro
|
|
3
3
|
description: Titanium SDK and Alloy research specialist. Loads the complete TiTools skill catalog and uses the relevant scopes together for research-style questions. Use for codebase analysis, architecture reviews, Titanium SDK implementation research, cross-feature questions, native game or sound modules, and platform-specific differences. Returns concrete findings with file and line references.
|
|
4
4
|
skills:
|
|
5
|
+
- ti-reuse-first
|
|
5
6
|
- ti-expert
|
|
6
7
|
- purgetss
|
|
7
8
|
- ti-ui
|
|
@@ -24,6 +25,7 @@ You are a research specialist for Titanium SDK and Alloy mobile development. Wit
|
|
|
24
25
|
|
|
25
26
|
When invoked, research complex topics by consulting the applicable skills from the complete TiTools catalog:
|
|
26
27
|
|
|
28
|
+
- `ti-reuse-first` - Whether the thing should exist at all, and what already does it; duplication and dead-structure sweeps
|
|
27
29
|
- `ti-expert` - Architecture, patterns, conventions
|
|
28
30
|
- `purgetss` - Utility-first styling classes (optional add-on; use when the project already has it or the user asks about it)
|
|
29
31
|
- `ti-ui` - UI/UX patterns, layouts, gestures
|
|
@@ -56,6 +58,7 @@ Do not force conditional skills onto an unrelated project. State which scopes we
|
|
|
56
58
|
|
|
57
59
|
| Use Instead | Reason |
|
|
58
60
|
| -------------------------------------------- | -------------------------------------- |
|
|
61
|
+
| `/ti-reuse-first` before writing new code | Inline reuse check during development |
|
|
59
62
|
| `/ti-expert` for architecture guidance | Inline consultation during development |
|
|
60
63
|
| `/purgetss` to verify a class | Quick inline reference |
|
|
61
64
|
| `/ti-ui` for UI/UX patterns and layouts | Task-oriented guidance |
|
package/commands/ti-audit.md
CHANGED
|
@@ -7,22 +7,23 @@ Audit the current Titanium project by invoking every applicable TiTools skill. F
|
|
|
7
7
|
|
|
8
8
|
Core Titanium audit:
|
|
9
9
|
|
|
10
|
-
1. Invoke `ti-
|
|
11
|
-
2. Invoke `ti-
|
|
12
|
-
3. Invoke `ti-
|
|
13
|
-
4. Invoke `ti-
|
|
14
|
-
5. Invoke `ti-
|
|
10
|
+
1. Invoke `ti-reuse-first` — before judging how anything is built, establish whether it should exist and what already does it: near-identical controllers, panels copied instead of parameterized, events with a single listener, wrappers around one call, repeated class strings, and layout recomputed in JavaScript that TSS resolves declaratively. Report each finding with the command that measured it, and with what consolidating it would cost as well as save — duplication kept on purpose is a valid answer.
|
|
11
|
+
2. Invoke `ti-expert` — check architecture, navigation ownership, event/timer cleanup, memory leaks, bridge pressure, testing boundaries, adaptive layouts and code conventions.
|
|
12
|
+
3. Invoke `ti-guides` — check `tiapp.xml`, CommonJS/Hyperloop usage, Titanium CLI/build assumptions, memory and bridge guidance, distribution settings, and toolchain compatibility.
|
|
13
|
+
4. Invoke `ti-api` — verify every audited `Ti.*` API, event, property, constant and third-party module call against its exact signature. Do not report a suspected API defect from memory.
|
|
14
|
+
5. Invoke `ti-ui` when the project creates Titanium UI — check layouts, list performance, event bubbling, gestures, animations, accessibility, orientation and platform-specific UI.
|
|
15
|
+
6. Invoke `ti-howtos` when the project implements a native integration such as location, maps, push, camera, media, SQLite, HTTPClient, WebView, intents, Keychain/iCloud, Hyperloop or CI/CD — audit the complete integration and platform setup, not only the JavaScript call site.
|
|
15
16
|
|
|
16
17
|
Project-type audit:
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
7. If the project is Alloy (`app/views/`, `app/controllers/`, `app/styles/`), invoke `alloy-guides` — check controllers, XML/TSS, models, collections, widgets, sync adapters and cleanup patterns.
|
|
20
|
+
8. If the Alloy project uses `alloy.jmk`, `config.json`, `widget.json`, conditional views, custom XML tags or Alloy CLI workflows, invoke `alloy-howtos` — check configuration precedence, build hooks, generated structures and compilation/debugging conventions.
|
|
21
|
+
9. If the project uses PurgeTSS (`purgetss/`, `purgetss/config.cjs`, a PurgeTSS-generated `app.tss`, or `purgetss.{ui,colors,fonts}.js`), invoke `purgetss` and detect Alloy vs. Classic before applying findings. In Alloy, audit unsupported utility classes, padding on Views, unnecessary manual TSS, dynamic components, grids, icons and animations. In Classic, audit only the supported standalone asset/CommonJS commands and their `Resources/` outputs; do not require or recommend `alloy.jmk`, generated TSS, utility classes, or `$.UI.create()`.
|
|
21
22
|
|
|
22
23
|
Native-module audit:
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
10. If the project declares `ti.game`, invoke `ti-game` — check for sprites moved from timers instead of native properties, levels sized from `Ti.Platform.displayCaps` instead of the `resize` event, sprites added one by one instead of in one array, and timers or sounds left running when the window closes.
|
|
26
|
+
11. If the project declares `ti.synthengine`, invoke `ti-synthengine` — check strict option names and types, mutually exclusive pitch fields, valid envelope intersections, checked Boolean results, one clear lifecycle owner, delayed startup after layout, app-owned timer cancellation, and `shutdown()` cleanup.
|
|
26
27
|
|
|
27
28
|
If a conditional skill does not apply, record it as `not applicable` with the detected reason instead of inventing findings from it.
|
|
28
29
|
|
|
@@ -6,6 +6,7 @@ argument-hint: "<screenName>"
|
|
|
6
6
|
|
|
7
7
|
Create a new Alloy screen with the given name. Before creating files:
|
|
8
8
|
|
|
9
|
+
0. Invoke the `ti-reuse-first` skill and confirm the screen has to be new. Run rung 1 against this project: list `app/controllers/` and `app/widgets/` and check whether an existing screen covers the case with a parameter, or whether a `<Require>` into an existing window is enough. If the answer is that an existing screen should be parameterized instead, say so, name it, and **stop** — do not generate the trio. Only continue when a new screen is genuinely the answer.
|
|
9
10
|
1. Invoke the `alloy-guides` skill to follow Alloy MVC patterns
|
|
10
11
|
2. If PurgeTSS is detected (purgetss/ folder exists), invoke the `purgetss` skill — all styling MUST use utility classes in the XML, NOT custom TSS
|
|
11
12
|
3. If PurgeTSS is detected, invoke the `ti-ui` skill for layout patterns
|
package/lib/config.js
CHANGED
package/package.json
CHANGED
|
@@ -9,12 +9,15 @@ allowed-tools: Read, Grep, Glob, Edit, Write, Bash(git *), Bash(node *)
|
|
|
9
9
|
|
|
10
10
|
Practical architecture and implementation guidance for Titanium SDK apps (Alloy and Classic). Focus on maintainability, clear boundaries, and low-friction testing.
|
|
11
11
|
|
|
12
|
+
> **Before anything new exists, run the `ti-reuse-first` skill.** It answers *whether* a controller, view, Widget, `app/lib/` module, event or wrapper should exist at all and what already does it; this guide answers *how* to build whatever survives that question. Copying an existing controller as a template counts as creating something new.
|
|
13
|
+
|
|
12
14
|
## Required workflow
|
|
13
15
|
|
|
14
16
|
This file is an index. Read the references that govern the task before making a recommendation or changing code.
|
|
15
17
|
|
|
16
18
|
| Task | Required reading |
|
|
17
19
|
| --- | --- |
|
|
20
|
+
| Anything new — a controller/view/style trio, Widget, `app/lib/` module, event, wrapper, or a file copied as a template | Run the `ti-reuse-first` skill first, then the row below that matches what survives |
|
|
18
21
|
| Architecture tier, folders, or boundaries | [Architecture tiers](references/architecture-tiers.md), [Alloy structure](references/alloy-structure.md) |
|
|
19
22
|
| Feedback, alerts, choices, or app-owned versus system UI | [Feedback surfaces](references/feedback-surfaces.md) |
|
|
20
23
|
| Reusable Snackbar, Dialog, Bottom Sheet, Widget, or `<Require>` | [Feedback Widget contracts](references/feedback-widget-contracts.md), [Alloy structure](references/alloy-structure.md) |
|
|
@@ -151,14 +154,14 @@ For the complete reference with examples, see [Alloy builtins and globals](refer
|
|
|
151
154
|
| ---------------------------------- | -------------------------------------------------------------- |
|
|
152
155
|
| How to create a new Alloy project? | `ti create -t app --alloy` (not `--classic` + `alloy new`) |
|
|
153
156
|
| Fastest way to build? | `tn <recipe>` (using TiNy CLI wrapper) |
|
|
154
|
-
| Controller > 100 lines? |
|
|
157
|
+
| Controller > 100 lines? | A smell, not a threshold — see `ti-reuse-first` |
|
|
155
158
|
| More than 50 IDs in XML? | Use Tier 3 (ID Scoping) |
|
|
156
159
|
| Where does API call go? | `lib/api/` |
|
|
157
160
|
| Where does business logic go? | `lib/services/` |
|
|
158
161
|
| How deep should `lib` folders be? | One level: `lib/<type>/<file>.js` |
|
|
159
162
|
| Where do I store auth tokens? | Keychain (iOS) / KeyStore (Android) via service |
|
|
160
163
|
| Models or Collections? | Collections for API data, Models for SQLite persistence |
|
|
161
|
-
| Ti.App.fireEvent or EventBus? |
|
|
164
|
+
| Ti.App.fireEvent or EventBus? | EventBus — once `ti-reuse-first` says it needs an event |
|
|
162
165
|
| Direct navigation or service? | Always Navigation service (auto cleanup) |
|
|
163
166
|
| Snackbar, Dialog, or Bottom Sheet? | Classify urgency, persistence, reversibility, and choice first |
|
|
164
167
|
| Widget or `<Require>`? | Widget for a self-contained API/lifecycle; `<Require>` for app composition |
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
|
|
16
16
|
<!-- TOC-END -->
|
|
17
17
|
|
|
18
|
+
> **Before introducing any of these:** each pattern below adds a layer, and a layer only pays for itself where there is a boundary — a place the implementation could plausibly change, or where the two sides genuinely should not know each other. Around a single call site with one caller, the same pattern is a detour that costs on every read. The `ti-reuse-first` skill runs that check; use it first, and treat the patterns here as the answer to *how* once it has established *whether*.
|
|
19
|
+
|
|
18
20
|
## 1. Native module abstraction (Wrapper)
|
|
19
21
|
|
|
20
22
|
**Use when:** Using native modules like Maps, Biometrics, or specialized Media players.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ti-reuse-first
|
|
3
|
+
description: "Use before anything new exists in a Titanium or Alloy project — 'hazme un panel de avisos', 'necesito otra pantalla', 'hazlo igual que el de Index', 'voy a copiar paneles/derechos como base', '¿dónde pongo esta función?'. A search order stopping at the first rung that holds: must this exist, is it already in app/, does a Ti.* API, the platform, an installed module or a PurgeTSS class cover it, can it be one line, and only then the smallest thing that works — copied exactly, without guards nobody asked for. AUTO-DETECT: if tiapp.xml exists, invoke BEFORE creating any controller, view, Widget, app/lib/ module, event or wrapper, or copying a file as a template: it answers whether the thing should exist and what already does it; ti-expert answers how to build what survives. Also audits app/ for duplicated controllers, single-listener events, wrappers around one call, and layout JS that TSS resolves — 'esto ya lo tengo en otro lado', 'simplifícalo', DRY, KISS. Not a licence to skip a module five call sites need."
|
|
4
|
+
allowed-tools: Read, Grep, Glob, Edit, Write, Bash(git *), Bash(node *)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Reuse before you write
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
Most of the avoidable damage in a mature Alloy app is not wrong code. It is code that should never have been written: the fourth panel that repeats the first three, the event with one listener, the wrapper around one call, the helper that restates a `Ti.*` API. It compiles, it passes review, and it is permanent.
|
|
12
|
+
|
|
13
|
+
This skill supplies the missing question. Every other Titanium skill answers *how do I build this well*; this one asks *should this exist at all, and if so, does something already do it*. Run it first — its answer changes what the other skills are asked to build.
|
|
14
|
+
|
|
15
|
+
The method is a search order with early exit: seven rungs, cheapest first, stop at the first one that holds. It is a search order, not a prohibition. Climbing all seven honestly and landing on "write the module" is a correct outcome — see [When adding structure is the right answer](references/the-ladder.md#when-adding-structure-is-the-right-answer). A bias against structure is the same failure as a bias toward it, pointed the other way.
|
|
16
|
+
|
|
17
|
+
## When to use
|
|
18
|
+
|
|
19
|
+
Use it before:
|
|
20
|
+
|
|
21
|
+
- Creating any controller/view/style trio, Widget, or `app/lib/` module.
|
|
22
|
+
- Copying an existing controller, view, or panel as a template.
|
|
23
|
+
- Introducing an event, a wrapper, a service layer, a registry, or a factory.
|
|
24
|
+
- Answering "where should this live" — the answer is often "nowhere new".
|
|
25
|
+
- Reviewing a design that adds a layer, or a diff whose file count outgrew its intent.
|
|
26
|
+
|
|
27
|
+
Use it in diagnostic mode when someone says a codebase feels repetitive, when a bug had to be fixed in four places, or when planning a refactor.
|
|
28
|
+
|
|
29
|
+
Do not use it to choose between two designs that both already exist and both work — that is `ti-expert`'s decision matrix. Do not use it to argue against a module that several call sites genuinely need. And do not let it stall a one-line fix: rung 6 exists so trivial work stays trivial.
|
|
30
|
+
|
|
31
|
+
## Required workflow
|
|
32
|
+
|
|
33
|
+
This file is an index. Open the reference that governs the decision before recommending or writing anything.
|
|
34
|
+
|
|
35
|
+
| Decision | Required reading |
|
|
36
|
+
| --- | --- |
|
|
37
|
+
| Anything new — first pass, always | [The ladder](references/the-ladder.md) |
|
|
38
|
+
| A new panel, screen, dialog, or a controller/view/style trio; Widget vs `<Require>` vs a shared base | [Alloy reuse](references/alloy-reuse.md) |
|
|
39
|
+
| An event, wrapper, service layer, registry, factory, or any new layer between two pieces of code | [Indirection](references/indirection.md) |
|
|
40
|
+
| Repeating class strings in XML, or reaching for a new TSS block | [PurgeTSS reuse](references/purgetss-reuse.md) |
|
|
41
|
+
| Auditing an existing `app/` for duplication and dead structure | [Diagnostics](references/diagnostics.md) |
|
|
42
|
+
| Whether the thing you would build already exists as a Titanium API | Verify the exact signature with the `ti-api` skill — never from memory |
|
|
43
|
+
|
|
44
|
+
Support each recommendation with `[source: references/<file>.md]`. If a claim cannot be verified from the loaded references or from the inspected project, prefix it with `FROM_MEMORY (unverified):` and do not present it as settled. This matters more here than in most skills: the whole method is *look before you build*, and a skill that answers from memory has already skipped its own first step.
|
|
45
|
+
|
|
46
|
+
## The ladder
|
|
47
|
+
|
|
48
|
+
Ask these in order and stop at the first rung that holds. Full detail, with the Titanium specifics for each, is in [The ladder](references/the-ladder.md).
|
|
49
|
+
|
|
50
|
+
| # | Question | If it holds |
|
|
51
|
+
| --- | --- | --- |
|
|
52
|
+
| 0 | Does this need to exist? | Delete the requirement, not the code. State what you skipped and why. |
|
|
53
|
+
| 1 | Is it already in this codebase? | Reuse or parameterize it. `grep` before you conclude it is not. |
|
|
54
|
+
| 2 | Does a `Ti.*` API or Alloy builtin cover it? | Use it. Verify the signature with `ti-api`. |
|
|
55
|
+
| 3 | Does the platform own this workflow? | Use the native surface. Trust, permission and payment flows stay native. |
|
|
56
|
+
| 4 | Does a module already in `tiapp.xml` or `package.json` cover it? | Use it. New dependencies need an argument. |
|
|
57
|
+
| 5 | Can it be one line at the call site? | Write the line. A direct reference beats a mechanism. |
|
|
58
|
+
| 6 | None of the above. | Build the smallest thing that works, where the next person will look. |
|
|
59
|
+
|
|
60
|
+
Two rules keep the ladder honest:
|
|
61
|
+
|
|
62
|
+
**Rung 1 is a search, not a recollection.** "I do not think we have that" is not an answer to rung 1. Run the search — `grep`, `ls app/lib/`, `ls app/widgets/` — and report what you found. The command is cheap; the duplicate is permanent.
|
|
63
|
+
|
|
64
|
+
**Classic projects climb the same ladder.** Only the paths change: `Resources/` instead of `app/`, CommonJS modules instead of `app/lib/`, and rungs 2 through 5 are identical because they are about Titanium and the platform, not about Alloy. Rung 1's Alloy-specific answers — `<Require>`, Widgets, `$.args` — have no Classic equivalent; there the reuse mechanism is a CommonJS module that returns a configured view. Detect which kind of project you are in before quoting a path.
|
|
65
|
+
|
|
66
|
+
**Copy the file that solves your problem, not the one whose name sounds close.** A full-screen legal-text window and a three-line confirmation overlay share a folder and nothing else. Before adopting any file as a template, open it and check its *shape* matches: how much screen it takes, how it opens and closes, what it owns. See [the worked example](references/alloy-reuse.md#worked-example-1-the-wrong-template).
|
|
67
|
+
|
|
68
|
+
## Diagnostic mode
|
|
69
|
+
|
|
70
|
+
Run the ladder backwards over code that already exists. The sweep — greps for near-identical controllers, single-listener events, wrappers around one call, and JS that recomputes what TSS resolves — is in [Diagnostics](references/diagnostics.md).
|
|
71
|
+
|
|
72
|
+
Report what you measure, with the command that measured it: *"14 of 18 panel controllers define their own `mostrarPanel`"* is a finding. *"There is a lot of duplication in the panels"* is an impression. The first one survives a disagreement; the second one starts an argument nobody can settle.
|
|
73
|
+
|
|
74
|
+
Duplication is evidence, not a verdict. Three copies that are drifting apart on purpose — because they are genuinely three different things — are cheaper than one abstraction with three flags. Say what the consolidation would cost as well as what it would save, then let the human decide.
|
|
75
|
+
|
|
76
|
+
## Reference guides
|
|
77
|
+
|
|
78
|
+
- [The ladder](references/the-ladder.md) — the seven rungs, each translated into Titanium and Alloy decisions, plus when adding structure is the right answer.
|
|
79
|
+
- [Alloy reuse](references/alloy-reuse.md) — Widget vs `<Require>` vs a shared base vs a new trio; creating a controller on demand instead of embedding one per window; the measured panel case.
|
|
80
|
+
- [Indirection](references/indirection.md) — events vs direct calls, wrappers, service layers, and how to tell a boundary from a detour.
|
|
81
|
+
- [PurgeTSS reuse](references/purgetss-reuse.md) — custom rules and `apply` in `config.cjs` instead of repeating class strings; what belongs in `_app.tss` and what never goes in `app.tss`.
|
|
82
|
+
- [Diagnostics](references/diagnostics.md) — the sweep for duplication and dead structure in an existing `app/`, with the exact commands.
|
|
83
|
+
|
|
84
|
+
## Related skills
|
|
85
|
+
|
|
86
|
+
- `ti-expert` — architecture, tiers, and the patterns worth introducing *after* this skill has established that something must be built.
|
|
87
|
+
- `purgetss` — the class contract itself; verify every utility class there.
|
|
88
|
+
- `ti-api` — the exact signature of any `Ti.*` API this skill sends you to reuse.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Alloy reuse
|
|
2
|
+
|
|
3
|
+
Alloy has real mechanisms for reusing XML and JS together — `<Require>` and Widgets — and `app/lib/` is not the only place shared code can live. This reference is about choosing between them, and about the decision that comes before all of them: whether the new screen needs to be a new file at all.
|
|
4
|
+
|
|
5
|
+
## The four exits
|
|
6
|
+
|
|
7
|
+
When something has to appear in more than one place, there are four ways out, and the reflex — copy the trio, or embed one instance everywhere — is usually the worst of them.
|
|
8
|
+
|
|
9
|
+
| Exit | What it is | When it is right |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| **Parameterize what exists** | Pass `$.args` to a controller that already does 90% of it | The differences are text, an icon, a callback, or a visible/hidden branch |
|
|
12
|
+
| **`<Require>`** | Compose an app-local controller inside a view | Shares the host's lifecycle, styling and domain; no independent contract |
|
|
13
|
+
| **Widget** | A packaged component with `widget.json`, its own styles and assets | Owns a public API, its own state and cleanup, and could move to another project |
|
|
14
|
+
| **New trio** | A fresh controller/view/style | Genuinely a different screen, not a variation of one |
|
|
15
|
+
|
|
16
|
+
`ti-expert`'s [alloy-structure.md § "When to use widget vs require"](../../ti-expert/references/alloy-structure.md) has the Widget-vs-`<Require>` table in full; read it rather than deciding from memory. What it does not cover, and what this reference adds, is the first row — parameterizing rather than adding anything — and the question of *where the instance lives*, below.
|
|
17
|
+
|
|
18
|
+
Usage count alone does not decide. Three call sites for a thing with no state and no lifecycle may still be three lines of `$.args`; one call site for something with timers and cleanup may still deserve a Widget.
|
|
19
|
+
|
|
20
|
+
## Where the instance lives: on demand, not one per window
|
|
21
|
+
|
|
22
|
+
A component needed in four windows does not need four instances. The reflex — a `<Require>` in each window's XML, a listener in each controller, and a `destruir()` each one has to remember to call — pays the cost of four view hierarchies, four listener registrations, and four chances to forget the cleanup, in exchange for saving a `createController` call.
|
|
23
|
+
|
|
24
|
+
The alternative is to build it when it is needed, on whatever window is visible, and destroy it on close:
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
// In the controller that knows which window is on screen.
|
|
28
|
+
Alloy.createController('paneles/avisoPush', opciones).mostrar()
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
// In the component: it attaches to the window it was handed, and cleans itself up.
|
|
33
|
+
const ventana = $.args.ventana
|
|
34
|
+
|
|
35
|
+
function mostrar() {
|
|
36
|
+
if (!ventana) { return false }
|
|
37
|
+
ventana.add($.getView())
|
|
38
|
+
$.getView().show()
|
|
39
|
+
$.animarVista.open($.getView())
|
|
40
|
+
return true
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function cerrar(despues) {
|
|
44
|
+
$.animarVista.close($.getView(), () => {
|
|
45
|
+
$.getView().hide()
|
|
46
|
+
ventana.remove($.getView())
|
|
47
|
+
$.destroy()
|
|
48
|
+
if (typeof despues === 'function') { despues() }
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The trade is real and worth stating: creating on demand costs a controller construction at the moment of use, which is visible if the component appears many times per second. That is the case for a list row, and it is why `ListView` templates exist. It is not the case for an alert, a confirmation, or a notice that arrives occasionally — there, a resident instance is memory held for an event that mostly does not happen.
|
|
54
|
+
|
|
55
|
+
The test: **how often does this appear, and does it need to survive being closed?** Rarely and no → create on demand. Constantly, or it holds state between appearances → resident.
|
|
56
|
+
|
|
57
|
+
## When a family of screens has already formed
|
|
58
|
+
|
|
59
|
+
The interesting case is not one duplicate. It is the fifteenth, when a house pattern has emerged without anyone deciding on it, and each new screen re-types it.
|
|
60
|
+
|
|
61
|
+
Measure before proposing anything. In one shipping Alloy app, over the 18 modal panels in `app/controllers/paneles/` (excluding the shared header component):
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# Denominator: panel controllers, excluding the shared component.
|
|
65
|
+
PANELS=$(ls app/controllers/paneles/*.js | grep -v panelHeader)
|
|
66
|
+
for p in "animarVista.open" "animarVista.close" "opacoClick" '$.getView().hide()' "mostrarPanel"; do
|
|
67
|
+
printf "%-22s %s/%s\n" "$p" "$(grep -lF "$p" $PANELS | wc -l | tr -d ' ')" "$(echo "$PANELS" | wc -l | tr -d ' ')"
|
|
68
|
+
done
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
| Repeated in every panel | Count |
|
|
72
|
+
| --- | --- |
|
|
73
|
+
| `$.animarVista.open($.getView())` | 16 / 18 |
|
|
74
|
+
| `$.animarVista.close($.getView(), …)` | 15 / 18 |
|
|
75
|
+
| `opacoClick` — dismiss on background tap | 15 / 18 |
|
|
76
|
+
| `$.getView().hide()` / `.show()` | 15 / 18 |
|
|
77
|
+
| An exported `mostrarPanel` | 14 / 18 |
|
|
78
|
+
|
|
79
|
+
And in the 18 corresponding views, counted as exact `class` attributes rather than substrings: 16 carry an identical `<Animation>` element, 14 an identical full-screen scrim, and 9 an identical zoom-in card.
|
|
80
|
+
|
|
81
|
+
That is a house pattern with no home. Every new panel re-types show, close, dismiss-on-background and animation, and a change to any of them is a fifteen-file edit that will be applied to twelve.
|
|
82
|
+
|
|
83
|
+
**What the numbers do not decide is the fix.** They establish the cost; the shape of the consolidation is a judgment call, and there are three defensible answers:
|
|
84
|
+
|
|
85
|
+
- **A Widget** (`app/widgets/panelBase/`) exposing `open()`, `close()` and a content slot. Strongest boundary, real public API, portable to the next project. Costs the most to introduce and forces every panel through one contract — which is wrong if the panels are drifting apart deliberately.
|
|
86
|
+
- **A shared behavior module** (`app/lib/panel.js`) that each controller hands `$` to, keeping the XML per-panel and lifting only the repeated JS. Smallest change, preserves per-panel layout freedom, does not unify the duplicated XML.
|
|
87
|
+
- **Leave it, and only stop the bleeding** — a `config.cjs` custom rule for the two repeated class strings, so at least new panels stop re-typing the appearance. Correct when the panels are genuinely diverging and a shared base would become a flag farm.
|
|
88
|
+
|
|
89
|
+
Present the measurement and the three options with what each costs. The choice of which is right depends on where the app is going, and that is the human's to make — a consolidation imposed on screens that were about to diverge is worse than the duplication it removed.
|
|
90
|
+
|
|
91
|
+
## Worked example 1: the wrong template
|
|
92
|
+
|
|
93
|
+
**The task**: show a three-line notice over whatever window is visible when a push notification arrives with the app open.
|
|
94
|
+
|
|
95
|
+
**What happened**: `paneles/derechos` was copied as the template — a full-screen modal window built to scroll legal text. The result filled the screen for a three-line message.
|
|
96
|
+
|
|
97
|
+
**What was already there**: `paneles/tombolaSalir` — a scrim with a centered card, two buttons, dismiss on background tap. The same shape as the task. It was one folder listing away.
|
|
98
|
+
|
|
99
|
+
**The error was not in the code**; the copied file was perfectly good code. The error was choosing the template by proximity — same folder, similar name — instead of by shape. Both files are "panels"; only one of them is a small overlay.
|
|
100
|
+
|
|
101
|
+
**The check that would have caught it**: before copying any file as a template, open it and answer three questions. *How much screen does it take? How does it open and close? What does it own?* If any answer differs from the task, keep looking — the right file is usually in the same folder.
|
|
102
|
+
|
|
103
|
+
## Worked example 2: four instances of a thing that appears once
|
|
104
|
+
|
|
105
|
+
**The task**: the same notice, reachable from four different windows.
|
|
106
|
+
|
|
107
|
+
**What was built first**: a `<Require>` in each of the four window XMLs, an `Alloy.Events` listener in each of the four controllers, and a `destruir()` each window had to remember to call. Four copies of a view hierarchy, permanently resident, for a notice that arrives occasionally.
|
|
108
|
+
|
|
109
|
+
**What replaced it**: one `Alloy.createController('paneles/avisoPush', opciones).mostrar()` in the controller that already tracks which window is visible, with the panel attaching itself to the window it is handed and calling `$.destroy()` on close. The four `<Require>` elements, the four listeners and the `destruir()` contract all went away.
|
|
110
|
+
|
|
111
|
+
**The tell**: *am I creating N copies of something that is only ever used one at a time?* If the answer is yes, the instance should be created at the moment of use. The question to ask out loud is the one that unlocked this: **can this be loaded dynamically?**
|
|
112
|
+
|
|
113
|
+
## Worked example 3: the right template, plus inventions
|
|
114
|
+
|
|
115
|
+
**The task**: a panel inside the Tómbola window, behaving like the one already working in `index` — the same handling the Estilos and Ruleta panels use. The instruction named the reference implementation explicitly.
|
|
116
|
+
|
|
117
|
+
**What was built**: the reference behavior, plus additions nobody asked for. Touch events were intercepted, `touchEnabled` was toggled on and off around the transition, and an `animating` flag was introduced that refused to close the panel until the open callback fired. None of the reference panels carry any of that.
|
|
118
|
+
|
|
119
|
+
**What happened**: the flag deadlocked. When the open callback did not arrive, `animating` stayed `true` and the panel could no longer be closed. The user could not open and close it repeatedly, and could not keep drawing cards. Each attempt to fix the symptom added another guard on top of the first — a patch on a patch, all of it defending against a problem that had never been observed.
|
|
120
|
+
|
|
121
|
+
**What replaced it**: the reference behavior, unedited. Animate, hide the whole container on close, leave the events connected. The `touchEnabled` toggling and the `animating` lock were deleted, not repaired.
|
|
122
|
+
|
|
123
|
+
**The admission afterwards was the diagnosis**: *"intenté prevenir toques durante la animación sin evidencia de que hiciera falta"*. Three panels in the same app had run for months without that guard. The evidence that it was unnecessary was already in the codebase, in the very files that had been named as the model.
|
|
124
|
+
|
|
125
|
+
**The rule**: when the instruction is *do it like the one that already works*, reuse means copying the behavior, not copying it and improving it. Additions to a reference implementation need the same argument any new code needs — a real, observed failure. An input you are guarding against because it *might* happen is rung 0 work: the requirement does not exist yet, so neither should the guard.
|
|
126
|
+
|
|
127
|
+
**The tell**: *does the file I was told to copy do this?* If it does not, and no bug report says it should, do not add it. And if a symptom appears after an addition of yours, the first move is to remove the addition — not to guard it.
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Diagnostics
|
|
2
|
+
|
|
3
|
+
Running the ladder backwards over code that already exists. Each sweep below is a command, a reading, and — the part that matters — what would make the finding wrong.
|
|
4
|
+
|
|
5
|
+
Two rules before starting:
|
|
6
|
+
|
|
7
|
+
**Report the command with the number.** *"14 of 18 panel controllers define their own `mostrarPanel`"* is a finding someone can check and disagree with. *"There is a lot of duplication in the panels"* is an impression, and impressions produce arguments instead of decisions.
|
|
8
|
+
|
|
9
|
+
**Duplication is evidence, not a verdict.** Three copies drifting apart on purpose are cheaper than one abstraction with three flags. Every finding here needs its second half: what the consolidation would cost, not only what it would save. Then the human decides — this sweep produces a report, not a refactor.
|
|
10
|
+
|
|
11
|
+
## 1. Near-identical controllers
|
|
12
|
+
|
|
13
|
+
Look for a house pattern that formed without anyone deciding on it: the same function names, the same open/close dance, the same event wiring, re-typed per screen.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Which function names recur across a family of controllers?
|
|
17
|
+
# find, not **/ — globstar is off in the bash 3.2 that ships with macOS,
|
|
18
|
+
# where **/*.js silently collapses to */*.js and misses everything deeper.
|
|
19
|
+
find app/controllers -name '*.js' -exec grep -ho "^function [a-zA-Z0-9_]*" {} + |
|
|
20
|
+
sort | uniq -c | sort -rn | head -20
|
|
21
|
+
|
|
22
|
+
# How many files in a family share one construct? Exclude the shared pieces that
|
|
23
|
+
# live in the same folder but are not members — they inflate the denominator.
|
|
24
|
+
FAM=$(ls app/controllers/paneles/*.js | grep -v panelHeader)
|
|
25
|
+
for p in "mostrarPanel" "opacoClick" "animarVista.open" '$.getView().hide()'; do
|
|
26
|
+
printf "%-22s %s/%s\n" "$p" "$(grep -lF "$p" $FAM | wc -l | tr -d ' ')" "$(echo "$FAM" | wc -l | tr -d ' ')"
|
|
27
|
+
done
|
|
28
|
+
|
|
29
|
+
# Rough size clustering — near-identical files tend to have near-identical line counts.
|
|
30
|
+
wc -l app/controllers/paneles/*.js | sort -n
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Reading it:** a construct in most of a family is a house pattern with no home. Every new member re-types it, and a change to it is an N-file edit that will be applied to N-2.
|
|
34
|
+
|
|
35
|
+
**What makes it wrong:** the family is not a family. Screens that share a folder are not necessarily variations of one thing, and a shared base imposed on genuinely different screens becomes a flag farm. Check that the members do the same *kind* of job before proposing to unify them.
|
|
36
|
+
|
|
37
|
+
**Where to go:** [Alloy reuse § when a family of screens has already formed](alloy-reuse.md#when-a-family-of-screens-has-already-formed) — it lays out the three defensible fixes and what each costs.
|
|
38
|
+
|
|
39
|
+
## 2. Events with a single listener
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# Every event name that is fired, and every one that is listened for.
|
|
43
|
+
grep -rhno "Alloy\.Events\.trigger(['\"][^'\"]*" app/ | sed "s/.*['\"]//" | sort | uniq -c | sort -rn
|
|
44
|
+
grep -rhno "Alloy\.Events\.on(['\"][^'\"]*" app/ | sed "s/.*['\"]//" | sort | uniq -c | sort -rn
|
|
45
|
+
|
|
46
|
+
# Same, for the leakier cross-context bus.
|
|
47
|
+
grep -rn "Ti\.App\.fireEvent\|Ti\.App\.addEventListener" app/
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Reading it:** an event with one trigger and one listener, where the two modules can see each other, is a function call with a name to keep in sync and a listener to clean up. Replace it with a direct call or a callback in `$.args`.
|
|
51
|
+
|
|
52
|
+
An event fired but never listened for is dead. An event listened for but never fired is either dead or fired from native code — check before deleting.
|
|
53
|
+
|
|
54
|
+
**What makes it wrong:** the listener set is genuinely open (a theme finishing its download, a session expiring), or the sender is a lower layer that must not know about the receiver. Both are correct uses; the count alone cannot tell them apart. Read the two modules before concluding.
|
|
55
|
+
|
|
56
|
+
**Where to go:** [Indirection § events versus a direct call](indirection.md#events-versus-a-direct-call).
|
|
57
|
+
|
|
58
|
+
## 3. Wrappers that wrap nothing
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# One-line functions that forward their arguments unchanged.
|
|
62
|
+
grep -rn -B1 -A3 "^function [a-zA-Z0-9_]*(" app/lib/*.js |
|
|
63
|
+
grep -A1 "^function" | grep -E "return (Ti|Modules|Alloy)\.[A-Za-z.]+\("
|
|
64
|
+
|
|
65
|
+
# How many callers does each exported function actually have?
|
|
66
|
+
grep -ho "^exports\.[a-zA-Z0-9_]*" app/lib/*.js | sed 's/exports\.//' | sort -u |
|
|
67
|
+
while read f; do printf "%-28s %s\n" "$f" "$(grep -rl "\.$f(" app/ | wc -l | tr -d ' ')"; done | sort -k2 -n
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Reading it:** a function that forwards one call unchanged, from one site, adding no normalization, no error handling and no default, is a longer name for the thing it calls.
|
|
71
|
+
|
|
72
|
+
**What makes it wrong:** it is a vendor seam. A wrapper whose whole job is to be the one place that changes when the module does is doing that job even while it is trivial. Ask whether the module's API is spreading through controllers — if it is, the wrapper is earning its keep in advance.
|
|
73
|
+
|
|
74
|
+
**Where to go:** [Indirection § wrappers](indirection.md#wrappers).
|
|
75
|
+
|
|
76
|
+
## 4. Layout computed in JavaScript
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
# Measuring and repositioning after the draw.
|
|
80
|
+
grep -rn "postlayout\|\.rect\b\|\.size\.width\|\.size\.height" app/controllers/ app/lib/
|
|
81
|
+
|
|
82
|
+
# Positions and sizes assigned from JS rather than declared in TSS.
|
|
83
|
+
grep -rn "applyProperties({[^}]*\(top\|left\|right\|bottom\|width\|height\):" app/controllers/
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Reading it:** a `postlayout` handler that measures a window and repositions its children runs after the first draw — the content visibly jumps — and re-runs on every layout pass. TSS conditionals (`[formFactor=tablet]`, `[platform=ios]`, `[if=…]`) resolve once, before anything is drawn.
|
|
87
|
+
|
|
88
|
+
**What makes it wrong:** the handler reacts to a real orientation change, or the value genuinely is not knowable at build time. When a decision must be made before the first draw, `Ti.Platform.displayCaps` answers it without waiting for a layout pass.
|
|
89
|
+
|
|
90
|
+
**Where to go:** [Indirection § layout logic in JavaScript](indirection.md#layout-logic-in-javascript).
|
|
91
|
+
|
|
92
|
+
## 5. Repeated class strings in the views
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
# The most-repeated class attributes in the project.
|
|
96
|
+
grep -rho 'class="[^"]*"' app/views/ | sort | uniq -c | sort -rn | head -20
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Reading it:** the same combination in several views, meant to stay identical, is a name waiting to be given — an `apply` rule in `purgetss/config.cjs`.
|
|
100
|
+
|
|
101
|
+
**What makes it wrong:** the agreement is coincidental. Two screens with the same padding today for unrelated reasons will need different padding tomorrow, and a shared class couples things that were never related.
|
|
102
|
+
|
|
103
|
+
**Where to go:** [PurgeTSS reuse § when a repeated string has earned a name](purgetss-reuse.md#when-a-repeated-string-has-earned-a-name).
|
|
104
|
+
|
|
105
|
+
## 6. Structure nobody reaches
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
# Exported functions with no caller anywhere in app/.
|
|
109
|
+
grep -ho "^exports\.[a-zA-Z0-9_]*" app/lib/*.js | sed 's/exports\.//' | sort -u |
|
|
110
|
+
while read f; do
|
|
111
|
+
[ "$(grep -rl "\.$f(" app/ | grep -v "app/lib" | wc -l | tr -d ' ')" = "0" ] && echo "no external caller: $f"
|
|
112
|
+
done
|
|
113
|
+
|
|
114
|
+
# Config keys nobody reads, and modules declared but never required.
|
|
115
|
+
grep -o '"[a-zA-Z0-9_]*":' app/config.json | tr -d '":' | sort -u |
|
|
116
|
+
while read k; do
|
|
117
|
+
[ "$(grep -rl "Alloy\.CFG\.$k" app/ | wc -l | tr -d ' ')" = "0" ] && echo "unread config key: $k"
|
|
118
|
+
done
|
|
119
|
+
grep -o "<module[^>]*>[^<]*</module>" tiapp.xml | sed 's/.*>\(.*\)<.*/\1/' |
|
|
120
|
+
while read m; do
|
|
121
|
+
[ "$(grep -rl "require('$m')\|require(\"$m\")" app/ | wc -l | tr -d ' ')" = "0" ] && echo "module never required in JS: $m"
|
|
122
|
+
done
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**Reading it:** each of these is rung 0 in hindsight — something built for a caller that never arrived.
|
|
126
|
+
|
|
127
|
+
**What makes it wrong, and this one bites:** a `grep` for a caller finds only the callers spelled that way. A function reached through a dynamic name, from a Widget, from `alloy.js`, from a native module, or from a project file outside `app/` will look dead and will not be. Modules in particular are frequently declared for their build-time side effects and never `require`d in JS — reached instead from a `module=` attribute in a view, or simply linked into the build. That is normal, not dead. A module declared once per platform also shows up twice; pipe through `sort -u` before reading the list. **Never delete on the strength of this sweep alone.** Report the candidate, say how it was found, and let someone who knows the history confirm.
|
|
128
|
+
|
|
129
|
+
## Reporting
|
|
130
|
+
|
|
131
|
+
Group findings by what they cost, not by how many there are:
|
|
132
|
+
|
|
133
|
+
1. **Costs a bug soon** — one behavior in N places that must agree and are already drifting. Cite the files and how they differ.
|
|
134
|
+
2. **Costs on every read** — a detour that always resolves to one place; a wrapper around one call; an event with one listener.
|
|
135
|
+
3. **Costs nothing yet** — a house pattern with no home, worth consolidating when the next member is added rather than today.
|
|
136
|
+
|
|
137
|
+
For each, give the measurement, the command, the proposed change, **and what it would cost** — including the case for leaving it alone. Then stop. A sweep that ends in an unrequested refactor has answered a question nobody asked, and the diff will be judged against a request that does not exist.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Indirection
|
|
2
|
+
|
|
3
|
+
Every layer between a caller and the work it wants done has to earn its place. This reference is about telling the ones that do from the ones that do not — and the distinction is not "how many layers" but whether each one is a **boundary** (a place where the two sides genuinely should not know each other, or where the implementation could plausibly change) or a **detour** (a hop that always resolves to the same destination).
|
|
4
|
+
|
|
5
|
+
Both look identical in a diff. Only one of them pays for itself.
|
|
6
|
+
|
|
7
|
+
## Events versus a direct call
|
|
8
|
+
|
|
9
|
+
An event bus is the right tool when the sender must not know the receiver: several listeners, an unknown number of them, or a genuine layering boundary the sender should not reach across. When there is exactly one listener and the sender can already see it, an event is a function call with a name to keep in sync, a registration to perform, and a leak to clean up.
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
// A lib that has no business knowing about windows announces an intention.
|
|
13
|
+
// Somewhere else, exactly one controller is listening.
|
|
14
|
+
Alloy.Events.trigger('abrirTienda')
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
// The controller that already owns abrirTienda() hands it over as a value.
|
|
19
|
+
if (esTemaNuevo) {
|
|
20
|
+
opciones.alAceptar = abrirTienda
|
|
21
|
+
}
|
|
22
|
+
Alloy.createController('paneles/avisoPush', opciones).mostrar()
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The second version deletes the event name, the listener, and the cleanup. It also fixes a layering problem the event was hiding: the module that handles push registration had to know that "open the store" was a thing that could happen. With a callback passed in, it does not — it takes a function and calls it, and the decision about what that function is stays in the controller that owns the navigation.
|
|
26
|
+
|
|
27
|
+
**Decision table:**
|
|
28
|
+
|
|
29
|
+
| Situation | Use |
|
|
30
|
+
| --- | --- |
|
|
31
|
+
| One known listener, caller can see it | Direct call or a callback in `$.args` |
|
|
32
|
+
| Parent needs to hear from a child controller it created | The child's exported function, or a callback the parent passed down |
|
|
33
|
+
| Several listeners, or the set is genuinely open | `Alloy.Events` (a `Backbone.Events` clone) |
|
|
34
|
+
| Sender must not know the receiver exists — a real seam | `Alloy.Events` |
|
|
35
|
+
| Cross-context, JS to native or between contexts | `Ti.App.fireEvent` — the last resort, and only then |
|
|
36
|
+
|
|
37
|
+
`ti-expert`'s decision matrix answers "`Ti.App.fireEvent` or EventBus?" with "always EventBus", and that is correct as far as it goes — it is comparing two event mechanisms, and `Ti.App.fireEvent` is the leakier one. It is not an argument that the interaction needs an event at all. Ask this question first; ask `ti-expert`'s second.
|
|
38
|
+
|
|
39
|
+
**The test:** *if I delete the event and call the function directly, what breaks?* If the answer is "nothing, but it feels less decoupled", the coupling was imaginary and the event was the cost of imagining it.
|
|
40
|
+
|
|
41
|
+
## Wrappers
|
|
42
|
+
|
|
43
|
+
A wrapper around a native module is a real pattern with a real purpose: it is the one place that changes when the vendor does, and it keeps the module's API from spreading through the app.
|
|
44
|
+
|
|
45
|
+
It stops being that when it forwards one call, unchanged, from one call site.
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
// A boundary: normalizes two platform APIs, owns the error contract, one place to change.
|
|
49
|
+
function comprar(sku) { … }
|
|
50
|
+
|
|
51
|
+
// A detour: this is Ti.Media.vibrate with a longer name.
|
|
52
|
+
function vibrar(patron) {
|
|
53
|
+
Ti.Media.vibrate(patron)
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Signs a wrapper is a detour: it has one function; that function forwards its arguments unchanged; it has one call site; it adds no error handling, no normalization, and no default. Signs it is a boundary: it reconciles iOS and Android behavior; it owns retries, logging or error translation; it is called from several places; or it is the seam a test substitutes.
|
|
58
|
+
|
|
59
|
+
The honest version of "we might swap this vendor later" is: write the wrapper when the swap becomes plausible, not when it becomes imaginable. The refactor from direct calls to a wrapper is mechanical and a `grep` finds every site; the cost of carrying an unnecessary layer is paid every time someone reads the code.
|
|
60
|
+
|
|
61
|
+
## Service layers and registries
|
|
62
|
+
|
|
63
|
+
`ti-expert` describes three architectural tiers and a set of patterns — Repository, Service layer, Factory, ServiceRegistry with dependency injection — and its matrix says a controller over 100 lines should be extracted to Tier 2. Those tiers are real and the guidance is sound *for apps that are at that tier*. The failure mode is applying a tier's machinery to an app that is not there yet, because the pattern is available rather than because the app needs it.
|
|
64
|
+
|
|
65
|
+
The line-count rule is a smell detector, not a instruction. A 140-line controller that does one screen's work, reads top to bottom, and shares nothing with any other screen is fine. A 90-line controller that repeats what three other controllers do is already a problem. Split when there is a second consumer, a real seam, or something worth testing on its own — not when a number is exceeded.
|
|
66
|
+
|
|
67
|
+
Before introducing a registry or dependency injection, answer: how many services are there, and how many of them have more than one implementation? Injection buys the ability to substitute. If nothing is ever substituted — not in tests, not per platform, not per environment — it has been bought and not used.
|
|
68
|
+
|
|
69
|
+
## Layout logic in JavaScript
|
|
70
|
+
|
|
71
|
+
This is the same failure in a different costume: JavaScript recomputing what the style system resolves declaratively.
|
|
72
|
+
|
|
73
|
+
A `postlayout` handler that measures a window and repositions its children runs after the first draw, so the content visibly jumps; it re-runs on every layout pass; and each device rule it encodes is a hand-written `if` that will eventually miss a case. TSS conditionals — `[formFactor=tablet]`, `[formFactor=handheld]`, `[platform=ios]`, `[if=…]` — resolve once, before anything is drawn, and cost nothing at runtime.
|
|
74
|
+
|
|
75
|
+
The same applies to positioning by arithmetic. Anchoring a button with `right: 178` works exactly once; a horizontal container with `height: Ti.UI.SIZE`, where `left` is the gap from the previous sibling, keeps working when the text, the width or the language changes.
|
|
76
|
+
|
|
77
|
+
**The test:** *am I adding up widths in my head to pick a number?* If so, the layout system is being asked to accept a result it could have computed. Reserve `postlayout` for reacting to a real orientation change, and use `Ti.Platform.displayCaps` when a decision must be made before anything is drawn.
|
|
78
|
+
|
|
79
|
+
## When indirection is the right call
|
|
80
|
+
|
|
81
|
+
Do not read this reference as an argument for flat code. These layers are worth their cost, and refusing them is the same error inverted:
|
|
82
|
+
|
|
83
|
+
- **A seam with two implementations today** — per-platform behavior, a test double, a mock server. Substitution that actually happens is what injection is for.
|
|
84
|
+
- **A repeated integration.** Five controllers each with their own `HTTPClient`, timeout and error handling is one bug in five places. The module that unifies them is not indirection; it is the fix.
|
|
85
|
+
- **An event with an open listener set.** A theme finishing its download, a session expiring — the publisher genuinely cannot know who cares, and the set grows. That is a bus, correctly used.
|
|
86
|
+
- **A wrapper at a vendor boundary.** When the native module's API is spreading through controllers, one wrapper is cheaper than the eventual migration.
|
|
87
|
+
- **Something with its own lifecycle** — state, timers, listeners, cleanup that belong together. One owner beats scattering them across the controllers that happen to use it.
|
|
88
|
+
|
|
89
|
+
State which of these applies when you propose a layer. "It is a seam: iOS and Android disagree here, and the wrapper is where they are reconciled" is a case anyone can check. "For decoupling" is not.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# PurgeTSS reuse
|
|
2
|
+
|
|
3
|
+
In a PurgeTSS project the duplication moves into the XML, where it is easy to miss because a repeated `class` string does not look like copied code. It is. The same five utilities typed into fifteen views is fifteen places to edit when the design changes, and the edit will land in twelve of them.
|
|
4
|
+
|
|
5
|
+
This reference covers where a style belongs and when a repeated string has earned a name. For whether a given utility class exists at all, invoke the `purgetss` skill — its class contract resembles Tailwind's but is not identical, and a class that does not exist fails silently.
|
|
6
|
+
|
|
7
|
+
## Where a style belongs
|
|
8
|
+
|
|
9
|
+
| Situation | Where it goes |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| A composition of existing utilities used in several views | A named class in `purgetss/config.cjs` via `apply` |
|
|
12
|
+
| A property PurgeTSS has no utility for | A rule in `purgetss/config.cjs`, or a hand-written block in `_app.tss` |
|
|
13
|
+
| A one-off on a single element | The `class` attribute in that view, inline |
|
|
14
|
+
| Anything at all | **Never `app/styles/app.tss`** |
|
|
15
|
+
|
|
16
|
+
`app.tss` is generated. PurgeTSS overwrites it on every run — it copies `_app.tss` into it [source: purgetss installation-setup.md] — so edits made there are work that deletes itself, usually a build or two later, which is long enough for the cause to be forgotten.
|
|
17
|
+
|
|
18
|
+
## Rung 1 for styles: search before you write a block
|
|
19
|
+
|
|
20
|
+
Before hand-writing a TSS rule, check whether a utility already does it. The PurgeTSS class set is large, and reimplementing a utility as a manual block is the styling version of reimplementing a `Ti.*` API.
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
grep -n "className" app/styles/_app.tss purgetss/config.cjs # is it already a project class?
|
|
24
|
+
grep -rn "class=\"[^\"]*someUtility" app/views/ # how is this done elsewhere?
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
If the design already exists somewhere in `app/views/`, copy how it is expressed there rather than inventing a second way to say the same thing. Two spellings of one visual result is duplication that no `grep` for a class name will ever find.
|
|
28
|
+
|
|
29
|
+
## When a repeated string has earned a name
|
|
30
|
+
|
|
31
|
+
Extract with `apply` when the same combination appears in several views **and** they are meant to stay identical — a design decision, not a coincidence.
|
|
32
|
+
|
|
33
|
+
```javascript
|
|
34
|
+
// purgetss/config.cjs
|
|
35
|
+
theme: {
|
|
36
|
+
'.fondo-panel': {
|
|
37
|
+
apply: 'fondo-opaco hidden opacity-0'
|
|
38
|
+
},
|
|
39
|
+
'.tarjeta-panel': {
|
|
40
|
+
apply: 'zoom-in-125 bg-panel close:duration-0 open:duration-100'
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```xml
|
|
46
|
+
<!-- Before: fifteen views carrying the same two strings -->
|
|
47
|
+
<View class="fondo-opaco hidden opacity-0">
|
|
48
|
+
<View class="zoom-in-125 bg-panel close:duration-0 open:duration-100">
|
|
49
|
+
|
|
50
|
+
<!-- After: one place to change the house style -->
|
|
51
|
+
<View class="fondo-panel">
|
|
52
|
+
<View class="tarjeta-panel">
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
A measured example, from the same app whose panels are counted in [Alloy reuse](alloy-reuse.md#when-a-family-of-screens-has-already-formed): of 18 panel views, 14 carry an identical scrim string and 9 an identical card string. Two `apply` rules replace 23 repetitions, and the next panel inherits the house style instead of re-typing it.
|
|
56
|
+
|
|
57
|
+
Count exact attributes, not substrings. Grepping for `zoom-in-125` in those same views returns 15 files — but six of them spell the rest of the string differently, and a class extracted from a substring match would quietly change how those six look. The count that matters is of the whole attribute.
|
|
58
|
+
|
|
59
|
+
Counting is one command:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# Exact class attributes, most repeated first.
|
|
63
|
+
grep -rho 'class="[^"]*"' app/views/ | sort | uniq -c | sort -rn | head -20
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**When not to extract:**
|
|
67
|
+
|
|
68
|
+
- **One use.** A name for a single site is indirection with a stylesheet accent — now the reader has to look somewhere else to learn what one element looks like.
|
|
69
|
+
- **Coincidental agreement.** Two screens that happen to use the same padding today, for unrelated reasons, will need different padding tomorrow. Extracting them couples two things that were never related, and the flag that eventually separates them costs more than the duplication did.
|
|
70
|
+
- **Names that describe appearance rather than role.** `.tarjeta-panel` survives a redesign; `.caja-blanca-redondeada` becomes a lie the first time the card turns grey, and nobody renames it.
|
|
71
|
+
|
|
72
|
+
The test: *if this changes, must every user of it change too?* Yes → one name. No → leave them apart.
|
|
73
|
+
|
|
74
|
+
## Device and platform differences belong here, not in JavaScript
|
|
75
|
+
|
|
76
|
+
PurgeTSS carries platform and device modifiers, and `config.cjs` rules take `ios`, `android`, `handheld` and `tablet` blocks [source: purgetss custom-rules.md]. A tablet layout expressed as `tablet:w-1/2` resolves at build time, once. The same result computed in a `postlayout` handler runs after the first draw, so the content visibly jumps, and re-runs on every layout pass.
|
|
77
|
+
|
|
78
|
+
If a layout decision is being made with arithmetic in a controller, the question is which modifier expresses it. See [Indirection § layout logic in JavaScript](indirection.md#layout-logic-in-javascript).
|
|
79
|
+
|
|
80
|
+
## Verify every class
|
|
81
|
+
|
|
82
|
+
PurgeTSS looks like Tailwind and diverges from it in specific places, and Titanium has no equivalent of a browser's inspector to show a class doing nothing. A misremembered class name produces no error, no warning, and an element that quietly ignores it.
|
|
83
|
+
|
|
84
|
+
Invoke the `purgetss` skill and confirm each class you write. Citing it costs one line; a class that silently does nothing costs a build, a device, and someone's afternoon.
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# The ladder
|
|
2
|
+
|
|
3
|
+
Seven rungs, cheapest first. Ask them in order and stop at the first one that holds. The point of the order is that each rung costs less than the one below it — a `grep` costs seconds, a new `app/lib/` module costs every future reader — so the search should exhaust the cheap answers before it reaches for the expensive one.
|
|
4
|
+
|
|
5
|
+
The ladder is a search order, not a prohibition. See [When adding structure is the right answer](#when-adding-structure-is-the-right-answer) at the bottom before using it to argue against building something.
|
|
6
|
+
|
|
7
|
+
## Rung 0 — Does this need to exist?
|
|
8
|
+
|
|
9
|
+
The only code with no maintenance cost is the code that was never written. Before anything else, ask what breaks if this does not exist.
|
|
10
|
+
|
|
11
|
+
Things that usually fail rung 0 in a Titanium app:
|
|
12
|
+
|
|
13
|
+
- A setting nobody will change. A constant is a setting with the configuration removed.
|
|
14
|
+
- Error handling for a state that cannot occur. `if (!Alloy.Globals.ventanaActual)` earns its place because there genuinely is a moment with no visible window; a null check on `$.getView()` inside its own controller does not.
|
|
15
|
+
- A "manager" holding one function. That is a function.
|
|
16
|
+
- A parameter with one caller passing one value.
|
|
17
|
+
- A cleanup path for an object the platform already destroys.
|
|
18
|
+
|
|
19
|
+
If the answer is "nothing breaks", say so and stop. Then say it out loud in the response: *"I skipped the retry wrapper — the call has one caller and `ti-api` says the method throws synchronously."* Work you decided not to do is invisible unless you name it, and unnamed decisions get re-litigated next session.
|
|
20
|
+
|
|
21
|
+
## Rung 1 — Is it already in this codebase?
|
|
22
|
+
|
|
23
|
+
This rung fails most often not because the thing was absent but because nobody looked. **"I do not think we have that" is not an answer to rung 1.** Run the search and report what came back.
|
|
24
|
+
|
|
25
|
+
Where to look, in a standard Alloy project. In a Classic project the same search runs over `Resources/`, and the reuse mechanism at the end of it is a CommonJS module returning a configured view rather than a Widget or `<Require>`:
|
|
26
|
+
|
|
27
|
+
| Looking for | Search |
|
|
28
|
+
| --- | --- |
|
|
29
|
+
| A helper or service | `ls app/lib/` then `grep -rn "functionName" app/lib/` |
|
|
30
|
+
| A screen, panel, or dialog | `ls app/controllers/` and any subfolders |
|
|
31
|
+
| A reusable component with its own API | `ls app/widgets/` |
|
|
32
|
+
| A style or class | `grep -n "className" app/styles/_app.tss purgetss/config.cjs` |
|
|
33
|
+
| A constant, endpoint, or flag | `grep -rn "VALUE" app/config.json app/alloy.js app/lib/` |
|
|
34
|
+
| An already-declared native module | `grep -n "<module" tiapp.xml` |
|
|
35
|
+
|
|
36
|
+
Three failure modes live on this rung. The first two are opposites; the third arrives after the search has already succeeded.
|
|
37
|
+
|
|
38
|
+
**Missing what is there.** A `grep` for the exact identifier finds nothing, so the conclusion is "we do not have it" — when the project spells it differently. Search for the *concept*: two or three plausible names, plus the domain noun. Looking for a debounce, also try `throttle`, `timer`, `delay`.
|
|
39
|
+
|
|
40
|
+
**Copying the wrong thing.** This one is more expensive, because it produces working code that is wrong in shape. When a file *is* found, the temptation is to copy it as a template. Before doing that, open it and check that it solves the same problem — not that it lives in the same folder or has a similar name. A full-screen modal window for scrolling legal text and a three-line confirmation overlay are both "panels", and adopting the first as a template for the second produces an aviso that fills the screen. See [Alloy reuse § worked example 1](alloy-reuse.md#worked-example-1-the-wrong-template).
|
|
41
|
+
|
|
42
|
+
The test: *does this file's shape match my problem — how much screen it takes, how it opens and closes, what it owns — or does it just share a folder with it?*
|
|
43
|
+
|
|
44
|
+
**Copying the right thing and then improving it.** The template is found, it is the correct one, and the copy arrives with additions the original never had: a lock around the animation, a `touchEnabled` toggle, an interception "just in case". The reference implementation has been running without them; that is the evidence they are not needed. Reuse means copying the behavior, not copying it and defending it against a failure nobody has seen. See [Alloy reuse § worked example 3](alloy-reuse.md#worked-example-3-the-right-template-plus-inventions).
|
|
45
|
+
|
|
46
|
+
## Rung 2 — Does a `Ti.*` API or Alloy builtin cover it?
|
|
47
|
+
|
|
48
|
+
Titanium's namespace is broad, and reimplementing part of it is easy to do by accident because the reimplementation always looks reasonable in isolation.
|
|
49
|
+
|
|
50
|
+
Reinventions worth checking for, each verified against the `ti-api` skill's references:
|
|
51
|
+
|
|
52
|
+
| Hand-rolled | Already exists |
|
|
53
|
+
| --- | --- |
|
|
54
|
+
| A date formatter | `String.formatDate(date, format)` [source: ti-api api-xml-global.md] |
|
|
55
|
+
| A key/value store over the filesystem | `Ti.App.Properties` [source: ti-api api-app-platform.md] |
|
|
56
|
+
| Screen-size math from a window's `size` | `Ti.Platform.displayCaps` [source: ti-api api-app-platform.md] |
|
|
57
|
+
| A lookup table of translated strings | `L(key, hint)` [source: ti-api api-xml-global.md] |
|
|
58
|
+
| A language check helper | `Ti.Locale.currentLanguage` — a property, not a method [source: ti-api api-core.md] |
|
|
59
|
+
| A custom event emitter | `Alloy.Events` (a `Backbone.Events` clone) [source: ti-expert migration-patterns.md] |
|
|
60
|
+
| A globals bag for passing controller params | `$.args` |
|
|
61
|
+
| A compile-time platform flag | `OS_IOS` / `OS_ANDROID` |
|
|
62
|
+
| A config module | `Alloy.CFG`, from `app/config.json` |
|
|
63
|
+
|
|
64
|
+
**Verify the exact signature before writing the call.** Whether something is a property or a method, and which namespace owns it, is exactly the class of detail that memory gets wrong — and a wrong `Ti.*` call does not fail at lint, it fails on the device in the user's hand. Invoke `ti-api` and cite it.
|
|
65
|
+
|
|
66
|
+
## Rung 3 — Does the platform own this workflow?
|
|
67
|
+
|
|
68
|
+
Some interactions belong to the operating system, and rebuilding them costs more than the code: an app-built version loses the platform's accessibility, localization, and — where trust is involved — its credibility.
|
|
69
|
+
|
|
70
|
+
Keep native: permission prompts, authentication and biometrics, payment and in-app purchase, the share sheet and intent chooser, media pickers, and the system alert and option dialogs (`Ti.UI.createAlertDialog`, `Ti.UI.createOptionDialog` [source: ti-api api-core.md]).
|
|
71
|
+
|
|
72
|
+
Build app-owned: feedback that belongs to the app's own surface — inline validation, snackbars, screen-level empty and error states, and modals whose content is the app's rather than the system's. `ti-expert`'s feedback-surfaces reference draws the line in detail; consult it rather than guessing.
|
|
73
|
+
|
|
74
|
+
The trap is the middle: a confirmation dialog. If it is a plain "are you sure?", the system dialog is free and correct. If it carries the app's own branding, illustration, or a third option, it is app-owned — and then rung 1 applies, because the app almost certainly already has one.
|
|
75
|
+
|
|
76
|
+
## Rung 4 — Does a module already in the project cover it?
|
|
77
|
+
|
|
78
|
+
Check `tiapp.xml` `<modules>` and `package.json` before adding anything. Apps accumulate capable dependencies and then hand-roll what they already paid for.
|
|
79
|
+
|
|
80
|
+
Frequent cases: an animation module already declared (`purgetss.ui`, `ti.animation`) while new code animates by hand with `setTimeout`; a networking or storage module present while a controller talks to `Ti.Network.HTTPClient` directly; an icon font already installed while a new screen ships a PNG.
|
|
81
|
+
|
|
82
|
+
A genuinely new dependency needs an argument, not just a fit — its size, its platform coverage, who maintains it, and what happens at the next SDK bump. State that argument before proposing it.
|
|
83
|
+
|
|
84
|
+
## Rung 5 — Can it be one line at the call site?
|
|
85
|
+
|
|
86
|
+
This rung catches the most common over-engineering in Alloy code: replacing a reference with a mechanism.
|
|
87
|
+
|
|
88
|
+
If module A needs module B to do something and A can see B, **call B**. An event is a mechanism for when the sender must not know the receiver — several unknown listeners, or a genuine layering boundary. With one known listener it is a function call with extra steps, plus a listener to register, a name to keep in sync, and a leak to clean up.
|
|
89
|
+
|
|
90
|
+
```js
|
|
91
|
+
// Mechanism: the sender announces, hoping someone is listening.
|
|
92
|
+
Alloy.Events.trigger('abrirTienda')
|
|
93
|
+
|
|
94
|
+
// Reference: the caller passes the function it already has in scope.
|
|
95
|
+
opciones.alAceptar = abrirTienda
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The same rung catches layout: a `postlayout` handler that measures a window and repositions children is JavaScript doing what a TSS conditional (`[formFactor=tablet]`, `[platform=ios]`) does declaratively, once, before anything is drawn. See [Indirection](indirection.md) for the full treatment.
|
|
99
|
+
|
|
100
|
+
## Rung 6 — Build the smallest thing that works
|
|
101
|
+
|
|
102
|
+
Reaching this rung honestly is a result, not a failure. Now build it — and only it.
|
|
103
|
+
|
|
104
|
+
- One file, in the place the next person would look for it: `app/lib/<type>/<name>.js`, one level deep.
|
|
105
|
+
- The narrowest API that serves today's callers. Parameters get added when a second caller needs them, not in anticipation.
|
|
106
|
+
- No configuration until something needs configuring.
|
|
107
|
+
- Named for what it does in this app's vocabulary, not for the pattern it implements. `PushManager` beats `NotificationServiceFactory`.
|
|
108
|
+
|
|
109
|
+
Then check the diff against the request: every changed line should trace back to something that was asked for.
|
|
110
|
+
|
|
111
|
+
## When adding structure is the right answer
|
|
112
|
+
|
|
113
|
+
A skill that only ever says "do not build it" has replaced one bias with another, and the second one is harder to notice because it looks like discipline. These are the cases where climbing all seven rungs correctly lands on *build it*, and where hesitating is the error:
|
|
114
|
+
|
|
115
|
+
- **The same logic exists at three or more call sites and they must stay in agreement.** Network calls repeated across five controllers, each with its own timeout and error handling, is not five small conveniences — it is five places to fix one bug, and four of them will be missed. That is a module in `app/lib/`, and rung 1 is what proves the case: the `grep` that finds five copies is the argument.
|
|
116
|
+
- **A boundary the app genuinely has.** Persistence, networking, and native-module access are real seams. A wrapper there is not indirection; it is the place where the swap happens when the vendor changes.
|
|
117
|
+
- **Something with its own lifecycle.** State, timers, listeners, and cleanup that belong together belong in one owner — a Widget or a service. Scattering them across the controllers that happen to use them is the expensive version.
|
|
118
|
+
- **A contract several teams or projects depend on.** Portability is a reason, and `app/widgets/` is where it lives.
|
|
119
|
+
- **Tests.** Extracting logic so it can be tested is a real reason to add a file, even when there is only one caller.
|
|
120
|
+
|
|
121
|
+
The distinction that matters is between a **boundary** and a **detour**. A boundary is a place where the implementation could plausibly change, or where the two sides genuinely should not know each other. A detour is a hop that always resolves to the same destination. Both look like a layer in the diff; only one of them pays for itself.
|
|
122
|
+
|
|
123
|
+
When the answer is "build it", say why in a sentence — which rung failed, and what the evidence was. *"Five call sites, measured; extracting to `lib/api/themes.js`"* is a decision anyone can check. *"For maintainability"* is not.
|
package/skills/ti-ui/SKILL.md
CHANGED
|
@@ -9,6 +9,8 @@ allowed-tools: Read, Grep, Glob, Edit, Write, Bash(node *)
|
|
|
9
9
|
|
|
10
10
|
A practical guide to Titanium SDK UI. Covers layouts, event handling, animations, performance, and platform-specific components for iOS and Android.
|
|
11
11
|
|
|
12
|
+
> **When "new UI" means new files, run the `ti-reuse-first` skill first.** A new window, panel or dialog is often an existing one that needs a parameter, and a layout computed in JavaScript is often a TSS condition. This guide covers how to build whatever survives that question.
|
|
13
|
+
|
|
12
14
|
## Project detection
|
|
13
15
|
|
|
14
16
|
> **️ℹ️ auto-detects titanium projects**
|