@maccesar/titools 4.19.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/purgetss/SKILL.md +1 -1
- package/skills/purgetss/references/cli-commands.md +25 -6
- package/skills/purgetss/references/customization-deep-dive.md +9 -4
- package/skills/purgetss/references/multi-density-images.md +35 -5
- package/skills/purgetss/references/version-history.md +5 -0
- package/skills/ti-expert/SKILL.md +5 -2
- package/skills/ti-expert/references/patterns.md +2 -0
- package/skills/ti-game/SKILL.md +5 -2
- package/skills/ti-game/references/api.md +38 -4
- package/skills/ti-game/references/debugging-performance.md +1 -1
- package/skills/ti-game/references/examples.md +6 -5
- package/skills/ti-game/references/patterns.md +1 -1
- package/skills/ti-game/references/project-setup.md +7 -3
- package/skills/ti-game/references/recipes-action.md +1 -1
- package/skills/ti-game/references/recipes-adventure-effects.md +1 -1
- package/skills/ti-game/references/roadmap.md +8 -4
- package/skills/ti-game/references/tilemaps.md +1 -1
- 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
|
@@ -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**
|