@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.
package/README.md CHANGED
@@ -333,6 +333,10 @@ When it activates:
333
333
  - Choosing data strategies (Models vs Collections)
334
334
  - Writing clean ES6+ code
335
335
  - Memory management and cleanup patterns
336
+ - Choosing a feedback surface (Snackbar, Dialog, Bottom Sheet) and deciding what stays native
337
+ - Designing reusable Alloy Widgets and their lifecycle contracts
338
+ - Adaptive layouts for tablets, foldables, and large screens
339
+ - Theming and dark mode
336
340
  - Performance optimization
337
341
  - Security patterns
338
342
  - Migrating legacy apps
@@ -343,13 +347,15 @@ Example prompts:
343
347
  "Create a user service that fetches data from an API."
344
348
  "What's the best pattern for navigation between screens?"
345
349
  "Prevent memory leaks in my controllers."
350
+ "Every confirmation in my app is an alertDialog — what should each one actually be?"
351
+ "Should this be a Widget or just a <Require>?"
346
352
  "Migrate classic Titanium code to modern Alloy patterns."
347
353
  "What's a clean way to organize services in an Alloy app?"
348
354
  ```
349
355
 
350
356
  Key features:
351
357
  - Quick decision matrix for common questions
352
- - 21 reference guides for deep dives
358
+ - 24 reference guides for deep dives
353
359
  - Delegates to specialized skills when needed
354
360
 
355
361
  ---
@@ -718,7 +724,7 @@ This pattern is documented across three TiTools skills: `ti-ui`, `ti-expert`, an
718
724
 
719
725
  | Skill | SKILL.md | References |
720
726
  | --------- | ----------------------------- | ----------------------------------------------------- |
721
- | ti-expert | Architecture + Implementation | 21 files (patterns, testing, security, etc.) |
727
+ | ti-expert | Architecture + Implementation | 24 files (patterns, feedback surfaces, testing, security, etc.) |
722
728
  | purgetss | Setup + Critical Rules | 33 files (grid, animations, icons, class-index, SVG pipeline, etc.) |
723
729
  | ti-ui | UI Rules + Platform Diffs | 14 files (layouts, lists, gestures, etc.) |
724
730
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maccesar/titools",
3
- "version": "4.3.0",
3
+ "version": "4.4.0",
4
4
  "description": "Titanium SDK skills and agents for AI coding assistants (Claude Code, Gemini CLI, Codex CLI)",
5
5
  "main": "lib/index.js",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: ti-expert
3
- description: "Use when designing, reviewing, analyzing, or examining Titanium SDK architecture and implementation (Alloy or Classic) — creating controllers/views/services, choosing models vs collections, implementing communication patterns, handling memory cleanup, testing, auditing code, migrating legacy apps, or building adaptive/responsive layouts for tablets, foldables, and large screens. AUTO-DETECT: If tiapp.xml exists, invoke BEFORE making architectural decisions, creating new controllers/views, or restructuring code. Titanium has its own patterns for navigation (NavigationWindow, TabGroup), memory management, and event handling that differ from web frameworks."
3
+ description: "Use when designing, reviewing, auditing, analyzing, or examining Titanium SDK architecture and implementation (Alloy or Classic) — creating controllers/views/services, choosing models vs collections, deciding between app-owned and system UI, designing reusable Widgets or feedback/modality patterns, hunting memory leaks and cleanup gaps, testing, migrating legacy apps, or building adaptive layouts for tablets, foldables, and large screens. AUTO-DETECT: If tiapp.xml exists, invoke BEFORE architectural decisions, new controllers/views, feedback components, or restructuring. Titanium navigation, memory, event, and modal patterns differ from web frameworks."
4
4
  argument-hint: "[architecture-topic]"
5
5
  allowed-tools: Read, Grep, Glob, Edit, Write, Bash(git *), Bash(node *)
6
6
  ---
@@ -9,6 +9,26 @@ 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
+ ## Required workflow
13
+
14
+ This file is an index. Read the references that govern the task before making a
15
+ recommendation or changing code.
16
+
17
+ | Task | Required reading |
18
+ | --- | --- |
19
+ | Architecture tier, folders, or boundaries | [Architecture tiers](references/architecture-tiers.md), [Alloy structure](references/alloy-structure.md) |
20
+ | Feedback, alerts, choices, or app-owned versus system UI | [Feedback surfaces](references/feedback-surfaces.md) |
21
+ | Reusable Snackbar, Dialog, Bottom Sheet, Widget, or `<Require>` | [Feedback Widget contracts](references/feedback-widget-contracts.md), [Alloy structure](references/alloy-structure.md) |
22
+ | Migrating native dialogs, Toasts, or local overlays | [Feedback migration](references/feedback-migration.md), [Migration patterns](references/migration-patterns.md) |
23
+ | Theme or dark mode | [Theming](references/theming.md); if PurgeTSS is detected, invoke the `purgetss` skill before styling |
24
+ | Tablet, foldable, large screen, or responsive behavior | [Adaptive layouts](references/adaptive-layouts.md) |
25
+ | Security architecture | [Security fundamentals](references/security-fundamentals.md), [Device security](references/security-device.md) |
26
+ | Testing or performance | Load the matching guide under Quality & Performance below |
27
+
28
+ Support recommendations with `[source: references/<file>.md]`. If a claim cannot
29
+ be verified from the loaded references or inspected project, prefix it with
30
+ `FROM_MEMORY (unverified):` and do not present it as a settled project fact.
31
+
12
32
  ## Project detection
13
33
 
14
34
  > **️ℹ️ Auto-detects Alloy vs Classic projects**
@@ -33,9 +53,10 @@ Practical architecture and implementation guidance for Titanium SDK apps (Alloy
33
53
  1. Architecture: define structure by technical type with flat folders (`lib/api`, `lib/services`, `lib/actions`, `lib/repositories`, `lib/helpers`)
34
54
  2. Data strategy: choose Models (SQLite) or Collections (API)
35
55
  3. Contracts: define I/O specs between layers
36
- 4. Implementation: write XML views and ES6+ controllers
37
- 5. Quality: testing, error handling, logging, performance
38
- 6. Cleanup: implement a `cleanup()` pattern for memory management
56
+ 4. UI ownership: classify app-owned feedback, screen state, and system-owned flows
57
+ 5. Implementation: write XML views and ES6+ controllers
58
+ 6. Quality: testing, error handling, logging, performance
59
+ 7. Cleanup: implement a `cleanup()` pattern for memory management
39
60
 
40
61
  ## Architectural Maturity Tiers
41
62
 
@@ -84,6 +105,12 @@ Detailed examples and full implementation samples are available in: [Architectur
84
105
 
85
106
  ## Titanium style sheets rules (low freedom)
86
107
 
108
+ Before styling, detect `purgetss/` or `purgetss/config.cjs`. If present, invoke
109
+ the `purgetss` skill, verify every utility, and never edit the generated
110
+ `app/styles/app.tss`. Without PurgeTSS, use standard Alloy TSS files. In either
111
+ case, keep behavior in XML/controllers and use semantic colors for app-owned
112
+ surfaces when the project supports them.
113
+
87
114
  > **🚨 Critical: platform-specific properties require modifiers**
88
115
  > Using `Ti.UI.iOS.*` or `Ti.UI.Android.*` properties without platform modifiers breaks cross-platform builds.
89
116
  >
@@ -138,7 +165,10 @@ For the complete reference with examples, see [Alloy builtins and globals](refer
138
165
  | Models or Collections? | Collections for API data, Models for SQLite persistence |
139
166
  | Ti.App.fireEvent or EventBus? | Always EventBus (Backbone.Events) |
140
167
  | Direct navigation or service? | Always Navigation service (auto cleanup) |
141
- | Inline styles or TSS files? | Always TSS files (per-controller + `app.tss` for global) |
168
+ | Snackbar, Dialog, or Bottom Sheet? | Classify urgency, persistence, reversibility, and choice first |
169
+ | Widget or `<Require>`? | Widget for a self-contained API/lifecycle; `<Require>` for app composition |
170
+ | App-owned or native UI? | Keep OS trust/protected flows native; style app-owned feedback |
171
+ | Inline styles or style system? | PurgeTSS utilities when detected; otherwise Alloy TSS |
142
172
 
143
173
  ## Reference guides (progressive disclosure)
144
174
 
@@ -155,6 +185,9 @@ For the complete reference with examples, see [Alloy builtins and globals](refer
155
185
  - [Alloy Builtins & Globals](references/alloy-builtins.md)
156
186
  - [Code Conventions](references/code-conventions.md)
157
187
  - [Controller Patterns](references/controller-patterns.md)
188
+ - [Feedback Surfaces & App-Owned UI](references/feedback-surfaces.md)
189
+ - [Feedback Widget Contracts](references/feedback-widget-contracts.md)
190
+ - [Feedback Migration](references/feedback-migration.md)
158
191
  - [Theming & Dark Mode](references/theming.md)
159
192
  - [Migration Patterns](references/migration-patterns.md)
160
193
  - [Examples Collection](references/examples.md)
@@ -75,13 +75,6 @@ Shared state and utilities accessible from any controller or lib file. Set in `a
75
75
  // alloy.js
