@maccesar/titools 4.3.0 → 4.4.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.
@@ -1,448 +1,190 @@
1
- # Theming & dark mode
1
+ # Theming and dark mode
2
+
3
+ Choose the styling path from the project, not from personal memory. Semantic
4
+ roles are the shared contract; PurgeTSS or standard Alloy materializes them.
2
5
 
3
- <!-- TOC-START -->
4
6
  ## Contents
5
7
 
6
- - [Alloy theme system](#alloy-theme-system)
7
- - [Theme folder structure](#theme-folder-structure)
8
- - [Activating a theme](#activating-a-theme)
9
- - [Theme cascade](#theme-cascade)
10
- - [Dynamic theming with Alloy.Globals](#dynamic-theming-with-alloyglobals)
11
- - [Step 1: define color palette](#step-1-define-color-palette)
12
- - [Step 2: use in TSS with Alloy.Globals](#step-2-use-in-tss-with-alloyglobals)
13
- - [Step 3: theme switching service](#step-3-theme-switching-service)
14
- - [Step 4: respond to theme changes in controllers](#step-4-respond-to-theme-changes-in-controllers)
15
- - [System dark mode detection](#system-dark-mode-detection)
16
- - [iOS 13+ / Android 10+](#ios-13-android-10)
17
- - [Semantic colors (Cross-Platform)](#semantic-colors-cross-platform)
18
- - [Reusable style classes pattern](#reusable-style-classes-pattern)
19
- - [Using style classes in views](#using-style-classes-in-views)
8
+ - [Decision path](#decision-path)
9
+ - [Semantic color contract](#semantic-color-contract)
10
+ - [PurgeTSS projects](#purgetss-projects)
11
+ - [Standard Alloy projects](#standard-alloy-projects)
12
+ - [Alloy build-time themes](#alloy-build-time-themes)
13
+ - [Runtime custom themes](#runtime-custom-themes)
14
+ - [Component rules](#component-rules)
15
+ - [Verification](#verification)
20
16
 
21
- <!-- TOC-END -->
17
+ ## Decision path
22
18
 
23
- ## Alloy theme system
19
+ 1. Detect `purgetss/` or `purgetss/config.cjs`.
20
+ 2. If detected, invoke the `purgetss` skill before writing any class or style.
21
+ 3. If not detected, use standard Alloy TSS and Titanium semantic colors.
22
+ 4. Use `Alloy.Globals` for a runtime palette only when the product requires
23
+ custom themes beyond the semantic light/dark/system model.
24
24
 
25
- Alloy has a built-in theme system via the `app/themes/` folder. Themes override views, styles, and assets without modifying originals.
25
+ Do not mix these paths accidentally. In particular, never teach a PurgeTSS
26
+ project to maintain the generated `app/styles/app.tss` by hand.
26
27
 
27
- ### Theme folder structure
28
+ ## Semantic color contract
28
29
 
29
- ```
30
- app/
31
- ├── themes/
32
- │ ├── light/ # Light theme (or default)
33
- │ │ ├── styles/
34
- │ │ │ └── app.tss # Overrides app/styles/app.tss
35
- │ │ └── assets/
36
- │ │ └── images/
37
- │ │ └── logo.png # Overrides app/assets/images/logo.png
38
- │ └── dark/ # Dark theme
39
- │ ├── styles/
40
- │ │ └── app.tss # Dark overrides
41
- │ └── assets/
42
- │ └── images/
43
- │ └── logo.png # Dark version of logo
44
- ├── styles/
45
- │ └── app.tss # Base styles (used when no theme active)
46
- ├── config.json
47
- ```
30
+ Name colors by purpose rather than appearance. A practical design-system
31
+ contract includes:
32
+
33
+ - app background and elevated surface;
34
+ - primary and secondary text;
35
+ - border/divider;
36
+ - accent and text on accent;
37
+ - modal overlay;
38
+ - success, warning, error, and their readable foreground colors;
39
+ - disabled surface/text where needed.
48
40
 
49
- ### Activating a theme
41
+ Define light and dark values in:
50
42
 
51
- Set the theme in `config.json`:
43
+ - Alloy: `app/assets/semantic.colors.json`
44
+ - Classic: `Resources/semantic.colors.json`
45
+
46
+ Example:
52
47
 
53
48
  ```json
54
49
  {
55
- "global": {
56
- "theme": "light"
50
+ "surfaceColor": {
51
+ "light": "#FFFFFF",
52
+ "dark": "#1E293B"
57
53
  },
58
- "env:development": {
59
- "theme": "light"
60
- }
61
- }
62
- ```
63
-
64
- > **⚠️ Theme is compile-time only**
65
- > Alloy themes are applied at **build time**. You cannot switch themes at runtime using this system alone. For runtime switching, use the Dynamic Theming approach below.
66
-
67
- ### Theme cascade
68
-
69
- When a theme is active, Alloy merges files in this order:
70
- 1. `app/styles/app.tss` (base)
71
- 2. `app/themes/<theme>/styles/app.tss` (theme override)
72
- 3. `app/styles/<controller>.tss` (controller-specific)
73
- 4. `app/themes/<theme>/styles/<controller>.tss` (theme + controller override)
74
-
75
- Theme files **merge** with base files , you only need to include properties you want to override.
76
-
77
- ---
78
-
79
- ## Dynamic theming with Alloy.Globals
80
-
81
- For runtime theme switching (including Dark Mode), use `Alloy.Globals` as a centralized color palette.
82
-
83
- ### Step 1: define color palette
84
-
85
- ```javascript
86
- // alloy.js
87
- const themes = {
88
- light: {
89
- bg: '#ffffff',
90
- bgSecondary: '#f3f4f6',
91
- text: '#111827',
92
- textSecondary: '#6b7280',
93
- primary: '#2563eb',
94
- primaryText: '#ffffff',
95
- border: '#e5e7eb',
96
- card: '#ffffff',
97
- danger: '#ef4444',
98
- success: '#22c55e'
54
+ "textColor": {
55
+ "light": "#111827",
56
+ "dark": "#F1F5F9"
99
57
  },
100
- dark: {
101
- bg: '#111827',
102
- bgSecondary: '#1f2937',
103
- text: '#f9fafb',
104
- textSecondary: '#9ca3af',
105
- primary: '#3b82f6',
106
- primaryText: '#ffffff',
107
- border: '#374151',
108
- card: '#1f2937',
109
- danger: '#f87171',
110
- success: '#4ade80'
58
+ "overlayColor": {
59
+ "light": "#00000080",
60
+ "dark": "#000000CC"
111
61
  }
112
62
  }
113
-
114
- // Load saved preference or default to light
115
- const savedTheme = Ti.App.Properties.getString('app:theme', 'light')
116
- Alloy.Globals.theme = themes[savedTheme]
117
- Alloy.Globals.themeName = savedTheme
118
63
  ```
119
64
 
120
- ### Step 2: use in TSS with Alloy.Globals
65
+ Titanium resolves a semantic color name when a color-accepting property uses
66
+ that name. Keep component APIs tone-based (`neutral`, `success`, `warning`,
67
+ `error`) and map tones to semantic roles inside the visual layer. Do not pass
68
+ raw brand hex values through business controllers.
121
69
 
122
- ```tss
123
- /* app.tss - Global base styles using theme colors */
124
- "Window": {
125
- backgroundColor: Alloy.Globals.theme.bg
126
- }
70
+ ## PurgeTSS projects
127
71
 
128
- "Label": {
129
- color: Alloy.Globals.theme.text
130
- }
72
+ The `purgetss` skill is the source of truth for supported classes, configuration,
73
+ appearance behavior, and commands. Apply these integration rules:
131
74
 
132
- ".card": {
133
- backgroundColor: Alloy.Globals.theme.card,
134
- borderWidth: 1,
135
- borderColor: Alloy.Globals.theme.border,
136
- borderRadius: 12
137
- }
75
+ - Map semantic color names under `theme.extend.colors` in
76
+ `purgetss/config.cjs`.
77
+ - Use verified utility classes in Alloy XML.
78
+ - Configure `options.widgets: true` when Widgets contain utility classes.
79
+ - Prefer `config.cjs` for reusable custom rules.
80
+ - Treat `app/styles/app.tss` as generated output and never edit it.
81
+ - Keep only truly specific properties in Widget/controller TSS when no verified
82
+ utility exists.
83
+ - Use the PurgeTSS Appearance module for light/dark/system selection instead of
84
+ creating a competing mode service.
85
+ - Read the current PurgeTSS references before naming any utility. Similarity to
86
+ Tailwind is not evidence that a class exists.
138
87
 
139
- ".btn-primary": {
140
- backgroundColor: Alloy.Globals.theme.primary,
141
- color: Alloy.Globals.theme.primaryText,
142
- height: 48,
143
- borderRadius: 8,
144
- font: { fontWeight: 'bold' }
145
- }
146
-
147
- ".text-secondary": {
148
- color: Alloy.Globals.theme.textSecondary
149
- }
150
-
151
- ".bg-secondary": {
152
- backgroundColor: Alloy.Globals.theme.bgSecondary
153
- }
154
- ```
155
-
156
- ### Step 3: theme switching service
88
+ Conceptual mapping only; verify the actual PurgeTSS syntax before use:
157
89
 
158
90
  ```javascript
159
- // lib/services/themeService.js
160
- const EventBus = require('services/eventBus')
161
-
162
- const themes = {
163
- light: { /* ... same as alloy.js ... */ },
164
- dark: { /* ... same as alloy.js ... */ }
165
- }
166
-
167
- exports.ThemeService = {
168
- getCurrentTheme() {
169
- return Alloy.Globals.themeName
170
- },
171
-
172
- setTheme(name) {
173
- if (!themes[name]) return
174
-
175
- Alloy.Globals.theme = themes[name]
176
- Alloy.Globals.themeName = name
177
- Ti.App.Properties.setString('app:theme', name)
178
-
179
- // Notify all listeners
180
- EventBus.trigger('theme:changed', { theme: name })
181
- },
182
-
183
- toggle() {
184
- const next = Alloy.Globals.themeName === 'light' ? 'dark' : 'light'
185
- this.setTheme(next)
186
- },
187
-
188
- // Follow system Dark Mode (iOS 13+, Android 10+)
189
- followSystem() {
190
- if (Ti.UI.semanticColorType !== undefined) {
191
- const isDark = Ti.UI.semanticColorType === Ti.UI.SEMANTIC_COLOR_TYPE_DARK
192
- this.setTheme(isDark ? 'dark' : 'light')
91
+ // purgetss/config.cjs
92
+ module.exports = {
93
+ theme: {
94
+ extend: {
95
+ colors: {
96
+ surface: 'surfaceColor',
97
+ 'on-surface': 'textColor',
98
+ overlay: 'overlayColor'
99
+ }
193
100
  }
194
101
  }
195
102
  }
196
103
  ```
197
104
 
198
- ### Step 4: respond to theme changes in controllers
199
-
200
- > **⚠️ Theme change requires UI rebuild**
201
- > Since TSS is applied at controller creation time, changing `Alloy.Globals.theme` does NOT retroactively update already-rendered views. You must manually update visible elements or restart the root controller.
202
-
203
- **Approach A: Rebuild root controller (simplest)**
204
-
205
- ```javascript
206
- // controllers/settings.js
207
- const { ThemeService } = require('services/themeService')
208
-
209
- function onToggleTheme() {
210
- ThemeService.toggle()
211
-
212
- // Restart app from root controller
213
- const root = Alloy.createController('index')
214
- root.getView().open()
215
-
216
- // Close current window tree
217
- $.getView().close()
218
- }
219
- ```
220
-
221
- **Approach B: Update visible elements in-place (smoother)**
222
-
223
- ```javascript
224
- // controllers/settings.js
225
- const EventBus = require('services/eventBus')
226
- const { ThemeService } = require('services/themeService')
227
-
228
- function init() {
229
- EventBus.on('theme:changed', applyTheme)
230
- }
231
-
232
- function applyTheme() {
233
- const t = Alloy.Globals.theme
234
-
235
- $.settingsWindow.applyProperties({ backgroundColor: t.bg })
236
- $.titleLabel.applyProperties({ color: t.text })
237
- $.themeToggle.applyProperties({ backgroundColor: t.bgSecondary })
238
- }
239
-
240
- function onToggleTheme() {
241
- ThemeService.toggle()
242
- }
243
-
244
- function cleanup() {
245
- EventBus.off('theme:changed', applyTheme)
246
- $.destroy()
247
- }
248
-
249
- $.cleanup = cleanup
250
- ```
251
-
252
- ---
253
-
254
- ## System dark mode detection
255
-
256
- ### iOS 13+ / Android 10+
257
-
258
- ```javascript
259
- // lib/services/themeService.js (add to existing service)
260
-
261
- exports.ThemeService = {
262
- // ... existing methods ...
263
-
264
- // Initialize system theme listener
265
- initSystemListener() {
266
- // Detect current system theme at startup
267
- this.followSystem()
268
-
269
- // Re-check on resume (user may have changed system Dark Mode while app was backgrounded)
270
- // Note: Titanium does not fire a dedicated dark-mode-change event.
271
- // `Ti.UI.semanticColorType` is the correct way to check the current setting.
272
- Ti.App.addEventListener('resumed', () => {
273
- this.followSystem()
274
- })
275
- }
276
- }
277
- ```
278
-
279
- ### Semantic colors (Cross-Platform)
105
+ After generation, inspect the final `app.tss` section for unused or unsupported
106
+ classes. A clean build that still lists a new unsupported class is not complete.
280
107
 
281
- Titanium provides a cross-platform semantic colors API for Dark Mode support. Define colors in a `semantic.colors.json` file:
108
+ ## Standard Alloy projects
282
109
 
283
- **File location:**
284
- - **Classic apps**: `Resources/semantic.colors.json`
285
- - **Alloy apps**: `app/assets/semantic.colors.json`
110
+ Without PurgeTSS:
286
111
 
287
- ```json
288
- {
289
- "textColor": {
290
- "dark": {
291
- "color": "#ff85e2",
292
- "alpha": "50"
293
- },
294
- "light": "#ff1f1f"
295
- },
296
- "backgroundColor": {
297
- "dark": "#1a1a2e",
298
- "light": "#ffffff"
299
- },
300
- "primaryColor": {
301
- "dark": {
302
- "color": "#4ade80",
303
- "alpha": "100"
304
- },
305
- "light": {
306
- "color": "#22c55e",
307
- "alpha": "100"
308
- }
309
- }
310
- }
311
- ```
112
+ - keep global selectors in manually maintained `app/styles/app.tss`;
113
+ - keep screen-specific selectors in `app/styles/<controller>.tss`;
114
+ - keep Widget-specific properties in `app/widgets/<id>/styles/widget.tss`;
115
+ - reference semantic color names from TSS or controller properties;
116
+ - use platform and form-factor modifiers for platform-specific properties.
312
117
 
313
- **Using semantic colors:**
118
+ Example:
314
119
 
315
120
  ```tss
316
- /* Reference via Ti.UI.fetchSemanticColor() */
317
- "#label": {
318
- color: Ti.UI.fetchSemanticColor('textColor')
121
+ "Window": {
122
+ backgroundColor: "backgroundColor"
319
123
  }
320
124
 
321
- "#container": {
322
- backgroundColor: Ti.UI.fetchSemanticColor('backgroundColor')
125
+ ".card": {
126
+ backgroundColor: "surfaceColor",
127
+ borderColor: "borderColor",
128
+ borderWidth: 1
323
129
  }
324
- ```
325
130
 
326
- ```javascript
327
- // Or use color names directly as property values
328
- $.label.color = 'textColor';
329
- $.container.backgroundColor = 'backgroundColor';
131
+ ".bodyText": {
132
+ color: "textColor"
133
+ }
330
134
  ```
331
135
 
332
- **How it works:**
333
- - On iOS 13+ it uses the native system method that checks the user's system-wide Dark Mode setting
334
- - On all other platforms it checks `Ti.UI.semanticColorType` and returns the correct color variant
335
- - Alpha can be set from 0.0-100.0 (integer or float)
336
- - Light values can use hex with alpha via ARGB/AARRGGBB format
136
+ Do not copy a large utility system into manual TSS. Create only the small set of
137
+ selectors the app actually uses.
337
138
 
338
- > **💡 When to use Semantic Colors vs Alloy.Globals**
339
- > - **Semantic Colors**: Built-in Dark Mode support, automatic switching, simpler setup. Works cross-platform.
340
- > - **Alloy.Globals palette**: More control over when/how themes switch, easier to add custom themes beyond light/dark, runtime theme changes with UI rebuild.
139
+ ## Alloy build-time themes
341
140
 
342
- ---
141
+ Alloy's `app/themes/<name>/` folders can override views, styles, and assets at
142
+ build time. Select a theme through `app/config.json`. This is appropriate for
143
+ separate branded builds or compile-time variants.
343
144
 
344
- ## Reusable style classes pattern
145
+ Do not describe an Alloy build-time theme as runtime light/dark switching.
146
+ Semantic colors are the default for appearance changes while the app runs.
345
147
 
346
- Define a library of reusable TSS classes in `app.tss` for consistency across the app:
148
+ ## Runtime custom themes
347
149
 
348
- ```tss
349
- /* app.tss - Design System */
350
-
351
- /* === Layout === */
352
- ".row": { layout: 'horizontal', width: Ti.UI.FILL, height: Ti.UI.SIZE }
353
- ".col": { layout: 'vertical', width: Ti.UI.FILL, height: Ti.UI.SIZE }
354
- ".centered": { width: Ti.UI.SIZE, height: Ti.UI.SIZE }
355
-
356
- /* === Cards === */
357
- ".card": {
358
- backgroundColor: Alloy.Globals.theme.card,
359
- borderRadius: 12,
360
- borderWidth: 1,
361
- borderColor: Alloy.Globals.theme.border,
362
- width: Ti.UI.FILL,
363
- height: Ti.UI.SIZE
364
- }
365
-
366
- /* === Buttons === */
367
- ".btn-primary": {
368
- backgroundColor: Alloy.Globals.theme.primary,
369
- color: Alloy.Globals.theme.primaryText,
370
- height: 48,
371
- width: Ti.UI.FILL,
372
- borderRadius: 8,
373
- font: { fontSize: 16, fontWeight: 'bold' }
374
- }
150
+ Use a centralized runtime palette or theme service only when semantic
151
+ light/dark/system colors cannot model the requirement, such as multiple user
152
+ selectable brand themes.
375
153
 
376
- ".btn-secondary": {
377
- backgroundColor: 'transparent',
378
- color: Alloy.Globals.theme.primary,
379
- height: 48,
380
- width: Ti.UI.FILL,
381
- borderRadius: 8,
382
- borderWidth: 1,
383
- borderColor: Alloy.Globals.theme.primary,
384
- font: { fontSize: 16, fontWeight: 'bold' }
385
- }
154
+ If that exception applies:
386
155
 
387
- ".btn-danger": {
388
- backgroundColor: Alloy.Globals.theme.danger,
389
- color: '#ffffff',
390
- height: 48,
391
- width: Ti.UI.FILL,
392
- borderRadius: 8,
393
- font: { fontSize: 16, fontWeight: 'bold' }
394
- }
156
+ - keep the selected theme in one service;
157
+ - expose semantic roles, not control IDs;
158
+ - document how already-created proxies receive updates;
159
+ - remove all listeners in controller cleanup;
160
+ - avoid rebuilding the entire window tree unless the app has measured and
161
+ accepted that tradeoff.
395
162
 
396
- /* === Typography === */
397
- ".text-title": { font: { fontSize: 24, fontWeight: 'bold' }, color: Alloy.Globals.theme.text }
398
- ".text-subtitle": { font: { fontSize: 18, fontWeight: 'semibold' }, color: Alloy.Globals.theme.text }
399
- ".text-body": { font: { fontSize: 16 }, color: Alloy.Globals.theme.text }
400
- ".text-caption": { font: { fontSize: 14 }, color: Alloy.Globals.theme.textSecondary }
401
- ".text-small": { font: { fontSize: 12 }, color: Alloy.Globals.theme.textSecondary }
402
-
403
- /* === Inputs === */
404
- ".input": {
405
- height: 48,
406
- width: Ti.UI.FILL,
407
- borderRadius: 8,
408
- borderWidth: 1,
409
- borderColor: Alloy.Globals.theme.border,
410
- backgroundColor: Alloy.Globals.theme.bg,
411
- color: Alloy.Globals.theme.text,
412
- paddingLeft: 12,
413
- font: { fontSize: 16 }
414
- }
163
+ `Alloy.Globals` is a legacy/advanced interoperability option, not the first
164
+ recommendation for ordinary dark mode.
415
165
 
416
- /* === Spacing (applied via class in XML) === */
417
- ".mt-sm": { top: 8 }
418
- ".mt-md": { top: 16 }
419
- ".mt-lg": { top: 24 }
420
- ".mt-xl": { top: 32 }
421
- ".mx-md": { left: 16, right: 16 }
422
- ".mx-lg": { left: 24, right: 24 }
423
- ```
166
+ ## Component rules
424
167
 
425
- ### Using style classes in views
168
+ - Components consume semantic roles owned by the host app.
169
+ - Widgets own anatomy and behavior, not the product palette.
170
+ - Localized strings and business callbacks come from the host.
171
+ - Destructive styling must remain distinguishable in both appearances.
172
+ - Overlays, focus indicators, disabled states, and text must retain usable
173
+ contrast in light, dark, and system modes.
174
+ - Do not create separate light/dark controller branches when a semantic color
175
+ can express the difference.
426
176
 
427
- ```xml
428
- <Alloy>
429
- <Window>
430
- <View class="col mx-md">
431
- <Label class="text-title mt-lg" text="L('settings')" />
177
+ ## Verification
432
178
 
433
- <View class="card mt-md">
434
- <Label class="text-body mx-md mt-md" text="L('appearance')" />
435
- <Button class="btn-secondary mx-md mt-md" title="L('toggle_theme')" onClick="onToggleTheme" />
436
- </View>
179
+ For every supported platform and form factor:
437
180
 
438
- <Button class="btn-danger mt-xl mx-md" title="L('logout')" onClick="onLogout" />
439
- </View>
440
- </Window>
441
- </Alloy>
442
- ```
181
+ 1. test light, dark, and follow-system modes;
182
+ 2. change appearance while the relevant window remains open;
183
+ 3. inspect dialogs, sheets, Snackbars, disabled controls, and overlays;
184
+ 4. test localized long text and accessibility focus;
185
+ 5. run PurgeTSS generation when detected and resolve unsupported classes;
186
+ 6. compile and build both platform targets before claiming completion.
443
187
 
444
- This approach gives you:
445
- - Consistent spacing and typography across the entire app
446
- - Theme-aware colors that change with `Alloy.Globals.theme`
447
- - Clean XML views without inline styles
448
- - A single place to update design decisions (`app.tss`)
188
+ For exact PurgeTSS behavior, load its current `semantic-colors.md` and
189
+ `appearance-module.md` references. For Titanium property availability, verify
190
+ against the current `ti-api` skill.