@maccesar/titools 2.0.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.
Files changed (120) hide show
  1. package/AGENTS-TEMPLATE.md +173 -0
  2. package/README.md +867 -0
  3. package/agents/ti-researcher.md +108 -0
  4. package/bin/titools.js +53 -0
  5. package/lib/commands/agents.js +126 -0
  6. package/lib/commands/install.js +188 -0
  7. package/lib/commands/uninstall.js +215 -0
  8. package/lib/commands/update.js +159 -0
  9. package/lib/config.js +119 -0
  10. package/lib/downloader.js +153 -0
  11. package/lib/installer.js +253 -0
  12. package/lib/platform.js +108 -0
  13. package/lib/symlink.js +142 -0
  14. package/lib/utils.js +270 -0
  15. package/package.json +67 -0
  16. package/skills/alloy-expert/SKILL.md +247 -0
  17. package/skills/alloy-expert/assets/ControllerAutoCleanup.js +182 -0
  18. package/skills/alloy-expert/references/alloy-structure.md +381 -0
  19. package/skills/alloy-expert/references/anti-patterns.md +133 -0
  20. package/skills/alloy-expert/references/code-conventions.md +469 -0
  21. package/skills/alloy-expert/references/contracts.md +280 -0
  22. package/skills/alloy-expert/references/controller-patterns.md +520 -0
  23. package/skills/alloy-expert/references/error-handling.md +484 -0
  24. package/skills/alloy-expert/references/examples.md +735 -0
  25. package/skills/alloy-expert/references/migration-patterns.md +298 -0
  26. package/skills/alloy-expert/references/patterns.md +448 -0
  27. package/skills/alloy-expert/references/performance-patterns.md +855 -0
  28. package/skills/alloy-expert/references/security-patterns.md +847 -0
  29. package/skills/alloy-expert/references/state-management.md +779 -0
  30. package/skills/alloy-expert/references/testing.md +872 -0
  31. package/skills/alloy-guides/SKILL.md +214 -0
  32. package/skills/alloy-guides/references/CLI_TASKS.md +243 -0
  33. package/skills/alloy-guides/references/CONCEPTS.md +191 -0
  34. package/skills/alloy-guides/references/CONTROLLERS.md +298 -0
  35. package/skills/alloy-guides/references/MODELS.md +1028 -0
  36. package/skills/alloy-guides/references/PURGETSS.md +56 -0
  37. package/skills/alloy-guides/references/VIEWS_DYNAMIC.md +242 -0
  38. package/skills/alloy-guides/references/VIEWS_STYLES.md +388 -0
  39. package/skills/alloy-guides/references/VIEWS_WITHOUT_CONTROLLERS.md +109 -0
  40. package/skills/alloy-guides/references/VIEWS_XML.md +558 -0
  41. package/skills/alloy-guides/references/WIDGETS.md +176 -0
  42. package/skills/alloy-howtos/SKILL.md +203 -0
  43. package/skills/alloy-howtos/references/best_practices.md +138 -0
  44. package/skills/alloy-howtos/references/cli_reference.md +253 -0
  45. package/skills/alloy-howtos/references/config_files.md +87 -0
  46. package/skills/alloy-howtos/references/custom_tags.md +147 -0
  47. package/skills/alloy-howtos/references/debugging_troubleshooting.md +101 -0
  48. package/skills/alloy-howtos/references/samples.md +167 -0
  49. package/skills/purgetss/SKILL.md +442 -0
  50. package/skills/purgetss/assets/purgetss.config.cjs +17 -0
  51. package/skills/purgetss/references/EXAMPLES.md +247 -0
  52. package/skills/purgetss/references/animation-system.md +1294 -0
  53. package/skills/purgetss/references/apply-directive.md +375 -0
  54. package/skills/purgetss/references/arbitrary-values.md +612 -0
  55. package/skills/purgetss/references/class-index.md +1350 -0
  56. package/skills/purgetss/references/cli-commands.md +948 -0
  57. package/skills/purgetss/references/configurable-properties.md +654 -0
  58. package/skills/purgetss/references/custom-rules.md +161 -0
  59. package/skills/purgetss/references/customization-deep-dive.md +722 -0
  60. package/skills/purgetss/references/dynamic-component-creation.md +489 -0
  61. package/skills/purgetss/references/grid-layout.md +455 -0
  62. package/skills/purgetss/references/icon-fonts.md +609 -0
  63. package/skills/purgetss/references/installation-setup.md +366 -0
  64. package/skills/purgetss/references/opacity-modifier.md +291 -0
  65. package/skills/purgetss/references/platform-modifiers.md +479 -0
  66. package/skills/purgetss/references/smart-mappings.md +42 -0
  67. package/skills/purgetss/references/titanium-resets.md +359 -0
  68. package/skills/purgetss/references/ui-ux-design.md +1526 -0
  69. package/skills/ti-guides/SKILL.md +94 -0
  70. package/skills/ti-guides/references/advanced-data-and-images.md +19 -0
  71. package/skills/ti-guides/references/alloy-cli-advanced.md +84 -0
  72. package/skills/ti-guides/references/alloy-data-mastery.md +29 -0
  73. package/skills/ti-guides/references/alloy-widgets-and-themes.md +19 -0
  74. package/skills/ti-guides/references/android-manifest.md +97 -0
  75. package/skills/ti-guides/references/app-distribution.md +258 -0
  76. package/skills/ti-guides/references/application-frameworks.md +377 -0
  77. package/skills/ti-guides/references/cli-reference.md +402 -0
  78. package/skills/ti-guides/references/coding-best-practices.md +102 -0
  79. package/skills/ti-guides/references/commonjs-advanced.md +134 -0
  80. package/skills/ti-guides/references/hello-world.md +100 -0
  81. package/skills/ti-guides/references/hyperloop-native-access.md +62 -0
  82. package/skills/ti-guides/references/javascript-primer.md +411 -0
  83. package/skills/ti-guides/references/reserved-words.md +36 -0
  84. package/skills/ti-guides/references/resources.md +183 -0
  85. package/skills/ti-guides/references/style-and-conventions.md +48 -0
  86. package/skills/ti-guides/references/tiapp-config.md +609 -0
  87. package/skills/ti-howtos/SKILL.md +174 -0
  88. package/skills/ti-howtos/references/android-platform-deep-dives.md +658 -0
  89. package/skills/ti-howtos/references/automation-fastlane-appium.md +95 -0
  90. package/skills/ti-howtos/references/buffer-codec-streams.md +140 -0
  91. package/skills/ti-howtos/references/cross-platform-development.md +348 -0
  92. package/skills/ti-howtos/references/debugging-profiling.md +543 -0
  93. package/skills/ti-howtos/references/extending-titanium.md +723 -0
  94. package/skills/ti-howtos/references/google-maps-v2.md +169 -0
  95. package/skills/ti-howtos/references/ios-map-kit.md +143 -0
  96. package/skills/ti-howtos/references/ios-platform-deep-dives.md +783 -0
  97. package/skills/ti-howtos/references/local-data-sources.md +301 -0
  98. package/skills/ti-howtos/references/location-and-maps.md +252 -0
  99. package/skills/ti-howtos/references/media-apis.md +210 -0
  100. package/skills/ti-howtos/references/notification-services.md +599 -0
  101. package/skills/ti-howtos/references/remote-data-sources.md +349 -0
  102. package/skills/ti-howtos/references/tutorials.md +502 -0
  103. package/skills/ti-howtos/references/using-modules.md +237 -0
  104. package/skills/ti-howtos/references/web-content-integration.md +307 -0
  105. package/skills/ti-howtos/references/webpack-build-pipeline.md +78 -0
  106. package/skills/ti-ui/SKILL.md +179 -0
  107. package/skills/ti-ui/references/accessibility-deep-dive.md +242 -0
  108. package/skills/ti-ui/references/animation-and-matrices.md +599 -0
  109. package/skills/ti-ui/references/application-structures.md +655 -0
  110. package/skills/ti-ui/references/custom-fonts-styling.md +579 -0
  111. package/skills/ti-ui/references/event-handling.md +393 -0
  112. package/skills/ti-ui/references/gestures.md +473 -0
  113. package/skills/ti-ui/references/icons-and-splash-screens.md +409 -0
  114. package/skills/ti-ui/references/layouts-and-positioning.md +462 -0
  115. package/skills/ti-ui/references/listviews-and-performance.md +619 -0
  116. package/skills/ti-ui/references/orientation.md +362 -0
  117. package/skills/ti-ui/references/platform-ui-android.md +635 -0
  118. package/skills/ti-ui/references/platform-ui-ios.md +469 -0
  119. package/skills/ti-ui/references/scrolling-views.md +252 -0
  120. package/skills/ti-ui/references/tableviews.md +568 -0