76
76
  const Backbone = require('alloy/backbone')
77
77
 
78
- // --- Theme / Design Tokens ---
79
- Alloy.Globals.theme = {
80
- primary: '#2563eb',
81
- bg: '#ffffff',
82
- text: '#111827'
83
- }
84
-
85
78
  // --- Device Info (cached once) ---
86
79
  Alloy.Globals.isTablet = Alloy.isTablet
87
80
  Alloy.Globals.screenWidth = Ti.Platform.displayCaps.platformWidth
@@ -100,27 +93,23 @@ Alloy.Collections.notifications = new Backbone.Collection()
100
93
  ### Access from anywhere
101
94
 
102
95
  ```javascript
103
- // In any controller
104
- const theme = Alloy.Globals.theme
105
- $.label.color = theme.primary
106
-
107
- // In TSS (evaluated at compile time for controller creation)
108
- "#header": {
109
- backgroundColor: Alloy.Globals.theme.primary
110
- }
111
-
112
96
  // In XML with conditional binding
113
97
  // <View if="Alloy.Globals.isLoggedIn">
114
98
  ```
115
99
 
100
+ Do not use `Alloy.Globals` as the default dark-mode palette. Prefer Titanium
101
+ semantic colors; when PurgeTSS is detected, use its semantic color and Appearance
102
+ workflow. Reserve a global runtime palette for custom theme requirements that
103
+ semantic light/dark/system roles cannot model. See [Theming](theming.md).
104
+
116
105
  ### Globals vs services
