@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.
- package/AGENTS-TEMPLATE.md +173 -0
- package/README.md +867 -0
- package/agents/ti-researcher.md +108 -0
- package/bin/titools.js +53 -0
- package/lib/commands/agents.js +126 -0
- package/lib/commands/install.js +188 -0
- package/lib/commands/uninstall.js +215 -0
- package/lib/commands/update.js +159 -0
- package/lib/config.js +119 -0
- package/lib/downloader.js +153 -0
- package/lib/installer.js +253 -0
- package/lib/platform.js +108 -0
- package/lib/symlink.js +142 -0
- package/lib/utils.js +270 -0
- package/package.json +67 -0
- package/skills/alloy-expert/SKILL.md +247 -0
- package/skills/alloy-expert/assets/ControllerAutoCleanup.js +182 -0
- package/skills/alloy-expert/references/alloy-structure.md +381 -0
- package/skills/alloy-expert/references/anti-patterns.md +133 -0
- package/skills/alloy-expert/references/code-conventions.md +469 -0
- package/skills/alloy-expert/references/contracts.md +280 -0
- package/skills/alloy-expert/references/controller-patterns.md +520 -0
- package/skills/alloy-expert/references/error-handling.md +484 -0
- package/skills/alloy-expert/references/examples.md +735 -0
- package/skills/alloy-expert/references/migration-patterns.md +298 -0
- package/skills/alloy-expert/references/patterns.md +448 -0
- package/skills/alloy-expert/references/performance-patterns.md +855 -0
- package/skills/alloy-expert/references/security-patterns.md +847 -0
- package/skills/alloy-expert/references/state-management.md +779 -0
- package/skills/alloy-expert/references/testing.md +872 -0
- package/skills/alloy-guides/SKILL.md +214 -0
- package/skills/alloy-guides/references/CLI_TASKS.md +243 -0
- package/skills/alloy-guides/references/CONCEPTS.md +191 -0
- package/skills/alloy-guides/references/CONTROLLERS.md +298 -0
- package/skills/alloy-guides/references/MODELS.md +1028 -0
- package/skills/alloy-guides/references/PURGETSS.md +56 -0
- package/skills/alloy-guides/references/VIEWS_DYNAMIC.md +242 -0
- package/skills/alloy-guides/references/VIEWS_STYLES.md +388 -0
- package/skills/alloy-guides/references/VIEWS_WITHOUT_CONTROLLERS.md +109 -0
- package/skills/alloy-guides/references/VIEWS_XML.md +558 -0
- package/skills/alloy-guides/references/WIDGETS.md +176 -0
- package/skills/alloy-howtos/SKILL.md +203 -0
- package/skills/alloy-howtos/references/best_practices.md +138 -0
- package/skills/alloy-howtos/references/cli_reference.md +253 -0
- package/skills/alloy-howtos/references/config_files.md +87 -0
- package/skills/alloy-howtos/references/custom_tags.md +147 -0
- package/skills/alloy-howtos/references/debugging_troubleshooting.md +101 -0
- package/skills/alloy-howtos/references/samples.md +167 -0
- package/skills/purgetss/SKILL.md +442 -0
- package/skills/purgetss/assets/purgetss.config.cjs +17 -0
- package/skills/purgetss/references/EXAMPLES.md +247 -0
- package/skills/purgetss/references/animation-system.md +1294 -0
- package/skills/purgetss/references/apply-directive.md +375 -0
- package/skills/purgetss/references/arbitrary-values.md +612 -0
- package/skills/purgetss/references/class-index.md +1350 -0
- package/skills/purgetss/references/cli-commands.md +948 -0
- package/skills/purgetss/references/configurable-properties.md +654 -0
- package/skills/purgetss/references/custom-rules.md +161 -0
- package/skills/purgetss/references/customization-deep-dive.md +722 -0
- package/skills/purgetss/references/dynamic-component-creation.md +489 -0
- package/skills/purgetss/references/grid-layout.md +455 -0
- package/skills/purgetss/references/icon-fonts.md +609 -0
- package/skills/purgetss/references/installation-setup.md +366 -0
- package/skills/purgetss/references/opacity-modifier.md +291 -0
- package/skills/purgetss/references/platform-modifiers.md +479 -0
- package/skills/purgetss/references/smart-mappings.md +42 -0
- package/skills/purgetss/references/titanium-resets.md +359 -0
- package/skills/purgetss/references/ui-ux-design.md +1526 -0
- package/skills/ti-guides/SKILL.md +94 -0
- package/skills/ti-guides/references/advanced-data-and-images.md +19 -0
- package/skills/ti-guides/references/alloy-cli-advanced.md +84 -0
- package/skills/ti-guides/references/alloy-data-mastery.md +29 -0
- package/skills/ti-guides/references/alloy-widgets-and-themes.md +19 -0
- package/skills/ti-guides/references/android-manifest.md +97 -0
- package/skills/ti-guides/references/app-distribution.md +258 -0
- package/skills/ti-guides/references/application-frameworks.md +377 -0
- package/skills/ti-guides/references/cli-reference.md +402 -0
- package/skills/ti-guides/references/coding-best-practices.md +102 -0
- package/skills/ti-guides/references/commonjs-advanced.md +134 -0
- package/skills/ti-guides/references/hello-world.md +100 -0
- package/skills/ti-guides/references/hyperloop-native-access.md +62 -0
- package/skills/ti-guides/references/javascript-primer.md +411 -0
- package/skills/ti-guides/references/reserved-words.md +36 -0
- package/skills/ti-guides/references/resources.md +183 -0
- package/skills/ti-guides/references/style-and-conventions.md +48 -0
- package/skills/ti-guides/references/tiapp-config.md +609 -0
- package/skills/ti-howtos/SKILL.md +174 -0
- package/skills/ti-howtos/references/android-platform-deep-dives.md +658 -0
- package/skills/ti-howtos/references/automation-fastlane-appium.md +95 -0
- package/skills/ti-howtos/references/buffer-codec-streams.md +140 -0
- package/skills/ti-howtos/references/cross-platform-development.md +348 -0
- package/skills/ti-howtos/references/debugging-profiling.md +543 -0
- package/skills/ti-howtos/references/extending-titanium.md +723 -0
- package/skills/ti-howtos/references/google-maps-v2.md +169 -0
- package/skills/ti-howtos/references/ios-map-kit.md +143 -0
- package/skills/ti-howtos/references/ios-platform-deep-dives.md +783 -0
- package/skills/ti-howtos/references/local-data-sources.md +301 -0
- package/skills/ti-howtos/references/location-and-maps.md +252 -0
- package/skills/ti-howtos/references/media-apis.md +210 -0
- package/skills/ti-howtos/references/notification-services.md +599 -0
- package/skills/ti-howtos/references/remote-data-sources.md +349 -0
- package/skills/ti-howtos/references/tutorials.md +502 -0
- package/skills/ti-howtos/references/using-modules.md +237 -0
- package/skills/ti-howtos/references/web-content-integration.md +307 -0
- package/skills/ti-howtos/references/webpack-build-pipeline.md +78 -0
- package/skills/ti-ui/SKILL.md +179 -0
- package/skills/ti-ui/references/accessibility-deep-dive.md +242 -0
- package/skills/ti-ui/references/animation-and-matrices.md +599 -0
- package/skills/ti-ui/references/application-structures.md +655 -0
- package/skills/ti-ui/references/custom-fonts-styling.md +579 -0
- package/skills/ti-ui/references/event-handling.md +393 -0
- package/skills/ti-ui/references/gestures.md +473 -0
- package/skills/ti-ui/references/icons-and-splash-screens.md +409 -0
- package/skills/ti-ui/references/layouts-and-positioning.md +462 -0
- package/skills/ti-ui/references/listviews-and-performance.md +619 -0
- package/skills/ti-ui/references/orientation.md +362 -0
- package/skills/ti-ui/references/platform-ui-android.md +635 -0
- package/skills/ti-ui/references/platform-ui-ios.md +469 -0
- package/skills/ti-ui/references/scrolling-views.md +252 -0
- package/skills/ti-ui/references/tableviews.md +568 -0
|
@@ -0,0 +1,735 @@
|
|
|
1
|
+
# PurgeTSS Implementation Examples (Alloy + PurgeTSS)
|
|
2
|
+
|
|
3
|
+
## API Client Service
|
|
4
|
+
Standard logic for network requests.
|
|
5
|
+
|
|
6
|
+
```javascript
|
|
7
|
+
// lib/api/client.js
|
|
8
|
+
exports.get = function(endpoint, params = {}) {
|
|
9
|
+
return new Promise(function(resolve, reject) {
|
|
10
|
+
const client = Ti.Network.createHTTPClient({
|
|
11
|
+
onload: function() { resolve(JSON.parse(client.responseText)) },
|
|
12
|
+
onerror: function(e) { reject(e) },
|
|
13
|
+
timeout: 5000
|
|
14
|
+
})
|
|
15
|
+
client.open('GET', endpoint)
|
|
16
|
+
client.send()
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Native Module Wrapper Service
|
|
22
|
+
Encapsulate native modules (e.g., audio, maps, facebook) to decouple from controllers.
|
|
23
|
+
|
|
24
|
+
```javascript
|
|
25
|
+
// lib/services/nativeService.js
|
|
26
|
+
const someModule = require('ti.someModule')
|
|
27
|
+
|
|
28
|
+
exports.performAction = function(data) {
|
|
29
|
+
someModule.doSomething({
|
|
30
|
+
data: data,
|
|
31
|
+
status: L('processing')
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## i18n Helper
|
|
37
|
+
Logic for complex string transformations and plurals.
|
|
38
|
+
|
|
39
|
+
```javascript
|
|
40
|
+
// lib/helpers/i18n.js
|
|
41
|
+
exports.getPluralMessages = function(count) {
|
|
42
|
+
const key = count === 1 ? 'one_message' : 'many_messages'
|
|
43
|
+
return L(key).replace('%d', count)
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Model with SQL Adapter
|
|
48
|
+
Definition for local SQLite persistence.
|
|
49
|
+
|
|
50
|
+
```javascript
|
|
51
|
+
// models/User.js
|
|
52
|
+
exports.definition = {
|
|
53
|
+
config: {
|
|
54
|
+
columns: {
|
|
55
|
+
id: 'INTEGER PRIMARY KEY AUTOINCREMENT',
|
|
56
|
+
name: 'TEXT',
|
|
57
|
+
email: 'TEXT'
|
|
58
|
+
},
|
|
59
|
+
adapter: {
|
|
60
|
+
type: 'sql',
|
|
61
|
+
collection_name: 'users'
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Fully Styled & Accessible View (PurgeTSS)
|
|
68
|
+
Applying PurgeTSS classes while maintaining accessibility.
|
|
69
|
+
|
|
70
|
+
```xml
|
|
71
|
+
<!-- views/login.xml -->
|
|
72
|
+
<Alloy>
|
|
73
|
+
<Window class="vertical bg-gray-50">
|
|
74
|
+
<Animation id="myAnim" module="purgetss.ui" class="close:opacity-0 duration-500 open:opacity-100" />
|
|
75
|
+
|
|
76
|
+
<!-- Spacer to center content -->
|
|
77
|
+
<View class="h-auto" />
|
|
78
|
+
|
|
79
|
+
<Label class="text-primary fa-solid fa-lock mx-6 mb-10 text-4xl"
|
|
80
|
+
accessibilityLabel="L('login_icon_label')"
|
|
81
|
+
/>
|
|
82
|
+
|
|
83
|
+
<TextField id="email"
|
|
84
|
+
class="border-(1) mx-6 h-12 w-screen rounded-lg border-gray-300 bg-white"
|
|
85
|
+
hintText="L('email_hint')"
|
|
86
|
+
accessibilityLabel="L('email_label')"
|
|
87
|
+
/>
|
|
88
|
+
|
|
89
|
+
<Button id="submit"
|
|
90
|
+
class="bg-primary mx-6 mt-10 h-14 w-screen rounded-xl font-bold text-white"
|
|
91
|
+
title="L('login_button')"
|
|
92
|
+
accessibilityLabel="L('login_button')"
|
|
93
|
+
accessibilityHint="L('login_hint')"
|
|
94
|
+
onClick="doLogin"
|
|
95
|
+
/>
|
|
96
|
+
|
|
97
|
+
<!-- Spacer to center content -->
|
|
98
|
+
<View class="h-auto" />
|
|
99
|
+
</Window>
|
|
100
|
+
</Alloy>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Notes:**
|
|
104
|
+
- Use `vertical` layout on Window (not `flex-col`)
|
|
105
|
+
- Use `mx-6` for horizontal padding (not `p-6` on parent)
|
|
106
|
+
- Use `w-screen` for full width (not `w-full`)
|
|
107
|
+
- Use `border-(1)` with parentheses for arbitrary border width
|
|
108
|
+
|
|
109
|
+
## Cleanup Pattern in Controller
|
|
110
|
+
Critical for memory management and avoiding leaks.
|
|
111
|
+
|
|
112
|
+
```javascript
|
|
113
|
+
// controllers/login.js
|
|
114
|
+
const onNotification = (e) => {
|
|
115
|
+
Ti.API.info('Notification received')
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
Ti.App.addEventListener('notification', onNotification)
|
|
119
|
+
|
|
120
|
+
function cleanup() {
|
|
121
|
+
// Always remove app-level listeners
|
|
122
|
+
Ti.App.removeEventListener('notification', onNotification)
|
|
123
|
+
// Destroy Alloy bindings
|
|
124
|
+
$.destroy()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Expose for Navigation Service
|
|
128
|
+
$.cleanup = cleanup
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Animation Component Usage
|
|
132
|
+
Using the toolkit for UI transformations via the `<Animation>` component.
|
|
133
|
+
|
|
134
|
+
```javascript
|
|
135
|
+
// Inside any controller
|
|
136
|
+
function shakeError(element) {
|
|
137
|
+
$.myAnim.play(element, 'animate-shake duration-200')
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Complete CRUD Example
|
|
142
|
+
|
|
143
|
+
```javascript
|
|
144
|
+
// lib/services/productService.js
|
|
145
|
+
const { productApi } = require('lib/api/productApi')
|
|
146
|
+
const { appStore } = require('lib/services/stateStore')
|
|
147
|
+
const logger = require('lib/services/logger')
|
|
148
|
+
|
|
149
|
+
exports.productService = {
|
|
150
|
+
getAll: async function(filters = {}) {
|
|
151
|
+
logger.debug('ProductService', 'Fetching products', filters)
|
|
152
|
+
|
|
153
|
+
appStore.setState({ 'ui.isLoading': true })
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const products = await productApi.getAll(filters)
|
|
157
|
+
Alloy.Collections.products.reset(products)
|
|
158
|
+
return products
|
|
159
|
+
} finally {
|
|
160
|
+
appStore.setState({ 'ui.isLoading': false })
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
getById: async function(id) {
|
|
165
|
+
return productApi.getById(id)
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
create: async function(data) {
|
|
169
|
+
const product = await productApi.create(data)
|
|
170
|
+
Alloy.Collections.products.add(product)
|
|
171
|
+
logger.info('ProductService', 'Product created', { id: product.id })
|
|
172
|
+
return product
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
update: async function(id, data) {
|
|
176
|
+
const product = await productApi.update(id, data)
|
|
177
|
+
|
|
178
|
+
// Update in collection
|
|
179
|
+
const existing = Alloy.Collections.products.get(id)
|
|
180
|
+
if (existing) {
|
|
181
|
+
existing.set(product)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
logger.info('ProductService', 'Product updated', { id })
|
|
185
|
+
return product
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
delete: async function(id) {
|
|
189
|
+
await productApi.delete(id)
|
|
190
|
+
Alloy.Collections.products.remove(id)
|
|
191
|
+
logger.info('ProductService', 'Product deleted', { id })
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
```xml
|
|
197
|
+
<!-- views/products/list.xml -->
|
|
198
|
+
<Alloy>
|
|
199
|
+
<Window class="bg-gray-50">
|
|
200
|
+
<ListView id="listView" class="wh-screen">
|
|
201
|
+
<RefreshControl id="refresh" onRefresh="onRefresh" />
|
|
202
|
+
|
|
203
|
+
<Templates>
|
|
204
|
+
<ItemTemplate name="product" height="80">
|
|
205
|
+
<View class="horizontal mb-2 h-20 w-screen bg-white">
|
|
206
|
+
<ImageView bindId="image" class="wh-16 ml-3 rounded-lg" />
|
|
207
|
+
<View class="vertical ml-3 w-auto">
|
|
208
|
+
<Label bindId="name" class="text-base font-semibold" />
|
|
209
|
+
<Label bindId="price" class="text-sm text-green-600" />
|
|
210
|
+
<Label bindId="stock" class="text-xs text-gray-400" />
|
|
211
|
+
</View>
|
|
212
|
+
</View>
|
|
213
|
+
</ItemTemplate>
|
|
214
|
+
</Templates>
|
|
215
|
+
|
|
216
|
+
<ListSection id="section" dataCollection="products">
|
|
217
|
+
<ListItem template="product"
|
|
218
|
+
image:image="{imageUrl}"
|
|
219
|
+
name:text="{name}"
|
|
220
|
+
price:text="${price}"
|
|
221
|
+
stock:text="{stock} in stock"
|
|
222
|
+
/>
|
|
223
|
+
</ListSection>
|
|
224
|
+
</ListView>
|
|
225
|
+
|
|
226
|
+
<Button id="addBtn"
|
|
227
|
+
class="bg-primary rounded-full-14 absolute bottom-6 right-6 shadow-lg"
|
|
228
|
+
onClick="onAddProduct"
|
|
229
|
+
>
|
|
230
|
+
<Label class="fa-solid fa-plus text-xl text-white" />
|
|
231
|
+
</Button>
|
|
232
|
+
</Window>
|
|
233
|
+
</Alloy>
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
```javascript
|
|
237
|
+
// controllers/products/list.js
|
|
238
|
+
const { Navigation } = require('lib/services/navigation')
|
|
239
|
+
const { productService } = require('lib/services/productService')
|
|
240
|
+
|
|
241
|
+
function init() {
|
|
242
|
+
loadProducts()
|
|
243
|
+
|
|
244
|
+
$.listView.addEventListener('itemclick', onItemClick)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function loadProducts() {
|
|
248
|
+
try {
|
|
249
|
+
await productService.getAll()
|
|
250
|
+
} catch (error) {
|
|
251
|
+
showError(error.message)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function onRefresh() {
|
|
256
|
+
try {
|
|
257
|
+
await productService.getAll()
|
|
258
|
+
} finally {
|
|
259
|
+
$.refresh.endRefreshing()
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function onItemClick(e) {
|
|
264
|
+
const productId = e.itemId
|
|
265
|
+
Navigation.open('products/detail', { productId })
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function onAddProduct() {
|
|
269
|
+
const ctrl = Navigation.open('products/edit', { mode: 'create' })
|
|
270
|
+
ctrl.on('product:saved', loadProducts)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function cleanup() {
|
|
274
|
+
$.listView.removeEventListener('itemclick', onItemClick)
|
|
275
|
+
$.destroy()
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
$.cleanup = cleanup
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
## ListView with Search and Filter
|
|
282
|
+
|
|
283
|
+
```xml
|
|
284
|
+
<!-- views/contacts/list.xml -->
|
|
285
|
+
<Alloy>
|
|
286
|
+
<Window class="bg-white">
|
|
287
|
+
<!-- Search and Filter Bar -->
|
|
288
|
+
<View class="horizontal h-14 w-screen bg-gray-100">
|
|
289
|
+
<SearchBar id="searchBar"
|
|
290
|
+
class="h-10 w-8/12"
|
|
291
|
+
hintText="L('search')"
|
|
292
|
+
showCancel="true"
|
|
293
|
+
/>
|
|
294
|
+
<Button id="filterBtn"
|
|
295
|
+
class="h-10 w-4/12"
|
|
296
|
+
title="L('filter')"
|
|
297
|
+
onClick="showFilters"
|
|
298
|
+
/>
|
|
299
|
+
</View>
|
|
300
|
+
|
|
301
|
+
<!-- Active Filters -->
|
|
302
|
+
<ScrollView id="filterTags" class="horizontal hidden h-10 w-screen">
|
|
303
|
+
<!-- Dynamically populated filter tags -->
|
|
304
|
+
</ScrollView>
|
|
305
|
+
|
|
306
|
+
<ListView id="listView" class="wh-screen">
|
|
307
|
+
<ListSection id="section" />
|
|
308
|
+
</ListView>
|
|
309
|
+
|
|
310
|
+
<!-- Empty State -->
|
|
311
|
+
<View id="emptyState" class="wh-screen vertical hidden">
|
|
312
|
+
<Label class="fa-solid fa-search mt-20 text-6xl text-gray-300" />
|
|
313
|
+
<Label class="mt-4 text-lg text-gray-500" text="L('no_results')" />
|
|
314
|
+
</View>
|
|
315
|
+
</Window>
|
|
316
|
+
</Alloy>
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
```javascript
|
|
320
|
+
// controllers/contacts/list.js
|
|
321
|
+
let allItems = []
|
|
322
|
+
let activeFilters = { category: null, status: null }
|
|
323
|
+
let searchQuery = ''
|
|
324
|
+
let debounceTimer = null
|
|
325
|
+
|
|
326
|
+
function init() {
|
|
327
|
+
loadData()
|
|
328
|
+
|
|
329
|
+
$.searchBar.addEventListener('cancel', clearSearch)
|
|
330
|
+
$.searchBar.addEventListener('change', onSearchChange)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function onSearchChange(e) {
|
|
334
|
+
clearTimeout(debounceTimer)
|
|
335
|
+
debounceTimer = setTimeout(() => {
|
|
336
|
+
searchQuery = e.value.toLowerCase().trim()
|
|
337
|
+
applyFilters()
|
|
338
|
+
}, 300)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function clearSearch() {
|
|
342
|
+
searchQuery = ''
|
|
343
|
+
applyFilters()
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function showFilters() {
|
|
347
|
+
const dialog = Ti.UI.createOptionDialog({
|
|
348
|
+
title: L('filter_by_category'),
|
|
349
|
+
options: ['All', 'Work', 'Personal', 'Family', L('cancel')],
|
|
350
|
+
cancel: 4
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
dialog.addEventListener('click', (e) => {
|
|
354
|
+
if (e.index < 4) {
|
|
355
|
+
activeFilters.category = e.index === 0 ? null : ['work', 'personal', 'family'][e.index - 1]
|
|
356
|
+
updateFilterTags()
|
|
357
|
+
applyFilters()
|
|
358
|
+
}
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
dialog.show()
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function applyFilters() {
|
|
365
|
+
let filtered = [...allItems]
|
|
366
|
+
|
|
367
|
+
// Apply search
|
|
368
|
+
if (searchQuery) {
|
|
369
|
+
filtered = filtered.filter(item =>
|
|
370
|
+
item.name.toLowerCase().includes(searchQuery) ||
|
|
371
|
+
item.email?.toLowerCase().includes(searchQuery)
|
|
372
|
+
)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Apply category filter
|
|
376
|
+
if (activeFilters.category) {
|
|
377
|
+
filtered = filtered.filter(item => item.category === activeFilters.category)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
renderItems(filtered)
|
|
381
|
+
|
|
382
|
+
// Show/hide empty state
|
|
383
|
+
$.emptyState.visible = filtered.length === 0
|
|
384
|
+
$.listView.visible = filtered.length > 0
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function updateFilterTags() {
|
|
388
|
+
// Clear existing tags
|
|
389
|
+
$.filterTags.removeAllChildren()
|
|
390
|
+
|
|
391
|
+
const hasFilters = activeFilters.category || activeFilters.status
|
|
392
|
+
$.filterTags.visible = hasFilters
|
|
393
|
+
|
|
394
|
+
if (activeFilters.category) {
|
|
395
|
+
const tag = createFilterTag(activeFilters.category, () => {
|
|
396
|
+
activeFilters.category = null
|
|
397
|
+
updateFilterTags()
|
|
398
|
+
applyFilters()
|
|
399
|
+
})
|
|
400
|
+
$.filterTags.add(tag)
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function createFilterTag(label, onRemove) {
|
|
405
|
+
const tag = Ti.UI.createView({ width: Ti.UI.SIZE, height: 32 })
|
|
406
|
+
tag.add(Ti.UI.createLabel({ text: label }))
|
|
407
|
+
|
|
408
|
+
const closeBtn = Ti.UI.createLabel({ text: '×', right: 4 })
|
|
409
|
+
closeBtn.addEventListener('click', onRemove)
|
|
410
|
+
tag.add(closeBtn)
|
|
411
|
+
|
|
412
|
+
return tag
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function cleanup() {
|
|
416
|
+
clearTimeout(debounceTimer)
|
|
417
|
+
$.searchBar.removeEventListener('change', onSearchChange)
|
|
418
|
+
$.destroy()
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
$.cleanup = cleanup
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
## Form with Validation Example
|
|
425
|
+
|
|
426
|
+
```xml
|
|
427
|
+
<!-- views/auth/register.xml -->
|
|
428
|
+
<Alloy>
|
|
429
|
+
<Window class="vertical bg-white">
|
|
430
|
+
<ScrollView class="wh-screen vertical" contentHeight="Ti.UI.SIZE">
|
|
431
|
+
<View class="vertical mt-8 h-auto w-screen">
|
|
432
|
+
<!-- Logo -->
|
|
433
|
+
<ImageView class="wh-24" image="/images/logo.png" />
|
|
434
|
+
|
|
435
|
+
<!-- Title -->
|
|
436
|
+
<Label class="mt-6 text-2xl font-bold" text="L('create_account')" />
|
|
437
|
+
|
|
438
|
+
<!-- Name Field -->
|
|
439
|
+
<View class="mx-4 mt-6 w-screen">
|
|
440
|
+
<Label class="text-sm text-gray-600" text="L('full_name')" />
|
|
441
|
+
<TextField id="nameField"
|
|
442
|
+
class="border-(1) return-key-type-next mt-1 h-12 w-screen rounded-lg border-gray-300 px-3"
|
|
443
|
+
autocorrect="false"
|
|
444
|
+
/>
|
|
445
|
+
<Label id="nameError" class="mt-1 hidden text-xs text-red-500" />
|
|
446
|
+
</View>
|
|
447
|
+
|
|
448
|
+
<!-- Email Field -->
|
|
449
|
+
<View class="mx-4 mt-4 w-screen">
|
|
450
|
+
<Label class="text-sm text-gray-600" text="L('email')" />
|
|
451
|
+
<TextField id="emailField"
|
|
452
|
+
class="border-(1) keyboard-type-email return-key-type-next mt-1 h-12 w-screen rounded-lg border-gray-300 px-3"
|
|
453
|
+
autocapitalization="none"
|
|
454
|
+
/>
|
|
455
|
+
<Label id="emailError" class="mt-1 hidden text-xs text-red-500" />
|
|
456
|
+
</View>
|
|
457
|
+
|
|
458
|
+
<!-- Password Field -->
|
|
459
|
+
<View class="mx-4 mt-4 w-screen">
|
|
460
|
+
<Label class="text-sm text-gray-600" text="L('password')" />
|
|
461
|
+
<TextField id="passwordField"
|
|
462
|
+
class="border-(1) return-key-type-next mt-1 h-12 w-screen rounded-lg border-gray-300 px-3"
|
|
463
|
+
passwordMask="true"
|
|
464
|
+
/>
|
|
465
|
+
<Label id="passwordError" class="mt-1 hidden text-xs text-red-500" />
|
|
466
|
+
<Label class="mt-1 text-xs text-gray-400" text="L('password_hint')" />
|
|
467
|
+
</View>
|
|
468
|
+
|
|
469
|
+
<!-- Confirm Password -->
|
|
470
|
+
<View class="mx-4 mt-4 w-screen">
|
|
471
|
+
<Label class="text-sm text-gray-600" text="L('confirm_password')" />
|
|
472
|
+
<TextField id="confirmField"
|
|
473
|
+
class="border-(1) return-key-type-done mt-1 h-12 w-screen rounded-lg border-gray-300 px-3"
|
|
474
|
+
passwordMask="true"
|
|
475
|
+
/>
|
|
476
|
+
<Label id="confirmError" class="mt-1 hidden text-xs text-red-500" />
|
|
477
|
+
</View>
|
|
478
|
+
|
|
479
|
+
<!-- Terms Checkbox -->
|
|
480
|
+
<View class="horizontal mx-4 mt-6 w-screen">
|
|
481
|
+
<Switch id="termsSwitch" class="w-12" />
|
|
482
|
+
<Label class="ml-2 w-auto text-sm text-gray-600" text="L('accept_terms')" />
|
|
483
|
+
</View>
|
|
484
|
+
|
|
485
|
+
<!-- Register Button -->
|
|
486
|
+
<Button id="registerBtn"
|
|
487
|
+
class="bg-primary mx-4 mt-6 h-14 w-screen rounded-xl font-bold text-white"
|
|
488
|
+
title="L('register')"
|
|
489
|
+
onClick="onRegister"
|
|
490
|
+
/>
|
|
491
|
+
|
|
492
|
+
<!-- Login Link -->
|
|
493
|
+
<View class="horizontal mt-4">
|
|
494
|
+
<Label class="text-sm text-gray-600" text="L('have_account')" />
|
|
495
|
+
<Label class="text-primary ml-1 text-sm" text="L('login')" onClick="goToLogin" />
|
|
496
|
+
</View>
|
|
497
|
+
</View>
|
|
498
|
+
</ScrollView>
|
|
499
|
+
</Window>
|
|
500
|
+
</Alloy>
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
```javascript
|
|
504
|
+
// controllers/auth/register.js
|
|
505
|
+
const { AuthService } = require('lib/services/authService')
|
|
506
|
+
const { Navigation } = require('lib/services/navigation')
|
|
507
|
+
|
|
508
|
+
const validators = {
|
|
509
|
+
name: (value) => {
|
|
510
|
+
if (!value?.trim()) return L('error_name_required')
|
|
511
|
+
if (value.trim().length < 2) return L('error_name_short')
|
|
512
|
+
return null
|
|
513
|
+
},
|
|
514
|
+
|
|
515
|
+
email: (value) => {
|
|
516
|
+
if (!value?.trim()) return L('error_email_required')
|
|
517
|
+
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
518
|
+
if (!regex.test(value)) return L('error_email_invalid')
|
|
519
|
+
return null
|
|
520
|
+
},
|
|
521
|
+
|
|
522
|
+
password: (value) => {
|
|
523
|
+
if (!value) return L('error_password_required')
|
|
524
|
+
if (value.length < 8) return L('error_password_short')
|
|
525
|
+
if (!/[A-Z]/.test(value)) return L('error_password_uppercase')
|
|
526
|
+
if (!/[0-9]/.test(value)) return L('error_password_number')
|
|
527
|
+
return null
|
|
528
|
+
},
|
|
529
|
+
|
|
530
|
+
confirm: (value, password) => {
|
|
531
|
+
if (!value) return L('error_confirm_required')
|
|
532
|
+
if (value !== password) return L('error_passwords_mismatch')
|
|
533
|
+
return null
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function init() {
|
|
538
|
+
// Field navigation
|
|
539
|
+
$.nameField.addEventListener('return', () => $.emailField.focus())
|
|
540
|
+
$.emailField.addEventListener('return', () => $.passwordField.focus())
|
|
541
|
+
$.passwordField.addEventListener('return', () => $.confirmField.focus())
|
|
542
|
+
$.confirmField.addEventListener('return', onRegister)
|
|
543
|
+
|
|
544
|
+
// Real-time validation on blur
|
|
545
|
+
$.nameField.addEventListener('blur', () => validateField('name'))
|
|
546
|
+
$.emailField.addEventListener('blur', () => validateField('email'))
|
|
547
|
+
$.passwordField.addEventListener('blur', () => validateField('password'))
|
|
548
|
+
$.confirmField.addEventListener('blur', () => validateField('confirm'))
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function validateField(field) {
|
|
552
|
+
const fields = {
|
|
553
|
+
name: { input: $.nameField, error: $.nameError },
|
|
554
|
+
email: { input: $.emailField, error: $.emailError },
|
|
555
|
+
password: { input: $.passwordField, error: $.passwordError },
|
|
556
|
+
confirm: { input: $.confirmField, error: $.confirmError }
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const { input, error } = fields[field]
|
|
560
|
+
const value = input.value
|
|
561
|
+
|
|
562
|
+
let errorMsg
|
|
563
|
+
if (field === 'confirm') {
|
|
564
|
+
errorMsg = validators[field](value, $.passwordField.value)
|
|
565
|
+
} else {
|
|
566
|
+
errorMsg = validators[field](value)
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (errorMsg) {
|
|
570
|
+
error.applyProperties({ text: errorMsg, visible: true })
|
|
571
|
+
input.applyProperties({ borderColor: '#ef4444' })
|
|
572
|
+
return false
|
|
573
|
+
} else {
|
|
574
|
+
error.visible = false
|
|
575
|
+
input.applyProperties({ borderColor: '#d1d5db' })
|
|
576
|
+
return true
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function validateAll() {
|
|
581
|
+
const nameValid = validateField('name')
|
|
582
|
+
const emailValid = validateField('email')
|
|
583
|
+
const passwordValid = validateField('password')
|
|
584
|
+
const confirmValid = validateField('confirm')
|
|
585
|
+
|
|
586
|
+
if (!$.termsSwitch.value) {
|
|
587
|
+
Ti.UI.createAlertDialog({
|
|
588
|
+
title: L('error'),
|
|
589
|
+
message: L('error_accept_terms')
|
|
590
|
+
}).show()
|
|
591
|
+
return false
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
return nameValid && emailValid && passwordValid && confirmValid
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
async function onRegister() {
|
|
598
|
+
if (!validateAll()) return
|
|
599
|
+
|
|
600
|
+
setLoading(true)
|
|
601
|
+
|
|
602
|
+
try {
|
|
603
|
+
await AuthService.register({
|
|
604
|
+
name: $.nameField.value.trim(),
|
|
605
|
+
email: $.emailField.value.trim().toLowerCase(),
|
|
606
|
+
password: $.passwordField.value
|
|
607
|
+
})
|
|
608
|
+
|
|
609
|
+
// Success - navigate to home
|
|
610
|
+
Navigation.replace('main')
|
|
611
|
+
|
|
612
|
+
} catch (error) {
|
|
613
|
+
Ti.UI.createAlertDialog({
|
|
614
|
+
title: L('error'),
|
|
615
|
+
message: error.message
|
|
616
|
+
}).show()
|
|
617
|
+
} finally {
|
|
618
|
+
setLoading(false)
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function setLoading(loading) {
|
|
623
|
+
$.registerBtn.applyProperties({
|
|
624
|
+
enabled: !loading,
|
|
625
|
+
title: loading ? L('registering') : L('register')
|
|
626
|
+
})
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function goToLogin() {
|
|
630
|
+
Navigation.back()
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function cleanup() {
|
|
634
|
+
$.destroy()
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
$.cleanup = cleanup
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
## Tab-Based Navigation Example
|
|
641
|
+
|
|
642
|
+
```xml
|
|
643
|
+
<!-- views/main.xml -->
|
|
644
|
+
<Alloy>
|
|
645
|
+
<TabGroup id="tabGroup" class="tabs-bg-white active-tint-blue-500">
|
|
646
|
+
|
|
647
|
+
<Tab id="homeTab" title="L('home')" icon="/images/icons/home.png">
|
|
648
|
+
<Require src="tabs/home" />
|
|
649
|
+
</Tab>
|
|
650
|
+
|
|
651
|
+
<Tab id="searchTab" title="L('search')" icon="/images/icons/search.png">
|
|
652
|
+
<Require src="tabs/search" />
|
|
653
|
+
</Tab>
|
|
654
|
+
|
|
655
|
+
<Tab id="cartTab" title="L('cart')" icon="/images/icons/cart.png">
|
|
656
|
+
<Require src="tabs/cart" />
|
|
657
|
+
</Tab>
|
|
658
|
+
|
|
659
|
+
<Tab id="profileTab" title="L('profile')" icon="/images/icons/profile.png">
|
|
660
|
+
<Require src="tabs/profile" />
|
|
661
|
+
</Tab>
|
|
662
|
+
</TabGroup>
|
|
663
|
+
</Alloy>
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
```javascript
|
|
667
|
+
// controllers/main.js
|
|
668
|
+
const EventBus = require('lib/services/eventBus')
|
|
669
|
+
const Events = EventBus.Events
|
|
670
|
+
|
|
671
|
+
function init() {
|
|
672
|
+
// Listen for cart updates to show badge
|
|
673
|
+
EventBus.on(Events.CART_UPDATED, onCartUpdated)
|
|
674
|
+
|
|
675
|
+
// Track tab changes
|
|
676
|
+
$.tabGroup.addEventListener('focus', onTabFocus)
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function onTabFocus(e) {
|
|
680
|
+
// Analytics tracking
|
|
681
|
+
const tabNames = ['home', 'search', 'cart', 'profile']
|
|
682
|
+
Ti.Analytics.featureEvent(`tab:${tabNames[e.index]}`)
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function onCartUpdated({ itemCount }) {
|
|
686
|
+
// Update badge on cart tab
|
|
687
|
+
$.cartTab.badge = itemCount > 0 ? String(itemCount) : null
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Switch to specific tab programmatically
|
|
691
|
+
function switchToTab(tabName) {
|
|
692
|
+
const tabMap = { home: 0, search: 1, cart: 2, profile: 3 }
|
|
693
|
+
const index = tabMap[tabName]
|
|
694
|
+
|
|
695
|
+
if (index !== undefined) {
|
|
696
|
+
$.tabGroup.activeTab = $.tabGroup.tabs[index]
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Export for external access
|
|
701
|
+
$.switchToTab = switchToTab
|
|
702
|
+
|
|
703
|
+
function cleanup() {
|
|
704
|
+
EventBus.off(Events.CART_UPDATED, onCartUpdated)
|
|
705
|
+
$.tabGroup.removeEventListener('focus', onTabFocus)
|
|
706
|
+
$.destroy()
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
$.cleanup = cleanup
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
```javascript
|
|
713
|
+
// controllers/tabs/home.js
|
|
714
|
+
// Each tab is a separate controller with its own lifecycle
|
|
715
|
+
function init() {
|
|
716
|
+
loadFeaturedProducts()
|
|
717
|
+
|
|
718
|
+
// Handle window focus (tab selected)
|
|
719
|
+
$.getView().addEventListener('focus', onFocus)
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function onFocus() {
|
|
723
|
+
// Refresh data when returning to this tab
|
|
724
|
+
if (shouldRefresh()) {
|
|
725
|
+
loadFeaturedProducts()
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function cleanup() {
|
|
730
|
+
$.getView().removeEventListener('focus', onFocus)
|
|
731
|
+
$.destroy()
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
$.cleanup = cleanup
|
|
735
|
+
```
|