@@ -0,0 +1,520 @@
1
+ # PurgeTSS Implementation Patterns
2
+
3
+ ## Standard PurgeTSS View Template
4
+
5
+ ```xml
6
+ <!-- views/user/card.xml -->
7
+ <Alloy>
8
+ <View class="border-(1) m-2 rounded-xl border-gray-200 bg-white shadow-md">
9
+ <View class="horizontal m-3 w-screen">
10
+ <Label class="fa-solid fa-user-circle text-4xl text-blue-500" />
11
+ <View class="vertical ml-3">
12
+ <Label id="name" class="text-lg font-bold text-gray-900" />
13
+ <Label id="email" class="text-sm text-gray-500" />
14
+ </View>
15
+ </View>
16
+ <Button class="mx-3 mb-3 mt-4 h-10 w-screen rounded-md bg-blue-600 font-medium text-white"
17
+ title="L('view_profile')"
18
+ onClick="onViewProfile"
19
+ />
20
+ </View>
21
+ </Alloy>
22
+ ```
23
+
24
+ **PurgeTSS Layout Rules:**
25
+ - Use `horizontal` (not `flex-row`) for left-to-right
26
+ - Use `vertical` (not `flex-col`) for top-to-bottom
27
+ - Omit layout class for composite (absolute positioning)
28
+ - Use `m-*` on children instead of `p-*` on parent
29
+ - Use `border-(1)` with parentheses for arbitrary values
30
+
31
+ ## Animation Component Usage
32
+
33
+ Always prefer the PurgeTSS Animation component over manual matrix calculations.
34
+
35
+ ```xml
36
+ <Animation id="myAnim" module="purgetss.ui" class="close:opacity-0 duration-500 open:opacity-100" />
37
+ ```
38
+
39
+ ```javascript
40
+ // controllers/user/card.js
41
+ function show() {
42
+ $.myAnim.open($.container)
43
+ }
44
+
45
+ function hide() {
46
+ $.myAnim.close($.container, () => {
47
+ $.container.applyProperties({ visible: false })
48
+ })
49
+ }
50
+ ```
51
+
52
+ ## Draggable Method
53
+ Use the `draggable` method to convert views into draggable elements.
54
+
55
+ ```javascript
56
+ $.myAnim.draggable([$.red, $.green, $.blue])
57
+ ```
58
+
59
+ ## Dynamic Styling with Classes
60
+
61
+ To change styles dynamically, use `classes` instead of individual property updates.
62
+
63
+ ```javascript
64
+ function setStatus(isActive) {
65
+ $.statusLabel.applyProperties({
66
+ classes: isActive ? ['text-green-500'] : ['text-red-500'],
67
+ text: isActive ? L('active') : L('inactive')
68
+ })
69
+ }
70
+ ```
71
+
72
+ ## Grid System
73
+
74
+ Use PurgeTSS percentage-based widths for responsive layouts.
75
+
76
+ ```xml
77
+ <!-- Horizontal layout with percentage widths -->
78
+ <View class="horizontal w-screen">
79
+ <View class="h-20 w-8/12 bg-red-100" />
80
+ <View class="h-20 w-4/12 bg-blue-100" />
81
+ </View>
82
+
83
+ <!-- Or use the grid-cols system -->
84
+ <View class="w-screen grid-cols-3 gap-2">
85
+ <View class="h-20 bg-red-100" />
86
+ <View class="h-20 bg-blue-100" />
87
+ <View class="h-20 bg-green-100" />
88
+ </View>
89
+ ```
90
+
91
+ ## Controller Lifecycle Patterns
92
+
93
+ Show init, focus, blur, close lifecycle:
94
+ ```javascript
95
+ // controllers/user/profile.js
96
+ function init() {
97
+ // Called once when controller is created
98
+ loadUserData()
99
+ }
100
+
101
+ // Handle window focus (returning to this screen)
102
+ $.getView().addEventListener('focus', () => {
103
+ // Refresh data when screen becomes visible
104
+ refreshIfNeeded()
105
+ })
106
+
107
+ // Handle window blur (leaving this screen)
108
+ $.getView().addEventListener('blur', () => {
109
+ // Pause animations, stop timers
110
+ pauseAutoRefresh()
111
+ })
112
+
113
+ // Handle window close
114
+ $.getView().addEventListener('close', () => {
115
+ // Cleanup is called automatically by Navigation service
116
+ })
117
+
118
+ function cleanup() {
119
+ $.getView().removeEventListener('focus', onFocus)
120
+ $.getView().removeEventListener('blur', onBlur)
121
+ $.destroy()
122
+ }
123
+
124
+ $.cleanup = cleanup
125
+ ```
126
+
127
+ ## Form Handling Patterns
128
+
129
+ ```xml
130
+ <!-- views/user/edit.xml -->
131
+ <Alloy>
132
+ <Window class="vertical bg-white">
133
+ <ScrollView class="wh-screen vertical">
134
+ <View class="vertical mt-4 h-auto w-screen">
135
+ <Label class="ml-4 text-sm text-gray-500" text="L('name_label')" />
136
+ <TextField id="nameField"
137
+ class="border-(1) return-key-type-next mx-4 mt-1 h-12 w-screen rounded-lg border-gray-300 bg-gray-50 px-3"
138
+ hintText="L('name_hint')"
139
+ onReturn="focusNextField"
140
+ />
141
+
142
+ <Label class="ml-4 mt-4 text-sm text-gray-500" text="L('email_label')" />
143
+ <TextField id="emailField"
144
+ class="border-(1) keyboard-type-email return-key-type-done mx-4 mt-1 h-12 w-screen rounded-lg border-gray-300 bg-gray-50 px-3"
145
+ hintText="L('email_hint')"
146
+ onReturn="submitForm"
147
+ />
148
+
149
+ <Label id="errorLabel" class="ml-4 mt-2 hidden text-sm text-red-500" />
150
+
151
+ <Button id="submitBtn"
152
+ class="bg-primary mx-4 mt-6 h-14 w-screen rounded-xl font-bold text-white"
153
+ title="L('save')"
154
+ onClick="submitForm"
155
+ />
156
+ </View>
157
+ </ScrollView>
158
+ </Window>
159
+ </Alloy>
160
+ ```
161
+
162
+ ```javascript
163
+ // controllers/user/edit.js
164
+ const { validateEmail, validateRequired } = require('lib/helpers/validator')
165
+ const { userService } = require('lib/services/userService')
166
+
167
+ const fields = ['nameField', 'emailField']
168
+ let currentFieldIndex = 0
169
+
170
+ function focusNextField() {
171
+ currentFieldIndex++
172
+ if (currentFieldIndex < fields.length) {
173
+ $[fields[currentFieldIndex]].focus()
174
+ } else {
175
+ submitForm()
176
+ }
177
+ }
178
+
179
+ async function submitForm() {
180
+ // Hide keyboard
181
+ Ti.UI.Android?.hideSoftKeyboard() || $.nameField.blur()
182
+
183
+ // Clear previous errors
184
+ $.errorLabel.applyProperties({ visible: false, text: '' })
185
+
186
+ try {
187
+ // Validate
188
+ const name = validateRequired($.nameField.value, 'Name')
189
+ const email = validateEmail($.emailField.value)
190
+
191
+ // Show loading
192
+ setLoading(true)
193
+
194
+ // Submit
195
+ await userService.updateProfile({ name, email })
196
+
197
+ // Success - close and notify
198
+ $.trigger('user:saved')
199
+ Navigation.back()
200
+
201
+ } catch (error) {
202
+ $.errorLabel.applyProperties({
203
+ text: error.message,
204
+ visible: true
205
+ })
206
+ } finally {
207
+ setLoading(false)
208
+ }
209
+ }
210
+
211
+ function setLoading(isLoading) {
212
+ $.submitBtn.applyProperties({
213
+ enabled: !isLoading,
214
+ title: isLoading ? L('saving') : L('save')
215
+ })
216
+ }
217
+ ```
218
+
219
+ ## Loading State Patterns
220
+
221
+ ```xml
222
+ <!-- Skeleton loading pattern -->
223
+ <View id="loadingState" class="wh-screen vertical">
224
+ <View class="mx-4 mt-4 h-20 w-screen animate-pulse rounded-lg bg-gray-200" />
225
+ <View class="mx-4 mt-4 h-4 w-8/12 animate-pulse rounded bg-gray-200" />
226
+ <View class="mx-4 mt-2 h-4 w-6/12 animate-pulse rounded bg-gray-200" />
227
+ </View>
228
+
229
+ <View id="contentState" class="wh-screen hidden">
230
+ <!-- Actual content here -->
231
+ </View>
232
+
233
+ <View id="errorState" class="wh-screen vertical hidden">
234
+ <Label class="fa-solid fa-exclamation-circle mt-20 text-6xl text-gray-400" />
235
+ <Label class="mt-4 text-lg text-gray-500" text="L('error_loading')" />
236
+ <Button class="bg-primary mt-4 h-10 rounded-lg px-6 text-white"
237
+ title="L('retry')"
238
+ onClick="loadData"
239
+ />
240
+ </View>
241
+ ```
242
+
243
+ ```javascript
244
+ // State management helper
245
+ function setState(state) {
246
+ const states = ['loadingState', 'contentState', 'errorState']
247
+ states.forEach(s => {
248
+ $[s].visible = s === state
249
+ })
250
+ }
251
+
252
+ async function loadData() {
253
+ setState('loadingState')
254
+
255
+ try {
256
+ const data = await api.fetchData()
257
+ renderContent(data)
258
+ setState('contentState')
259
+ } catch (error) {
260
+ setState('errorState')
261
+ }
262
+ }
263
+ ```
264
+
265
+ ## Modal and Dialog Patterns
266
+
267
+ ```javascript
268
+ // AlertDialog
269
+ function showConfirmDelete(itemName) {
270
+ const dialog = Ti.UI.createAlertDialog({
271
+ title: L('confirm_delete'),
272
+ message: String.format(L('delete_message'), itemName),
273
+ buttonNames: [L('cancel'), L('delete')],
274
+ cancel: 0,
275
+ destructive: 1 // iOS: red button
276
+ })
277
+
278
+ dialog.addEventListener('click', (e) => {
279
+ if (e.index === 1) {
280
+ performDelete()
281
+ }
282
+ })
283
+
284
+ dialog.show()
285
+ }
286
+
287
+ // OptionDialog (Action Sheet)
288
+ function showSortOptions() {
289
+ const options = [L('sort_name'), L('sort_date'), L('sort_price')]
290
+
291
+ const dialog = Ti.UI.createOptionDialog({
292
+ title: L('sort_by'),
293
+ options: [...options, L('cancel')],
294
+ cancel: options.length
295
+ })
296
+
297
+ dialog.addEventListener('click', (e) => {
298
+ if (e.index < options.length) {
299
+ applySortOption(e.index)
300
+ }
301
+ })
302
+
303
+ dialog.show()
304
+ }
305
+
306
+ // Modal Window
307
+ function openModal(route, params) {
308
+ const controller = Alloy.createController(route, params)
309
+ const window = controller.getView()
310
+
311
+ // iOS modal presentation
312
+ if (OS_IOS) {
313
+ window.applyProperties({
314
+ modalStyle: Ti.UI.iOS.MODAL_PRESENTATION_PAGESHEET,
315
+ modalTransitionStyle: Ti.UI.iOS.MODAL_TRANSITION_STYLE_COVER_VERTICAL
316
+ })
317
+ }
318
+
319
+ window.addEventListener('close', () => {
320
+ if (controller.cleanup) controller.cleanup()
321
+ })
322
+
323
+ window.open({ modal: true })
324
+
325
+ return controller
326
+ }
327
+ ```
328
+
329
+ ## TabGroup Pattern
330
+
331
+ ```xml
332
+ <!-- views/main.xml -->
333
+ <Alloy>
334
+ <TabGroup id="tabGroup" class="bg-white">
335
+ <Tab title="L('home')" icon="/images/tab_home.png">
336
+ <Require src="home/index" />
337
+ </Tab>
338
+ <Tab title="L('search')" icon="/images/tab_search.png">
339
+ <Require src="search/index" />
340
+ </Tab>
341
+ <Tab title="L('profile')" icon="/images/tab_profile.png">
342
+ <Require src="profile/index" />
343
+ </Tab>
344
+ </TabGroup>
345
+ </Alloy>
346
+ ```
347
+
348
+ ```javascript
349
+ // controllers/main.js
350
+ // Tab styling is handled in XML with PurgeTSS classes:
351
+ // <TabGroup id="tabGroup" class="tabs-bg-white active-tint-blue-500" />
352
+
353
+ // Handle tab changes
354
+ $.tabGroup.addEventListener('focus', (e) => {
355
+ // Track screen view
356
+ Analytics.trackScreen(e.tab.title)
357
+ })
358
+
359
+ // Programmatic tab switching
360
+ function switchToTab(index) {
361
+ $.tabGroup.activeTab = $.tabGroup.tabs[index]
362
+ }
363
+
364
+ // Badge on tab (notifications)
365
+ function updateNotificationBadge(count) {
366
+ $.tabGroup.tabs[2].badge = count > 0 ? String(count) : null
367
+ }
368
+ ```
369
+
370
+ ## NavigationWindow Pattern (iOS)
371
+
372
+ ```xml
373
+ <!-- views/index.xml -->
374
+ <Alloy>
375
+ <NavigationWindow id="navWin" platform="ios">
376
+ <Require src="home/index" />
377
+ </NavigationWindow>
378
+
379
+ <!-- Android uses Window directly -->
380
+ <Window platform="android">
381
+ <Require src="home/index" />
382
+ </Window>
383
+ </Alloy>
384
+ ```
385
+
386
+ ```javascript
387
+ // Cross-platform navigation helper
388
+ function openWindow(controller, animated = true) {
389
+ const window = controller.getView()
390
+
391
+ if (OS_IOS) {
392
+ $.navWin.openWindow(window, { animated })
393
+ } else {
394
+ window.open({ animated })
395
+ }
396
+ }
397
+
398
+ function closeWindow(window, animated = true) {
399
+ if (OS_IOS) {
400
+ $.navWin.closeWindow(window, { animated })
401
+ } else {
402
+ window.close({ animated })
403
+ }
404
+ }
405
+ ```
406
+
407
+ ## SearchBar with ListView
408
+
409
+ ```xml
410
+ <!-- views/contacts/list.xml -->
411
+ <Alloy>
412
+ <Window class="bg-gray-50">
413
+ <SearchBar id="searchBar"
414
+ class="w-screen"
415
+ hintText="L('search_contacts')"
416
+ showCancel="true"
417
+ />
418
+
419
+ <ListView id="listView" class="wh-screen" top="50">
420
+ <Templates>
421
+ <ItemTemplate name="contact" height="64">
422
+ <View class="horizontal h-16 w-screen bg-white">
423
+ <Label bindId="avatar" class="fa-solid fa-user-circle ml-4 text-3xl text-gray-400" />
424
+ <View class="vertical ml-3">
425
+ <Label bindId="name" class="text-base font-semibold" />
426
+ <Label bindId="phone" class="text-sm text-gray-500" />
427
+ </View>
428
+ </View>
429
+ </ItemTemplate>
430
+ </Templates>
431
+
432
+ <ListSection id="section" />
433
+ </ListView>
434
+ </Window>
435
+ </Alloy>
436
+ ```
437
+
438
+ ```javascript
439
+ // controllers/contacts/list.js
440
+ let allContacts = []
441
+ let searchTimeout = null
442
+
443
+ function init() {
444
+ loadContacts()
445
+
446
+ $.searchBar.addEventListener('change', onSearchChange)
447
+ $.searchBar.addEventListener('cancel', onSearchCancel)
448
+ }
449
+
450
+ function onSearchChange(e) {
451
+ // Debounce search
452
+ clearTimeout(searchTimeout)
453
+ searchTimeout = setTimeout(() => {
454
+ filterContacts(e.value)
455
+ }, 300)
456
+ }
457
+
458
+ function onSearchCancel() {
459
+ filterContacts('')
460
+ }
461
+
462
+ function filterContacts(query) {
463
+ const q = query.toLowerCase().trim()
464
+
465
+ const filtered = q
466
+ ? allContacts.filter(c =>
467
+ c.name.toLowerCase().includes(q) ||
468
+ c.phone.includes(q)
469
+ )
470
+ : allContacts
471
+
472
+ renderContacts(filtered)
473
+ }
474
+
475
+ function renderContacts(contacts) {
476
+ $.section.items = contacts.map(c => ({
477
+ template: 'contact',
478
+ properties: { itemId: c.id },
479
+ name: { text: c.name },
480
+ phone: { text: c.phone }
481
+ }))
482
+ }
483
+
484
+ function cleanup() {
485
+ clearTimeout(searchTimeout)
486
+ $.searchBar.removeEventListener('change', onSearchChange)
487
+ $.searchBar.removeEventListener('cancel', onSearchCancel)
488
+ $.destroy()
489
+ }
490
+
491
+ $.cleanup = cleanup
492
+ ```
493
+
494
+ ## Pull-to-Refresh Pattern
495
+
496
+ ```xml
497
+ <ListView id="listView" class="wh-screen">
498
+ <RefreshControl id="refreshControl" onRefresh="onRefresh" />
499
+ <ListSection id="section" />
500
+ </ListView>
501
+ ```
502
+
503
+ ```javascript
504
+ let isRefreshing = false
505
+
506
+ async function onRefresh() {
507
+ if (isRefreshing) return
508
+ isRefreshing = true
509
+
510
+ try {
511
+ const data = await api.fetchLatest()
512
+ renderItems(data)
513
+ } catch (error) {
514
+ showError(error.message)
515
+ } finally {
516
+ $.refreshControl.endRefreshing()
517
+ isRefreshing = false
518
+ }
519
+ }
520
+ ```