117
106
 
118
- | Use `Alloy.Globals` for | Use a Service for |
119
- | ---------------------------------- | ----------------------------- |
120
- | Static config (theme, device info) | Business logic |
121
- | Simple flags (isLoggedIn) | Complex state with listeners |
122
- | Values set once in `alloy.js` | Values that change frequently |
123
- | Read-only data | Data with side effects |
107
+ | Use `Alloy.Globals` for | Use a Service for |
108
+ | ------------------------------------- | ----------------------------- |
109
+ | Immutable device/build facts | Business logic |
110
+ | Framework-level shared registries | Complex state with listeners |
111
+ | Values initialized once in `alloy.js` | Values that change frequently |
112
+ | Read-only interoperability data | Data with side effects |
124
113
 
125
114
  ---
126
115
 
@@ -30,7 +30,7 @@ app/
30
30
  │ ├── home.xml
31
31
  │ └── userProfile.xml
32
32
  ├── styles/ # TSS styles (one per view + global)
33
- │ ├── app.tss # Global application styles
33
+ │ ├── app.tss # Standard Alloy global styles or PurgeTSS generated output
34
34
  │ ├── index.tss # Styles for index view
35
35
  │ ├── home.tss
36
36
  │ └── userProfile.tss
@@ -59,7 +59,7 @@ app/
59
59
  │ └── providers/
60
60
  │ ├── containerProvider.js
61
61
  │ └── loggerProvider.js
62
- ├── widgets/ # Truly reusable components (used in 3+ places)
62
+ ├── widgets/ # Self-contained components with an API/lifecycle boundary
63
63
  │ └── customButton/
64
64
  ├── config.json # Alloy configuration
65
65
  └── alloy.js # Collections & Global services
@@ -188,7 +188,8 @@ api.getFrames()
188
188
  ## Controller rules
189
189
 
190
190
  **DO:**
191
- - Define styles in TSS files (per-controller + `app.tss` for global).
191
+ - Use verified PurgeTSS utilities when detected; otherwise define styles in
192
+ per-controller TSS and manually maintained global TSS.
192
193
  - Orchestrate view and model/collection interactions.
193
194
  - Handle UI events and delegate to services.
194
195
  - Format data for display (simple cases).
@@ -196,7 +197,7 @@ api.getFrames()
196
197
  - Keep `lib` modules flat and easy to locate.
197
198
 
198
199
  **DON'T:**
199
- - Use inline style attributes in XML (define in TSS files).
200
+ - Scatter visual properties inline when the active style system can express them.
200
201
  - Make direct API calls (use lib/api/ or lib/services/).
201
202
  - Contain heavy business logic.
202
203
  - Call native modules directly (use a service wrapper).
@@ -245,7 +246,10 @@ See [ControllerAutoCleanup.js](../assets/ControllerAutoCleanup.js) for the compl
245
246
 
246
247
  ## Widget structure
247
248
 
248
- Widgets are self-contained, reusable components used in 3+ places across the app.
249
+ Widgets are self-contained components with their own public API, state, assets,
250
+ and lifecycle. Choose one for a portable boundary, not after an arbitrary number
251
+ of usages. A first-use component with timers, modal state, and cleanup can merit
252
+ a Widget; repeated static styling does not.
249
253
 
250
254
  ```
251
255
  app/widgets/
@@ -255,7 +259,7 @@ app/widgets/
255
259
  ├── views/
256
260
  │ └── widget.xml # Main widget view
257
261
  ├── styles/
258
- │ └── widget.tss # Widget-specific styles
262
+ │ └── widget.tss # Optional component-specific styles
259
263
  └── widget.json # Widget manifest
260
264
  ```
261
265
 
@@ -276,6 +280,17 @@ app/widgets/
276
280
  }
277
281
  ```
278
282
 
283
+ Register the Widget in the app's `app/config.json`; the dependency key must
284
+ match the manifest `id`:
285
+
286
+ ```json
287
+ {
288
+ "dependencies": {
289
+ "com.app.loadingOverlay": "1.0.0"
290
+ }
291
+ }
292
+ ```
293
+
279
294
  ### Widget view (widget.xml)
280
295
  ```xml
281
296
  <Alloy>
@@ -290,10 +305,10 @@ app/widgets/
290
305
 
291
306
  ```tss
292
307
  /* styles/widget.tss */
293
- "#container": { width: Ti.UI.FILL, height: Ti.UI.FILL, visible: false, backgroundColor: 'rgba(0,0,0,0.5)' }
294
- "#box": { width: 128, height: 128, layout: 'vertical', borderRadius: 16, backgroundColor: '#fff' }
308
+ "#container": { width: Ti.UI.FILL, height: Ti.UI.FILL, visible: false, backgroundColor: 'overlayColor' }
309
+ "#box": { width: 128, height: 128, layout: 'vertical', borderRadius: 16, backgroundColor: 'surfaceColor' }
295
310
  "#spinner": { top: 24 }
296
- "#messageLabel": { left: 16, right: 16, top: 16, font: { fontSize: 14 }, color: '#4b5563' }
311
+ "#messageLabel": { left: 16, right: 16, top: 16, font: { fontSize: 14 }, color: 'textColor' }
297
312
  ```
298
313
 
299
314
  ### Widget controller (widget.js)
@@ -352,8 +367,12 @@ const loadData = () => {
352
367
  }
353
368
  ```
354
369
 
355
- > **💡 Widget Styles**
356
- > Widgets have their own `styles/widget.tss` file. Define all widget-specific styles there to keep them self-contained and portable.
370
+ > **Widget styles**
371
+ > In a standard Alloy project, keep component-specific properties in
372
+ > `styles/widget.tss` and consume semantic colors. If PurgeTSS is detected, load
373
+ > its skill first, enable Widget scanning, place verified utilities in XML, and
374
+ > reserve `widget.tss` for properties with no utility. Never edit the generated
375
+ > `app/styles/app.tss`.
357
376
 
358
377
  ### Widget ↔ controller communication
359
378
 
@@ -464,13 +483,18 @@ render()
464
483
 
465
484
  ### When to use widget vs require
466
485
 
467
- | Use `<Widget>` when | Use `<Require>` when |
468
- | ------------------------------------- | ------------------------------- |
469
- | Component is used in 3+ places | Component is used in 1-2 places |
470
- | Needs its own self-contained styles | Shares styles with parent |
471
- | Has public API (show/hide/setValue) | Just renders, no API needed |
472
- | Could be extracted to another project | Specific to this app |
473
- | Needs its own `widget.json` manifest | Lightweight, no manifest needed |
486
+ | Use `<Widget>` when | Use `<Require>` when |
487
+ | ------------------------------------- | ------------------------------------ |
488
+ | Owns a stable public API | Composes an app-specific controller |
489
+ | Owns state, timers, listeners, or cleanup | Shares the host's lifecycle |
490
+ | Has a portable design-system boundary | Shares the host's domain and styling |
491
+ | Could be extracted to another project | Has no independent package contract |
492
+ | Needs its own `widget.json` manifest | Remains lightweight and app-local |
493
+
494
+ Usage count is supporting evidence only. Also consider a screen-local XML state
495
+ for loading/empty/error content, a style class for appearance without behavior,
496
+ or native UI when the operating system owns the workflow. See
497
+ [Feedback Widget Contracts](feedback-widget-contracts.md).
474
498
 
475
499
  ## config.json reference
476
500
 
@@ -67,14 +67,22 @@
67
67
  ## 13. Using `Ti.UI.createNotification`
68
68
  **Symptom:** `Ti.UI.createNotification({ message: 'Hi' }).show()`
69
69
  **Problem:** This API doesn't exist in Titanium. Causes "invalid method" error.
70
- **Solution:** Use `Ti.UI.createAlertDialog` for simple messages, or create custom toast views.
70
+ **Solution:** Classify the feedback before choosing UI. Use the app's Snackbar
71
+ Widget for transient foreground feedback, inline/Banner state for persistent
72
+ conditions, and an app-owned Dialog only for blocking or critical information.
73
+ Use the supported platform notification APIs only for actual OS notifications.
74
+ See [Feedback Surfaces](feedback-surfaces.md).
71
75
 
72
76
  ## 14. Using nonexistent iOS share APIs
73
77
  **Symptom:** `Ti.UI.iOS.createActivityPopover` or `alloy/social` with wrong methods.
74
78
  **Problem:** These APIs either don't exist or have changed. Causes runtime errors.
75
79
  **Solution:**
76
- - iOS: Use `Ti.UI.iOS.createDocumentViewer` for files, or simple `Ti.UI.createOptionDialog` + `Ti.UI.Clipboard` for links
77
- - Android: Use `Ti.Android.createIntent` with ACTION_SEND
80
+ - Keep sharing system-owned. Use the currently supported Titanium share/document
81
+ API or native module for the target platform and verify its current contract
82
+ with the `ti-api` skill.
83
+ - Do not imitate a share sheet with an app-owned `OptionDialog`. A Bottom Sheet
84
+ may choose an app action such as an export format, but the OS share surface
85
+ owns the final destination.
78
86
 
79
87
  ## Community-Discovered Patterns
80
88
 
@@ -117,8 +125,8 @@ This applies to Windows inside both standalone NavigationWindow and TabGroup (wh
117
125
  | ----------------------- | ------------------------ | --------------------- |
118
126
  | `padding` on View | No padding on containers | Margins on children |
119
127
  | `layout: 'composite'` | Already default | Omit it |
120
- | Inline style attributes | Scattered styling | Define in TSS files |
128
+ | Inline style attributes | Scattered styling | PurgeTSS utilities when detected; otherwise TSS |
121
129
  | `lib/` prefix | lib/ is flattened | Use path without lib/ |
122
130
  | `$.index.open()` | Wrong ID reference | Use actual Window ID |
123
- | `createNotification` | API doesn't exist | `createAlertDialog` |
131
+ | `createNotification` | API doesn't exist | Classify feedback; Snackbar is the transient default |
124
132
  | `extendEdges` alone | Content behind nav bar | Add `autoAdjustScrollViewInsets: true` |
@@ -54,9 +54,15 @@ const name = user.name
54
54
 
55
55
  ## Styling with TSS
56
56
 
57
+ First detect `purgetss/` or `purgetss/config.cjs`. If present, invoke the
58
+ `purgetss` skill, use verified utilities, and do not edit generated
59
+ `app/styles/app.tss`. The manual TSS organization below applies when PurgeTSS is
60
+ not present or to specific properties for which that skill confirms no utility.
61
+
57
62
  ### TSS file organization
58
63
 
59
- Every controller can have a matching TSS file. Alloy also supports a global `app.tss`.
64
+ Every controller can have a matching TSS file. Standard Alloy also supports a
65
+ manually maintained global `app.tss`.
60
66
 
61
67
  ```
62
68
  app/styles/
@@ -95,7 +101,7 @@ app/styles/
95
101
  | Selector | Use for | Example |
96
102
  | ------------------- | --------------------------- | ---------------------- |
97
103
  | Element (`"Label"`) | App-wide defaults | Base font, color |
98
- | Class (`".card"`) | Reusable patterns (3+ uses) | Cards, buttons, inputs |
104
+ | Class (`".card"`) | Reusable visual pattern | Cards, buttons, inputs |
99
105
  | ID (`"#name"`) | Unique element positioning | Specific layout needs |
100
106
 
101
107
  ### Class selectors in XML
@@ -127,7 +133,7 @@ When an element has both an ID and classes, properties merge with ID taking prio
127
133
  <Button id="saveButton" class="btn-primary" title="L('save')" />
128
134
  ```
129
135
 
130
- ### Building a design system in app.tss
136
+ ### Building a standard Alloy design system in app.tss
131
137
 
132
138
  Define reusable style classes for consistency:
133
139
 
@@ -281,43 +281,42 @@ async function loadData() {
281
281
 
282
282
  ## Modal and dialog patterns
283
283
 
284
+ Declare the app-owned `dialog` and `bottomSheet` Widgets in the window XML.
285
+ Attach them after the host exists and destroy them from the controller cleanup.
286
+ Keep a modal Window for a distinct multi-step task, not for ordinary feedback.
287
+
284
288
  ```javascript
285
- // AlertDialog
289
+ function attachFeedback(hostWindow) {
290
+ $.dialog.attach(hostWindow)
291
+ $.bottomSheet.attach(hostWindow)
292
+ }
293
+
294
+ // Critical, irreversible confirmation
286
295
  function showConfirmDelete(itemName) {
287
- const dialog = Ti.UI.createAlertDialog({
296
+ $.dialog.show({
288
297
  title: L('confirm_delete'),
289
298
  message: String.format(L('delete_message'), itemName),
290
- buttonNames: [L('cancel'), L('delete')],
291
- cancel: 0,
292
- destructive: 1 // iOS: red button
293
- })
294
-
295
- dialog.addEventListener('click', (e) => {
296
- if (e.index === 1) {
297
- performDelete()
298
- }
299
+ cancelTitle: L('cancel'),
300
+ confirmTitle: L('delete'),
301
+ destructive: true,
302
+ tone: 'warning',
303
+ onConfirm: performDelete
299
304
  })
300
-
301
- dialog.show()
302
305
  }
303
306
 
304
- // OptionDialog (Action Sheet)
307
+ // Contextual choices
305
308
  function showSortOptions() {
306
- const options = [L('sort_name'), L('sort_date'), L('sort_price')]
309
+ const items = [
310
+ { id: 'name', title: L('sort_name') },
311
+ { id: 'date', title: L('sort_date') },
312
+ { id: 'price', title: L('sort_price') }
313
+ ]
307
314
 
308
- const dialog = Ti.UI.createOptionDialog({
315
+ $.bottomSheet.show({
309
316
  title: L('sort_by'),
310
- options: [...options, L('cancel')],
311
- cancel: options.length
312
- })
313
-
314
- dialog.addEventListener('click', (e) => {
315
- if (e.index < options.length) {
316
- applySortOption(e.index)
317
- }
317
+ items,
318
+ onSelect: item => applySortOption(item.id)
318
319
  })
319
-
320
- dialog.show()
321
320
  }
322
321
 
323
322
  // Modal Window
@@ -341,6 +340,14 @@ function openModal(route, params) {
341
340
 
342
341
  return controller
343
342
  }
343
+
344
+ function cleanup() {
345
+ $.dialog.destroy()
346
+ $.bottomSheet.destroy()
347
+ $.destroy()
348
+ }
349
+
350
+ $.cleanup = cleanup
344
351
  ```
345
352
 
346
353
  ## Tabgroup pattern
@@ -3,6 +3,7 @@
3
3
  <!-- TOC-START -->
4
4
  ## Contents
5
5
 
6
+ - [Reading these examples](#reading-these-examples)
6
7
  - [API client service](#api-client-service)
7
8
  - [Native module wrapper service](#native-module-wrapper-service)
8
9
  - [i18n helper](#i18n-helper)
@@ -17,6 +18,19 @@
17
18
 
18
19
  <!-- TOC-END -->
19
20
 
21
+ ## Reading these examples
22
+
23
+ Snippets that call `$.dialog`, `$.bottomSheet`, or `$.snackbar` expect the app to
24
+ own those surfaces as Alloy Widgets under `app/widgets/`. Their public API,
25
+ queueing, and cleanup rules live in [Feedback Widget Contracts](feedback-widget-contracts.md);
26
+ [Feedback Surfaces](feedback-surfaces.md) covers which surface a given event
27
+ deserves. The `<Widget>` declaration alone will not compile until those Widgets
28
+ exist and are registered in `app/config.json`.
29
+
30
+ Styles below carry literal hex values so each example reads on its own. Production
31
+ code should name semantic color roles instead, so light and dark resolve without a
32
+ second code path — see [Theming and dark mode](theming.md).
33
+
20
34
  ## API client service
21
35
  Standard logic for network requests.
22
36
 
@@ -350,6 +364,8 @@ $.cleanup = cleanup
350
364
  <ImageView id="emptyIcon" image="/images/search.png" />
351
365
  <Label id="emptyLabel" text="L('no_results')" />
352
366
  </View>
367
+
368
+ <Widget id="bottomSheet" src="com.example.ui.bottomsheet" />
353
369
  </Window>
354
370
  </Alloy>
355
371
  ```
@@ -375,6 +391,7 @@ let searchQuery = ''
375
391
  let debounceTimer = null
376
392
 
377
393
  function init() {
394
+ $.bottomSheet.attach($.contactsWindow)
378
395
  loadData()
379
396
 
380
397
  $.searchBar.addEventListener('cancel', clearSearch)
@@ -395,21 +412,23 @@ function clearSearch() {
395
412
  }
396
413
 
397
414
  function showFilters() {
398
- const dialog = Ti.UI.createOptionDialog({
415
+ const items = [
416
+ { id: 'all', title: L('all') },
417
+ { id: 'work', title: L('work') },
418
+ { id: 'personal', title: L('personal') },
419
+ { id: 'family', title: L('family') }
420
+ ]
421
+
422
+ $.bottomSheet.show({
399
423
  title: L('filter_by_category'),
400
- options: ['All', 'Work', 'Personal', 'Family', L('cancel')],
401
- cancel: 4
402
- })
403
-
404
- dialog.addEventListener('click', (e) => {
405
- if (e.index < 4) {
406
- activeFilters.category = e.index === 0 ? null : ['work', 'personal', 'family'][e.index - 1]
424
+ items,
425
+ selectedId: activeFilters.category || 'all',
426
+ onSelect: item => {
427
+ activeFilters.category = item.id === 'all' ? null : item.id
407
428
  updateFilterTags()
408
429
  applyFilters()
409
430
  }
410
431
  })
411
-
412
- dialog.show()
413
432
  }
414
433
 
415
434
  function applyFilters() {
@@ -465,6 +484,7 @@ function createFilterTag(label, onRemove) {
465
484
 
466
485
  function cleanup() {
467
486
  clearTimeout(debounceTimer)
487
+ $.bottomSheet.destroy()
468
488
  $.searchBar.removeEventListener('change', onSearchChange)
469
489
  $.destroy()
470
490
  }
@@ -530,6 +550,7 @@ $.cleanup = cleanup
530
550
  <Switch id="termsSwitch" />
531
551
  <Label id="termsLabel" text="L('accept_terms')" />
532
552
  </View>
553
+ <Label id="termsError" />
533
554
 
534
555
  <!-- Register Button -->
535
556
  <Button id="registerBtn"
@@ -544,6 +565,8 @@ $.cleanup = cleanup
544
565
  </View>
545
566
  </View>
546
567
  </ScrollView>
568
+
569
+ <Widget id="dialog" src="com.example.ui.dialog" />
547
570
  </Window>
548
571
  </Alloy>
549
572
  ```
@@ -570,6 +593,7 @@ $.cleanup = cleanup
570
593
  "#termsRow": { layout: 'horizontal', left: 16, right: 16, top: 24, width: Ti.UI.FILL }
571
594
  "#termsSwitch": { width: 48 }
572
595
  "#termsLabel": { left: 8 }
596
+ "#termsError": { left: 16, right: 16, top: 4, font: { fontSize: 12 }, color: '#ef4444', visible: false }
573
597
  "#registerBtn": { left: 16, right: 16, top: 24, height: 56, borderRadius: 12, backgroundColor: '#2563eb', color: '#fff', font: { fontWeight: 'bold' } }
574
598
  "#loginRow": { layout: 'horizontal', top: 16 }
575
599
  "#haveAccountLabel": { font: { fontSize: 14 }, color: '#4b5563' }
@@ -611,6 +635,8 @@ const validators = {
611
635
  }
612
636
 
613
637
  function init() {
638
+ $.dialog.attach($.registerWindow)
639
+
614
640
  // Field navigation
615
641
  $.nameField.addEventListener('return', () => $.emailField.focus())
616
642
  $.emailField.addEventListener('return', () => $.passwordField.focus())
@@ -658,16 +684,14 @@ function validateAll() {
658
684
  const emailValid = validateField('email')
659
685
  const passwordValid = validateField('password')
660
686
  const confirmValid = validateField('confirm')
687
+ const termsValid = $.termsSwitch.value
661
688
 
662
- if (!$.termsSwitch.value) {
663
- Ti.UI.createAlertDialog({
664
- title: L('error'),
665
- message: L('error_accept_terms')
666
- }).show()
667
- return false
668
- }
689
+ $.termsError.applyProperties({
690
+ text: termsValid ? '' : L('error_accept_terms'),
691
+ visible: !termsValid
692
+ })
669
693
 
670
- return nameValid && emailValid && passwordValid && confirmValid
694
+ return nameValid && emailValid && passwordValid && confirmValid && termsValid
671
695
  }
672
696
 
673
697
  async function onRegister() {
@@ -686,10 +710,12 @@ async function onRegister() {
686
710
  Navigation.replace('main')
687
711
 
688
712
  } catch (error) {
689
- Ti.UI.createAlertDialog({
713
+ $.dialog.show({
690
714
  title: L('error'),
691
- message: error.message
692
- }).show()
715
+ message: error.message,
716
+ tone: 'error',
717
+ confirmTitle: L('close')
718
+ })
693
719
  } finally {
694
720
  setLoading(false)
695
721
  }
@@ -707,6 +733,7 @@ function goToLogin() {
707
733
  }
708
734
 
709
735
  function cleanup() {
736
+ $.dialog.destroy()
710
737
  $.destroy()
711
738
  }
712
739