@awes-io/ui 2.142.3 → 2.144.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 (264) hide show
  1. package/assets/css/components/_index.css +7 -1
  2. package/assets/css/components/action-card.css +1 -0
  3. package/assets/css/components/action-icon.css +2 -2
  4. package/assets/css/components/alert.css +28 -22
  5. package/assets/css/components/animation.css +52 -32
  6. package/assets/css/components/badge.css +1 -0
  7. package/assets/css/components/banner-text.css +15 -4
  8. package/assets/css/components/card.css +0 -1
  9. package/assets/css/components/content-placeholder.css +104 -0
  10. package/assets/css/components/dropdown.css +20 -7
  11. package/assets/css/components/empty-container.css +69 -1
  12. package/assets/css/components/filter-chosen.css +6 -0
  13. package/assets/css/components/filter-date-range.css +17 -1
  14. package/assets/css/components/filter-month.css +23 -17
  15. package/assets/css/components/filter-select.css +11 -0
  16. package/assets/css/components/icon-menu-item.css +12 -7
  17. package/assets/css/components/layout.css +1 -32
  18. package/assets/css/components/mobile-menu-nav.css +8 -4
  19. package/assets/css/components/modal.css +1 -1
  20. package/assets/css/components/number.css +12 -0
  21. package/assets/css/components/page-aside.css +54 -0
  22. package/assets/css/components/text-field.css +4 -0
  23. package/assets/js/css.js +1 -1
  24. package/assets/js/icons/mono.js +59 -91
  25. package/assets/js/icons/multicolor.js +1 -31
  26. package/components/1_atoms/AwActionIcon.vue +11 -2
  27. package/components/1_atoms/AwContentPlaceholder.vue +60 -0
  28. package/components/1_atoms/AwFlow.vue +37 -49
  29. package/components/1_atoms/AwGrid.vue +11 -3
  30. package/components/1_atoms/AwIcon/AwIcon.vue +5 -3
  31. package/components/1_atoms/AwIcon/AwIconSystemMono.vue +3 -2
  32. package/components/1_atoms/AwInput.vue +2 -2
  33. package/components/1_atoms/AwLabel.vue +1 -1
  34. package/components/1_atoms/AwList.vue +3 -1
  35. package/components/1_atoms/AwRadio.vue +1 -1
  36. package/components/1_atoms/AwSlider.vue +15 -1
  37. package/components/1_atoms/AwTag.vue +6 -1
  38. package/components/2_molecules/AwAlert.vue +63 -42
  39. package/components/2_molecules/AwBadge.vue +1 -1
  40. package/components/2_molecules/AwBannerText.vue +8 -2
  41. package/components/2_molecules/AwButton.vue +1 -1
  42. package/components/2_molecules/AwDescriptionInput.vue +19 -1
  43. package/components/2_molecules/AwEmptyContainer.vue +74 -72
  44. package/components/2_molecules/AwNumber.vue +180 -0
  45. package/components/2_molecules/AwSelect.vue +11 -4
  46. package/components/3_organisms/AwBottomBar.vue +22 -4
  47. package/components/3_organisms/AwFilterChosen.vue +73 -0
  48. package/components/3_organisms/AwFilterDateRange.vue +177 -0
  49. package/components/3_organisms/AwFilterMonth.vue +37 -40
  50. package/components/3_organisms/AwFilterSelect.vue +368 -0
  51. package/components/3_organisms/AwMultiBlockBuilder.vue +1 -1
  52. package/components/3_organisms/AwSubnav.vue +11 -1
  53. package/components/3_organisms/AwTable/AwTableBuilder.vue +20 -60
  54. package/components/3_organisms/AwTable/_AwTableCellDropdown.vue +6 -1
  55. package/components/3_organisms/AwTable/_AwTableRow.vue +2 -1
  56. package/components/4_pages/AwPage.vue +1 -0
  57. package/components/4_pages/AwPageAside.vue +108 -0
  58. package/components/5_layouts/AwLayoutCenter.vue +3 -8
  59. package/components/5_layouts/_AwMenuItemIcon.vue +9 -2
  60. package/components/5_layouts/_AwMobileMenuItem.vue +5 -3
  61. package/components/5_layouts/_AwUserMenu.vue +1 -1
  62. package/components/_config.js +26 -1
  63. package/docs/_template.md +80 -0
  64. package/docs/components/atoms/aw-accordion-fold.md +129 -0
  65. package/docs/components/atoms/aw-action-card-body.md +99 -0
  66. package/docs/components/atoms/aw-action-card.md +130 -0
  67. package/docs/components/atoms/aw-action-icon.md +126 -0
  68. package/docs/components/atoms/aw-avatar.md +106 -0
  69. package/docs/components/atoms/aw-card.md +137 -0
  70. package/docs/components/atoms/aw-checkbox.md +288 -0
  71. package/docs/components/atoms/aw-content-placeholder.md +147 -0
  72. package/docs/components/atoms/aw-description.md +83 -0
  73. package/docs/components/atoms/aw-dock.md +90 -0
  74. package/docs/components/atoms/aw-dropdown-button.md +94 -0
  75. package/docs/components/atoms/aw-dropdown.md +178 -0
  76. package/docs/components/atoms/aw-file.md +73 -0
  77. package/docs/components/atoms/aw-flow.md +140 -0
  78. package/docs/components/atoms/aw-grid.md +109 -0
  79. package/docs/components/atoms/aw-headline.md +71 -0
  80. package/docs/components/atoms/aw-icon-system-color.md +122 -0
  81. package/docs/components/atoms/aw-icon-system-mono.md +206 -0
  82. package/docs/components/atoms/aw-icon.md +235 -0
  83. package/docs/components/atoms/aw-info.md +123 -0
  84. package/docs/components/atoms/aw-input.md +212 -0
  85. package/docs/components/atoms/aw-label.md +136 -0
  86. package/docs/components/atoms/aw-link.md +151 -0
  87. package/docs/components/atoms/aw-list.md +152 -0
  88. package/docs/components/atoms/aw-progress.md +119 -0
  89. package/docs/components/atoms/aw-radio.md +182 -0
  90. package/docs/components/atoms/aw-refresh-wrapper.md +81 -0
  91. package/docs/components/atoms/aw-select-native.md +234 -0
  92. package/docs/components/atoms/aw-slider.md +189 -0
  93. package/docs/components/atoms/aw-sub-headline.md +73 -0
  94. package/docs/components/atoms/aw-switcher.md +192 -0
  95. package/docs/components/atoms/aw-tag.md +144 -0
  96. package/docs/components/atoms/aw-title.md +70 -0
  97. package/docs/components/atoms/aw-toggler.md +90 -0
  98. package/docs/components/layouts/aw-layout-center.md +168 -0
  99. package/docs/components/layouts/aw-layout-error.md +153 -0
  100. package/docs/components/layouts/aw-layout-provider.md +238 -0
  101. package/docs/components/layouts/aw-layout.md +88 -0
  102. package/docs/components/molecules/aw-action-button.md +138 -0
  103. package/docs/components/molecules/aw-alert.md +191 -0
  104. package/docs/components/molecules/aw-badge.md +129 -0
  105. package/docs/components/molecules/aw-banner-text.md +156 -0
  106. package/docs/components/molecules/aw-button-nav.md +111 -0
  107. package/docs/components/molecules/aw-button.md +193 -0
  108. package/docs/components/molecules/aw-description-input.md +124 -0
  109. package/docs/components/molecules/aw-empty-container.md +235 -0
  110. package/docs/components/molecules/aw-island.md +506 -0
  111. package/docs/components/molecules/aw-number.md +138 -0
  112. package/docs/components/molecules/aw-select-object.md +401 -0
  113. package/docs/components/molecules/aw-select.md +215 -0
  114. package/docs/components/molecules/aw-tab-nav.md +108 -0
  115. package/docs/components/molecules/aw-tel.md +129 -0
  116. package/docs/components/molecules/aw-textarea.md +83 -0
  117. package/docs/components/molecules/aw-userpic.md +115 -0
  118. package/docs/components/organisms/aw-address-block.md +64 -0
  119. package/docs/components/organisms/aw-address.md +132 -0
  120. package/docs/components/organisms/aw-birthday-picker.md +73 -0
  121. package/docs/components/organisms/aw-bottom-bar.md +66 -0
  122. package/docs/components/organisms/aw-calendar-days.md +115 -0
  123. package/docs/components/organisms/aw-calendar-nav.md +98 -0
  124. package/docs/components/organisms/aw-calendar-view.md +98 -0
  125. package/docs/components/organisms/aw-calendar.md +166 -0
  126. package/docs/components/organisms/aw-chart.md +154 -0
  127. package/docs/components/organisms/aw-chip-select.md +164 -0
  128. package/docs/components/organisms/aw-chip.md +126 -0
  129. package/docs/components/organisms/aw-code-snippet.md +94 -0
  130. package/docs/components/organisms/aw-code.md +132 -0
  131. package/docs/components/organisms/aw-context-menu.md +117 -0
  132. package/docs/components/organisms/aw-cropper.md +151 -0
  133. package/docs/components/organisms/aw-date.md +161 -0
  134. package/docs/components/organisms/aw-display-date.md +33 -0
  135. package/docs/components/organisms/aw-download-link.md +46 -0
  136. package/docs/components/organisms/aw-fetch-data.md +161 -0
  137. package/docs/components/organisms/aw-filter-chosen.md +226 -0
  138. package/docs/components/organisms/aw-filter-date-range.md +205 -0
  139. package/docs/components/organisms/aw-filter-month.md +43 -0
  140. package/docs/components/organisms/aw-filter-select.md +239 -0
  141. package/docs/components/organisms/aw-form.md +174 -0
  142. package/docs/components/organisms/aw-gmap-marker.md +86 -0
  143. package/docs/components/organisms/aw-gmap.md +90 -0
  144. package/docs/components/organisms/aw-image-upload.md +56 -0
  145. package/docs/components/organisms/aw-island-avatar.md +87 -0
  146. package/docs/components/organisms/aw-markdown-editor.md +104 -0
  147. package/docs/components/organisms/aw-modal-buttons.md +57 -0
  148. package/docs/components/organisms/aw-modal.md +246 -0
  149. package/docs/components/organisms/aw-model-edit.md +74 -0
  150. package/docs/components/organisms/aw-money.md +53 -0
  151. package/docs/components/organisms/aw-multi-block-builder.md +165 -0
  152. package/docs/components/organisms/aw-pagination.md +121 -0
  153. package/docs/components/organisms/aw-password.md +103 -0
  154. package/docs/components/organisms/aw-preview-card.md +45 -0
  155. package/docs/components/organisms/aw-search.md +116 -0
  156. package/docs/components/organisms/aw-subnav.md +122 -0
  157. package/docs/components/organisms/aw-table-builder.md +165 -0
  158. package/docs/components/organisms/aw-table-col.md +123 -0
  159. package/docs/components/organisms/aw-table-head.md +92 -0
  160. package/docs/components/organisms/aw-table-row.md +91 -0
  161. package/docs/components/organisms/aw-table.md +172 -0
  162. package/docs/components/organisms/aw-tags.md +54 -0
  163. package/docs/components/organisms/aw-toggle-show-aside.md +43 -0
  164. package/docs/components/organisms/aw-uploader-files.md +125 -0
  165. package/docs/components/organisms/aw-uploader.md +163 -0
  166. package/docs/components/organisms/aw-user-menu.md +87 -0
  167. package/docs/components/pages/aw-page-aside.md +296 -0
  168. package/docs/components/pages/aw-page-menu-buttons.md +172 -0
  169. package/docs/components/pages/aw-page-modal.md +198 -0
  170. package/docs/components/pages/aw-page-single.md +300 -0
  171. package/docs/components/pages/aw-page.md +194 -0
  172. package/docs/configuration.md +493 -0
  173. package/docs/cookbook/advanced-patterns.md +1388 -0
  174. package/docs/cookbook/common-patterns.md +965 -0
  175. package/docs/cookbook/index.md +786 -0
  176. package/docs/getting-started.md +596 -0
  177. package/docs/guides/best-practices.md +1106 -0
  178. package/docs/guides/data-fetching.md +852 -0
  179. package/docs/guides/error-handling.md +1172 -0
  180. package/docs/guides/forms-guide.md +1329 -0
  181. package/docs/guides/mobile-subnavigation.md +359 -0
  182. package/docs/guides/page-patterns/aside-pages.md +1418 -0
  183. package/docs/guides/page-patterns/dashboard-pages.md +990 -0
  184. package/docs/guides/page-patterns/detail-pages.md +1556 -0
  185. package/docs/guides/page-patterns/list-pages.md +1242 -0
  186. package/docs/index.md +263 -1
  187. package/docs/integrations.md +870 -0
  188. package/docs/reference/colors.md +232 -0
  189. package/docs/reference/icons.md +163 -0
  190. package/docs/reference/menu.md +462 -0
  191. package/docs/reference/plugins.md +970 -0
  192. package/docs/reference/troubleshooting.md +964 -0
  193. package/nuxt/awes.config.js +9 -8
  194. package/nuxt/index.js +2 -2
  195. package/nuxt/pages/more.vue +1 -1
  196. package/package.json +5 -3
  197. package/readme.md +171 -1
  198. package/store/awesIo.js +11 -0
  199. package/CHANGELOG.md +0 -4520
  200. package/docs/aw-accordion-fold.md +0 -46
  201. package/docs/aw-address.md +0 -44
  202. package/docs/aw-avatar.md +0 -51
  203. package/docs/aw-badge.md +0 -32
  204. package/docs/aw-button-nav.md +0 -44
  205. package/docs/aw-button.md +0 -50
  206. package/docs/aw-calendar-days.md +0 -46
  207. package/docs/aw-calendar-nav.md +0 -25
  208. package/docs/aw-calendar-view.md +0 -12
  209. package/docs/aw-calendar.md +0 -59
  210. package/docs/aw-card.md +0 -48
  211. package/docs/aw-chart.md +0 -51
  212. package/docs/aw-checkbox.md +0 -56
  213. package/docs/aw-chip-select.md +0 -46
  214. package/docs/aw-chip.md +0 -53
  215. package/docs/aw-code-snippet.md +0 -18
  216. package/docs/aw-code.md +0 -56
  217. package/docs/aw-content-wrapper.md +0 -40
  218. package/docs/aw-context-menu.md +0 -31
  219. package/docs/aw-cropper.md +0 -60
  220. package/docs/aw-dashboard-card.md +0 -37
  221. package/docs/aw-dashboard-donut.md +0 -30
  222. package/docs/aw-dashboard-line.md +0 -20
  223. package/docs/aw-dashboard-progress.md +0 -33
  224. package/docs/aw-dashboard-section.md +0 -32
  225. package/docs/aw-dashboard-speed.md +0 -30
  226. package/docs/aw-date.md +0 -52
  227. package/docs/aw-dropdown-button.md +0 -31
  228. package/docs/aw-dropdown.md +0 -69
  229. package/docs/aw-fetch-data.md +0 -45
  230. package/docs/aw-form.md +0 -52
  231. package/docs/aw-grid.md +0 -48
  232. package/docs/aw-icon.md +0 -50
  233. package/docs/aw-info.md +0 -53
  234. package/docs/aw-input.md +0 -55
  235. package/docs/aw-layout-default.md +0 -30
  236. package/docs/aw-layout-frame-center.md +0 -29
  237. package/docs/aw-layout-simple.md +0 -49
  238. package/docs/aw-link.md +0 -54
  239. package/docs/aw-markdown-editor.md +0 -51
  240. package/docs/aw-modal.md +0 -63
  241. package/docs/aw-multi-block-builder.md +0 -66
  242. package/docs/aw-page.md +0 -36
  243. package/docs/aw-pagination.md +0 -54
  244. package/docs/aw-password.md +0 -48
  245. package/docs/aw-radio.md +0 -54
  246. package/docs/aw-search.md +0 -49
  247. package/docs/aw-select.md +0 -93
  248. package/docs/aw-slider.md +0 -40
  249. package/docs/aw-svg-image.md +0 -19
  250. package/docs/aw-switcher.md +0 -51
  251. package/docs/aw-tab-nav.md +0 -55
  252. package/docs/aw-table-builder.md +0 -58
  253. package/docs/aw-table-col.md +0 -33
  254. package/docs/aw-table-head.md +0 -28
  255. package/docs/aw-table-row.md +0 -33
  256. package/docs/aw-table.md +0 -59
  257. package/docs/aw-tel.md +0 -47
  258. package/docs/aw-textarea.md +0 -47
  259. package/docs/aw-toggler.md +0 -41
  260. package/docs/aw-uploader-files.md +0 -20
  261. package/docs/aw-uploader.md +0 -60
  262. package/docs/aw-user-menu.md +0 -34
  263. package/docs/aw-userpic.md +0 -34
  264. /package/components/{3_organisms → 2_molecules}/AwTel.vue +0 -0
@@ -0,0 +1,1556 @@
1
+ # Detail Page Patterns
2
+
3
+ Complete guide to building create, edit, and view pages using AwPageSingle with forms and validation.
4
+
5
+ ## When to Use AwPageSingle
6
+
7
+ Use `AwPageSingle` for:
8
+ - **Create pages** - Forms for creating new records
9
+ - **Edit pages** - Forms for updating existing records
10
+ - **Detail views** - Display single item details
11
+ - **Single-focus workflows** - Wizards, settings, profiles
12
+
13
+ **Key characteristics:**
14
+ - Can hide aside menu with `hide-menu` prop
15
+ - Full-width content area
16
+ - Action button in header (save, submit, etc.)
17
+ - Best for focused tasks and forms
18
+
19
+ **⚠️ Essential Requirements:**
20
+
21
+ 1. **Always use `layout: 'empty'`**
22
+
23
+ Every page using `AwPageSingle` must include `layout: 'empty'` in the component export:
24
+
25
+ ```javascript
26
+ export default {
27
+ layout: 'empty', // Required for AwPageSingle pages
28
+ // ... rest of component
29
+ }
30
+ ```
31
+
32
+ 2. **Always use vue-mc models for data fetching and creation**
33
+
34
+ Items fetching, creation, and updates MUST use vue-mc models (`BaseModel` from `@awes-io/vue-mc`). Do not use custom `fetch()` API calls or manual HTTP requests.
35
+
36
+ ```javascript
37
+ // ✅ GOOD - Use vue-mc model
38
+ import { BaseModel } from '@awes-io/vue-mc'
39
+
40
+ class Service extends BaseModel {
41
+ defaults() {
42
+ return {
43
+ id: null,
44
+ name: '',
45
+ description: ''
46
+ }
47
+ }
48
+
49
+ routes() {
50
+ return {
51
+ fetch: '/api/services/{id}',
52
+ save: '/api/services',
53
+ update: '/api/services/{id}',
54
+ delete: '/api/services/{id}'
55
+ }
56
+ }
57
+ }
58
+
59
+ export default {
60
+ data() {
61
+ return {
62
+ service: new Service()
63
+ }
64
+ },
65
+ methods: {
66
+ async save() {
67
+ await this.service.save() // Uses vue-mc
68
+ }
69
+ }
70
+ }
71
+
72
+ // ❌ BAD - Don't use custom fetch
73
+ async save() {
74
+ const res = await fetch('/api/services', { ... }) // Wrong!
75
+ }
76
+ ```
77
+
78
+ 3. **CTA button MUST use `:action` prop, not `#buttons` slot**
79
+
80
+ The primary action button (Save, Create, etc.) MUST use the `:action` prop and `@action` event on `AwPageSingle`. The `#buttons` slot is ONLY for secondary actions (Delete, Disable, etc.) wrapped in `AwPageMenuButtons`.
81
+
82
+ ```javascript
83
+ // ✅ GOOD - CTA button via :action prop
84
+ <AwPageSingle
85
+ :action="{ text: 'Save', loading: model.saving }"
86
+ @action="save"
87
+ >
88
+ <template #buttons>
89
+ <AwPageMenuButtons
90
+ v-if="!model.isNew()"
91
+ :items="[{ text: 'Delete', color: 'error', listeners: { click: delete } }]"
92
+ />
93
+ </template>
94
+ </AwPageSingle>
95
+
96
+ // ❌ BAD - CTA button in #buttons slot
97
+ <AwPageSingle>
98
+ <template #buttons>
99
+ <AwButton @click="save">Save</AwButton> // Wrong!
100
+ </template>
101
+ </AwPageSingle>
102
+ ```
103
+
104
+ ## Basic Create/Edit Page
105
+
106
+ ### Minimal Example
107
+
108
+ ```markup
109
+ <template>
110
+ <AwPageSingle
111
+ hide-menu
112
+ :title="pageTitle"
113
+ :action="saveButton"
114
+ @action="save"
115
+ >
116
+ <AwCard title="Customer Details">
117
+ <AwGrid>
118
+ <AwInput
119
+ v-model="customer.name"
120
+ label="Name"
121
+ :error="customer.errors.name"
122
+ required
123
+ />
124
+
125
+ <AwInput
126
+ v-model="customer.email"
127
+ label="Email"
128
+ type="email"
129
+ :error="customer.errors.email"
130
+ required
131
+ />
132
+
133
+ <AwTel
134
+ v-model="customer.phone"
135
+ label="Phone"
136
+ :error="customer.errors.phone"
137
+ />
138
+ </AwGrid>
139
+ </AwCard>
140
+
141
+ <AwCard title="Profile picture">
142
+ <AwImageUpload
143
+ v-model="customer.avatar"
144
+ @loading="customer.saving = $event"
145
+ />
146
+ </AwCard>
147
+ </AwPageSingle>
148
+ </template>
149
+
150
+ <script>
151
+ import Customer from '~/models/Customer'
152
+
153
+ export default {
154
+ layout: 'empty',
155
+
156
+ middleware: 'auth',
157
+
158
+ data() {
159
+ return {
160
+ customer: new Customer(
161
+ { id: this.$route.params.id },
162
+ null,
163
+ { shop_uuid: this.$route.params.shop_uuid }
164
+ )
165
+ }
166
+ },
167
+
168
+ computed: {
169
+ pageTitle() {
170
+ return this.customer.isNew() ? 'Create Customer' : 'Edit Customer'
171
+ },
172
+
173
+ saveButton() {
174
+ return {
175
+ text: 'Save',
176
+ loading: this.customer.saving
177
+ }
178
+ }
179
+ },
180
+
181
+ async mounted() {
182
+ if (!this.customer.isNew()) {
183
+ try {
184
+ await this.customer.fetch()
185
+ } catch (error) {
186
+ this.$notify({
187
+ title: 'Customer not found',
188
+ type: 'error'
189
+ })
190
+ this.$router.push(`/${this.$route.params.shop_uuid}/customers`)
191
+ }
192
+ }
193
+ },
194
+
195
+ methods: {
196
+ async save() {
197
+ try {
198
+ await this.customer.save()
199
+
200
+ this.$notify({
201
+ title: `Customer ${this.customer.isNew() ? 'created' : 'updated'} successfully`,
202
+ type: 'success'
203
+ })
204
+
205
+ this.$router.push(`/${this.$route.params.shop_uuid}/customers`)
206
+ } catch (error) {
207
+ this.$notify({
208
+ title: 'Failed to save customer',
209
+ type: 'error'
210
+ })
211
+ }
212
+ }
213
+ }
214
+ }
215
+ </script>
216
+ ```
217
+
218
+ ## Preview Slot Patterns
219
+
220
+ We have two standard patterns for the `#preview` slot in `AwPageSingle` to maintain visual consistency across the application.
221
+
222
+ ### Human Entities (Users, Customers)
223
+
224
+ Used for people. Displays a large avatar and the person's name.
225
+
226
+ ```markup
227
+ <template #preview>
228
+ <AwCard class="text-center">
229
+ <AwAvatar
230
+ :src="user.avatar"
231
+ :name="userName"
232
+ size="240"
233
+ class="mx-auto"
234
+ />
235
+ <AwHeadline class="mt-4">
236
+ {{ userName }}
237
+ </AwHeadline>
238
+ </AwCard>
239
+ </template>
240
+ ```
241
+
242
+ ### Non-Human Entities (Services, Products, Locations)
243
+
244
+ Used for items with a representative image. Displays a large image with rounded corners.
245
+
246
+ ```markup
247
+ <template #preview>
248
+ <AwCard style="--card-padding-x: 0.5rem; --card-padding-y: 0.5rem;">
249
+ <AwActionIcon
250
+ :size="380"
251
+ :image="item.image ? { src: item.image, alt: item.name } : null"
252
+ icon="awesio/image"
253
+ icon-color="mono-400"
254
+ color="mono-800"
255
+ class="w-full"
256
+ style="--icon-size: 48px; --radius: 0.5rem;"
257
+ />
258
+ <AwHeadline class="p-4 mt-2">
259
+ {{ item.name }}
260
+ </AwHeadline>
261
+ </AwCard>
262
+ </template>
263
+ ```
264
+
265
+ ## Image Uploads
266
+
267
+ For entities with images (avatars or item photos), provide an `AwImageUpload` component in its own `AwCard`.
268
+
269
+ ```markup
270
+ <AwCard title="Profile picture">
271
+ <AwImageUpload
272
+ v-model="user.avatar"
273
+ @loading="user.saving = $event"
274
+ />
275
+ </AwCard>
276
+ ```
277
+
278
+ **Key Points:**
279
+ - ✅ Use a separate `AwCard` with an appropriate title.
280
+ - ✅ Bind `v-model` to the model's image/avatar property.
281
+ - ✅ Listen to `@loading` to set the model's `saving` state, preventing form submission during upload.
282
+
283
+ ## Model Initialization
284
+
285
+ ### Constructor Pattern
286
+
287
+ **Model constructor signature:**
288
+ ```javascript
289
+ new Model(attributes, collection, options)
290
+ ```
291
+
292
+ **Three arguments:**
293
+ 1. `attributes` - Initial data (object)
294
+ 2. `collection` - Parent collection or `null`
295
+ 3. `options` - Additional options (shop_uuid, etc.)
296
+
297
+ ### Create Page (New Model)
298
+
299
+ ```javascript
300
+ data() {
301
+ return {
302
+ customer: new Customer(
303
+ {}, // Empty attributes for new record
304
+ null, // No parent collection
305
+ { shop_uuid: this.$route.params.shop_uuid }
306
+ )
307
+ }
308
+ }
309
+ ```
310
+
311
+ ### Edit Page (Existing Model)
312
+
313
+ ```javascript
314
+ data() {
315
+ return {
316
+ customer: new Customer(
317
+ { id: this.$route.params.id }, // ID from route
318
+ null,
319
+ { shop_uuid: this.$route.params.shop_uuid }
320
+ )
321
+ }
322
+ },
323
+
324
+ async mounted() {
325
+ // Fetch existing data
326
+ if (!this.customer.isNew()) {
327
+ await this.customer.fetch()
328
+ }
329
+ }
330
+ ```
331
+
332
+ ### Handling UUID Routes
333
+
334
+ For routes using 'new' as placeholder:
335
+
336
+ ```javascript
337
+ data() {
338
+ const uuid = this.$route.params.uuid
339
+ return {
340
+ template: new Template(
341
+ uuid === 'new' ? {} : { uuid },
342
+ null,
343
+ { shop_uuid: this.$route.params.shop_uuid }
344
+ )
345
+ }
346
+ },
347
+
348
+ async mounted() {
349
+ if (this.$route.params.uuid !== 'new') {
350
+ await this.template.fetch()
351
+ }
352
+ }
353
+ ```
354
+
355
+ ### Using Same Template for Create/Edit
356
+
357
+ **Best Practice:** Use the same template for both create and edit pages. Write the template in `create.vue` and extend it in `_id.vue`:
358
+
359
+ **`create.vue`** (main template):
360
+ ```javascript
361
+ export default {
362
+ layout: 'empty',
363
+
364
+ data() {
365
+ return {
366
+ service: new Service({ id: this.$route.params.id })
367
+ }
368
+ },
369
+
370
+ computed: {
371
+ pageTitle() {
372
+ return this.service.isNew() ? 'Create service' : 'Edit service'
373
+ },
374
+
375
+ actionButton() {
376
+ return {
377
+ text: this.service.isNew() ? 'Create' : 'Save',
378
+ loading: this.service.saving
379
+ }
380
+ }
381
+ },
382
+
383
+ async mounted() {
384
+ if (!this.service.isNew()) {
385
+ try {
386
+ await this.service.fetch()
387
+ } catch (error) {
388
+ this.$notify({
389
+ title: 'Service not found',
390
+ type: 'error'
391
+ })
392
+ this.$router.push('/services')
393
+ }
394
+ }
395
+ },
396
+
397
+ watch: {
398
+ '$route.params.id': {
399
+ handler(newId) {
400
+ this.service = new Service({ id: newId })
401
+ if (newId) {
402
+ this.service.fetch()
403
+ }
404
+ }
405
+ }
406
+ }
407
+ }
408
+ ```
409
+
410
+ **`_id.vue`** (extends create.vue):
411
+ ```javascript
412
+ <script>
413
+ import CreatePage from './create.vue'
414
+
415
+ export default {
416
+ extends: CreatePage
417
+ }
418
+ </script>
419
+ ```
420
+
421
+ **Benefits:**
422
+ - ✅ Single source of truth for form structure
423
+ - ✅ Automatic create/edit mode detection via `model.isNew()`
424
+ - ✅ Computed props handle dynamic titles and button texts
425
+ - ✅ Less code duplication
426
+
427
+ ## Page Title Patterns
428
+
429
+ ### Dynamic Title Based on State
430
+
431
+ ```javascript
432
+ computed: {
433
+ pageTitle() {
434
+ return this.model.isNew()
435
+ ? 'Create Product'
436
+ : 'Edit Product'
437
+ }
438
+ }
439
+ ```
440
+
441
+ ### Title with Item Name
442
+
443
+ ```javascript
444
+ computed: {
445
+ pageTitle() {
446
+ if (this.model.isNew()) {
447
+ return 'Create Template'
448
+ }
449
+ return this.model.name || 'Edit Template'
450
+ }
451
+ }
452
+ ```
453
+
454
+ ### Title with ID
455
+
456
+ ```javascript
457
+ computed: {
458
+ pageTitle() {
459
+ return this.model.isNew()
460
+ ? 'New Invoice'
461
+ : `Invoice #${this.model.invoice_number}`
462
+ }
463
+ }
464
+ ```
465
+
466
+ ## Person selectors with avatars (users/customers/instructors)
467
+
468
+ For detail/create/edit pages that pick a person (user, customer, instructor), use `AwSelectObject` with `icon` and `option-label` slots to show avatars and a fallback icon.
469
+
470
+ ```markup
471
+ <AwSelectObject
472
+ v-model="selectedPerson"
473
+ :options="searchPeople"
474
+ :option-label="person => `${person.first_name} ${person.last_name}`"
475
+ track-by="id"
476
+ clearable
477
+ >
478
+ <template #icon="{ option }">
479
+ <AwAvatar
480
+ v-if="option"
481
+ class="mx-3"
482
+ :src="option.avatar"
483
+ :name="`${option.first_name} ${option.last_name}`"
484
+ size="24"
485
+ icon="awesio/user"
486
+ />
487
+ <AwActionIcon
488
+ v-else
489
+ class="mx-3 rounded-full"
490
+ icon="awesio/user"
491
+ size="xs"
492
+ />
493
+ </template>
494
+
495
+ <template #option-label="{ option, highlightSearch }">
496
+ <div class="flex items-center gap-2">
497
+ <AwAvatar
498
+ :src="option.avatar"
499
+ :name="`${option.first_name} ${option.last_name}`"
500
+ size="24"
501
+ class="-ml-1"
502
+ icon="awesio/user"
503
+ />
504
+ <div class="leading-tight">
505
+ <div v-html="highlightSearch(`${option.first_name} ${option.last_name}`)" />
506
+ <div v-if="option.email" class="text-sm text-mono-500">
507
+ {{ option.email }}
508
+ </div>
509
+ </div>
510
+ </div>
511
+ </template>
512
+ </AwSelectObject>
513
+
514
+ <script>
515
+ export default {
516
+ methods: {
517
+ searchPeople(search, page) {
518
+ return {
519
+ url: '/api/users',
520
+ params: { search, page }
521
+ }
522
+ }
523
+ }
524
+ }
525
+ </script>
526
+ ```
527
+
528
+ ## Form Layouts
529
+
530
+ ### Single Column Layout
531
+
532
+ Simple forms with basic fields:
533
+
534
+ ```markup
535
+ <template>
536
+ <AwPageSingle hide-menu :title="pageTitle" :action="saveButton" @action="save">
537
+ <AwCard title="Settings">
538
+ <AwGrid>
539
+ <AwInput v-model="settings.company_name" label="Company Name" />
540
+ <AwInput v-model="settings.email" label="Email" />
541
+ <AwTel v-model="settings.phone" label="Phone" />
542
+ <AwTextarea v-model="settings.address" label="Address" />
543
+ </AwGrid>
544
+ </AwCard>
545
+ </AwPageSingle>
546
+ </template>
547
+ ```
548
+
549
+ ### Two-Column Layout
550
+
551
+ Complex forms or forms with sidebar:
552
+
553
+ ```markup
554
+ <template>
555
+ <AwPageSingle hide-menu :title="pageTitle" :action="saveButton" @action="save">
556
+ <AwGrid :col="{ lg: 3 }">
557
+ <!-- Main content (2/3 width) -->
558
+ <div span="{ lg: 2 }" class="space-y-6">
559
+ <AwCard title="Product Details">
560
+ <AwGrid>
561
+ <AwInput v-model="product.name" label="Name" />
562
+ <AwInput v-model="product.sku" label="SKU" />
563
+ <AwTextarea v-model="product.description" label="Description" />
564
+ </AwGrid>
565
+ </AwCard>
566
+
567
+ <AwCard title="Pricing">
568
+ <AwGrid>
569
+ <AwMoney v-model="product.price" label="Price" />
570
+ <AwMoney v-model="product.cost" label="Cost" />
571
+ </AwGrid>
572
+ </AwCard>
573
+ ```
574
+
575
+ **⚠️ Important: Use AwMoney for all money fields**
576
+
577
+ For all money-related fields (price, cost, discount_amount, tax_amount, etc.), always use `AwMoney` instead of `AwInput` with `type="number"`:
578
+
579
+ ```markup
580
+ <!-- ✅ GOOD - Use AwMoney for money fields -->
581
+ <AwMoney v-model="product.price" label="Price" :error="product.errors.price" />
582
+ <AwMoney v-model="product.cost" label="Cost" :error="product.errors.cost" />
583
+ <AwMoney v-model="order.discount_amount" label="Discount" :error="order.errors.discount_amount" />
584
+
585
+ <!-- ❌ BAD - Don't use AwInput with type="number" for money -->
586
+ <AwInput v-model="product.price" label="Price" type="number" />
587
+ </div>
588
+
589
+ <!-- Sidebar (1/3 width) -->
590
+ <div class="space-y-6">
591
+ <AwCard title="Status">
592
+ <AwSwitcher v-model="product.is_active" label="Active" />
593
+ <AwSwitcher v-model="product.is_featured" label="Featured" />
594
+ </AwCard>
595
+
596
+ <AwCard title="Category">
597
+ <AwSelect
598
+ v-model="product.category_id"
599
+ :options="categories"
600
+ label="Category"
601
+ />
602
+ </AwCard>
603
+ </div>
604
+ </AwGrid>
605
+ </AwPageSingle>
606
+ </template>
607
+ ```
608
+
609
+ ### Multiple Sections
610
+
611
+ Large forms organized by sections:
612
+
613
+ ```markup
614
+ <template>
615
+ <AwPageSingle hide-menu :title="pageTitle" :action="saveButton" @action="save">
616
+ <div class="space-y-6">
617
+ <!-- Basic Information -->
618
+ <AwCard title="Basic Information">
619
+ <AwGrid>
620
+ <AwInput v-model="user.first_name" label="First Name" />
621
+ <AwInput v-model="user.last_name" label="Last Name" />
622
+ <AwInput v-model="user.email" label="Email" />
623
+ </AwGrid>
624
+ </AwCard>
625
+
626
+ <!-- Contact Details -->
627
+ <AwCard title="Contact Details">
628
+ <AwGrid>
629
+ <AwTel v-model="user.phone" label="Phone" />
630
+ <AwAddress v-model="user.address" label="Address" />
631
+ </AwGrid>
632
+ </AwCard>
633
+
634
+ <!-- Permissions -->
635
+ <AwCard title="Permissions">
636
+ <AwSelect
637
+ v-model="user.role"
638
+ :options="['user', 'manager', 'admin']"
639
+ label="Role"
640
+ />
641
+ <AwCheckbox v-model="user.can_export" label="Can export data" />
642
+ <AwCheckbox v-model="user.can_import" label="Can import data" />
643
+ </AwCard>
644
+ </div>
645
+ </AwPageSingle>
646
+ </template>
647
+ ```
648
+
649
+ ## Validation & Error Handling
650
+
651
+ ### Field-Level Errors
652
+
653
+ ```markup
654
+ <AwInput
655
+ v-model="model.name"
656
+ label="Name"
657
+ :error="model.errors.name"
658
+ required
659
+ />
660
+ ```
661
+
662
+ **How it works:**
663
+ 1. Model's `save()` sends request to Laravel
664
+ 2. Laravel returns 422 with validation errors
665
+ 3. Model populates `errors` object automatically
666
+ 4. Error displays below field
667
+
668
+ ### Check for Errors After Save
669
+
670
+ ```javascript
671
+ async save() {
672
+ await this.model.save()
673
+
674
+ // Check if validation failed
675
+ if (Object.keys(this.model.errors).length > 0) {
676
+ this.$notify({
677
+ title: 'Please fix validation errors',
678
+ type: 'error'
679
+ })
680
+ return
681
+ }
682
+
683
+ // Success path
684
+ this.$notify({
685
+ title: 'Saved successfully',
686
+ type: 'success'
687
+ })
688
+ this.$router.push('/list')
689
+ }
690
+ ```
691
+
692
+ ### Handle Fetch Errors
693
+
694
+ ```javascript
695
+ async mounted() {
696
+ if (!this.model.isNew()) {
697
+ try {
698
+ await this.model.fetch()
699
+ } catch (error) {
700
+ // 404 - Record not found
701
+ if (error.response?.status === 404) {
702
+ this.$notify({
703
+ title: 'Record not found',
704
+ type: 'error'
705
+ })
706
+ this.$router.push('/list')
707
+ return
708
+ }
709
+
710
+ // Other errors
711
+ this.$notify({
712
+ title: 'Failed to load data',
713
+ type: 'error'
714
+ })
715
+ }
716
+ }
717
+ }
718
+ ```
719
+
720
+ ### Custom Validation Messages
721
+
722
+ ```markup
723
+ <template>
724
+ <AwInput
725
+ v-model="model.email"
726
+ label="Email"
727
+ :error="emailError"
728
+ required
729
+ />
730
+ </template>
731
+
732
+ <script>
733
+ export default {
734
+ computed: {
735
+ emailError() {
736
+ if (this.model.errors.email) {
737
+ return this.model.errors.email
738
+ }
739
+ if (this.model.email && !this.isValidEmail(this.model.email)) {
740
+ return 'Please enter a valid email address'
741
+ }
742
+ return null
743
+ }
744
+ },
745
+
746
+ methods: {
747
+ isValidEmail(email) {
748
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
749
+ }
750
+ }
751
+ }
752
+ </script>
753
+ ```
754
+
755
+ ## Loading & Saving States
756
+
757
+ ### Save Button Loading
758
+
759
+ ```javascript
760
+ computed: {
761
+ saveButton() {
762
+ return {
763
+ text: 'Save',
764
+ loading: this.model.saving // Automatically set by vue-mc
765
+ }
766
+ }
767
+ }
768
+ ```
769
+
770
+ ### Fetch Loading State
771
+
772
+ When loading data without a card wrapper:
773
+
774
+ ```markup
775
+ <template>
776
+ <AwPageSingle hide-menu :title="pageTitle">
777
+ <AwContentPlaceholder v-if="loading" type="form" :lines="6" />
778
+
779
+ <div v-else class="space-y-6">
780
+ <!-- Form content -->
781
+ </div>
782
+ </AwPageSingle>
783
+ </template>
784
+
785
+ <script>
786
+ export default {
787
+ data() {
788
+ return {
789
+ loading: true
790
+ }
791
+ },
792
+
793
+ async mounted() {
794
+ if (!this.model.isNew()) {
795
+ try {
796
+ await this.model.fetch()
797
+ } finally {
798
+ this.loading = false
799
+ }
800
+ } else {
801
+ this.loading = false
802
+ }
803
+ }
804
+ }
805
+ </script>
806
+ ```
807
+
808
+ When using `AwCard`, place the placeholder inside the card to replace the content:
809
+
810
+ ```markup
811
+ <template>
812
+ <AwPageSingle hide-menu :title="pageTitle">
813
+ <AwCard title="Details">
814
+ <AwContentPlaceholder v-if="model.loading && !model.isNew()" type="form" :lines="4" />
815
+
816
+ <AwGrid v-else>
817
+ <AwInput v-model="model.name" label="Name" />
818
+ <AwInput v-model="model.email" label="Email" />
819
+ </AwGrid>
820
+ </AwCard>
821
+ </AwPageSingle>
822
+ </template>
823
+ ```
824
+
825
+ ### Complex Loading State
826
+
827
+ For pages with multiple sections, use multiple placeholders:
828
+
829
+ ```markup
830
+ <template>
831
+ <AwPageSingle hide-menu :title="pageTitle">
832
+ <AwGrid v-if="loading" :col="{ lg: 3 }">
833
+ <div span="{ lg: 2 }">
834
+ <AwContentPlaceholder type="form" :lines="8" />
835
+ </div>
836
+ <div>
837
+ <AwContentPlaceholder type="form" :lines="4" />
838
+ </div>
839
+ </AwGrid>
840
+
841
+ <div v-else class="space-y-6">
842
+ <!-- Form content -->
843
+ </div>
844
+ </AwPageSingle>
845
+ </template>
846
+ ```
847
+
848
+ ### Progress Indicators in Header
849
+
850
+ ```markup
851
+ <template>
852
+ <AwPageSingle hide-menu :title="pageTitle" :action="saveButton" @action="save">
853
+ <!-- Custom header indicators -->
854
+ <template #buttons>
855
+ <div v-if="autoSaving" class="flex items-center gap-2 text-sm text-secondary">
856
+ <AwProgress size="sm" indeterminate />
857
+ <span>Saving...</span>
858
+ </div>
859
+ <div v-else-if="lastSaved" class="text-sm text-success">
860
+ Saved {{ $dayjs(lastSaved).fromNow() }}
861
+ </div>
862
+ </template>
863
+
864
+ <!-- Form content -->
865
+ </AwPageSingle>
866
+ </template>
867
+
868
+ <script>
869
+ export default {
870
+ data() {
871
+ return {
872
+ autoSaving: false,
873
+ lastSaved: null
874
+ }
875
+ },
876
+
877
+ watch: {
878
+ // Auto-save on changes
879
+ 'model.$attributes': {
880
+ handler() {
881
+ this.debouncedSave()
882
+ },
883
+ deep: true
884
+ }
885
+ },
886
+
887
+ methods: {
888
+ debouncedSave: _.debounce(function() {
889
+ this.autoSave()
890
+ }, 2000),
891
+
892
+ async autoSave() {
893
+ if (this.model.isNew()) return
894
+
895
+ this.autoSaving = true
896
+ try {
897
+ await this.model.save()
898
+ this.lastSaved = new Date()
899
+ } catch (error) {
900
+ // Silently fail auto-save
901
+ } finally {
902
+ this.autoSaving = false
903
+ }
904
+ }
905
+ }
906
+ }
907
+ </script>
908
+ ```
909
+
910
+ ## Action Button Patterns
911
+
912
+ **⚠️ Important: CTA button MUST use `:action` prop**
913
+
914
+ The primary action button (Save, Create, etc.) MUST use the `:action` prop and `@action` event on `AwPageSingle`. Do NOT put the CTA button in the `#buttons` slot.
915
+
916
+ The `#buttons` slot is ONLY for secondary actions (Delete, Disable, etc.) and they MUST be wrapped in `AwPageMenuButtons` without `cta` prop.
917
+
918
+ ### Delete Button
919
+
920
+ Use the `#buttons` slot with `AwPageMenuButtons` for delete and other secondary actions:
921
+
922
+ ```markup
923
+ <template>
924
+ <AwPageSingle
925
+ hide-menu
926
+ :title="pageTitle"
927
+ :action="saveButton"
928
+ @action="save"
929
+ >
930
+ <template #buttons>
931
+ <AwPageMenuButtons
932
+ v-if="!model.isNew()"
933
+ :items="[
934
+ {
935
+ text: 'Delete',
936
+ icon: 'awesio/delete',
937
+ color: 'error',
938
+ listeners: { click: deleteItem }
939
+ }
940
+ ]"
941
+ />
942
+ </template>
943
+
944
+ <!-- Form content -->
945
+ </AwPageSingle>
946
+ </template>
947
+
948
+ <script>
949
+ export default {
950
+ computed: {
951
+ saveButton() {
952
+ return {
953
+ text: this.model.isNew() ? 'Create' : 'Save',
954
+ loading: this.model.saving
955
+ }
956
+ }
957
+ },
958
+
959
+ methods: {
960
+ async save() {
961
+ try {
962
+ await this.model.save()
963
+ this.$notify({
964
+ title: `Item ${this.model.isNew() ? 'created' : 'updated'} successfully`,
965
+ type: 'success'
966
+ })
967
+ this.$router.push('/list')
968
+ } catch (error) {
969
+ this.$notify({
970
+ title: 'Failed to save item',
971
+ type: 'error'
972
+ })
973
+ }
974
+ },
975
+
976
+ async deleteItem() {
977
+ try {
978
+ await this.$confirm({
979
+ title: 'Delete Item',
980
+ message: `Are you sure you want to delete "${this.model.name}"?`
981
+ })
982
+
983
+ await this.model.delete()
984
+ this.$notify({
985
+ title: 'Item deleted successfully',
986
+ type: 'success'
987
+ })
988
+ this.$router.push('/list')
989
+ } catch (error) {
990
+ if (this.$confirm.isCancel(error)) {
991
+ return
992
+ }
993
+
994
+ this.$notify({
995
+ title: 'Failed to delete item',
996
+ type: 'error'
997
+ })
998
+ }
999
+ }
1000
+ }
1001
+ }
1002
+ </script>
1003
+ ```
1004
+
1005
+ **Key points:**
1006
+ - ✅ Delete button wrapped in `AwPageMenuButtons`
1007
+ - ✅ Delete button has `color: 'error'` prop
1008
+ - ✅ Delete button only shows when editing (`!model.isNew()`)
1009
+ - ✅ `$confirm` wrapped in try-catch with `$confirm.isCancel()` check
1010
+
1011
+ ### Multiple Secondary Actions
1012
+
1013
+ For multiple secondary actions, use `#buttons` slot with `AwPageMenuButtons`:
1014
+
1015
+ ```markup
1016
+ <template>
1017
+ <AwPageSingle
1018
+ hide-menu
1019
+ :title="pageTitle"
1020
+ :action="publishButton"
1021
+ @action="publish"
1022
+ >
1023
+ <template #buttons>
1024
+ <AwPageMenuButtons
1025
+ :items="[
1026
+ {
1027
+ text: 'Preview',
1028
+ icon: 'awesio/eye',
1029
+ listeners: { click: preview }
1030
+ },
1031
+ {
1032
+ text: 'Save as Draft',
1033
+ icon: 'awesio/save',
1034
+ listeners: { click: saveAsDraft }
1035
+ }
1036
+ ]"
1037
+ />
1038
+ </template>
1039
+
1040
+ <!-- Form content -->
1041
+ </AwPageSingle>
1042
+ </template>
1043
+
1044
+ <script>
1045
+ export default {
1046
+ computed: {
1047
+ publishButton() {
1048
+ return {
1049
+ key: 'publish',
1050
+ label: 'Publish',
1051
+ loading: this.model.saving,
1052
+ color: 'accent'
1053
+ }
1054
+ }
1055
+ },
1056
+
1057
+ methods: {
1058
+ async publish() {
1059
+ // Publish logic
1060
+ },
1061
+
1062
+ preview() {
1063
+ // Preview logic
1064
+ },
1065
+
1066
+ async saveAsDraft() {
1067
+ // Save as draft logic
1068
+ }
1069
+ }
1070
+ }
1071
+ </script>
1072
+ ```
1073
+
1074
+ ### Dropdown Menu for More Actions
1075
+
1076
+ For many secondary actions, use the `#buttons` slot with a dropdown:
1077
+
1078
+ ```markup
1079
+ <template>
1080
+ <AwPageSingle
1081
+ hide-menu
1082
+ :title="pageTitle"
1083
+ :action="saveButton"
1084
+ @action="save"
1085
+ >
1086
+ <template #buttons>
1087
+ <AwDropdown>
1088
+ <template #trigger>
1089
+ <AwButton text="More" icon="dots-vertical" />
1090
+ </template>
1091
+
1092
+ <AwButton @click="duplicate" theme="text" text="Duplicate" />
1093
+ <AwButton @click="archive" theme="text" text="Archive" />
1094
+ <AwButton @click="deleteItem" theme="text" color="error" text="Delete" />
1095
+ </AwDropdown>
1096
+ </template>
1097
+
1098
+ <!-- Form content -->
1099
+ </AwPageSingle>
1100
+ </template>
1101
+ ```
1102
+
1103
+ ## Complete Examples
1104
+
1105
+ ### Simple Create/Edit Page
1106
+
1107
+ ```markup
1108
+ <template>
1109
+ <AwPageSingle
1110
+ hide-menu
1111
+ :title="pageTitle"
1112
+ :action="saveButton"
1113
+ @action="save"
1114
+ >
1115
+ <AwCard title="Template Details">
1116
+ <AwGrid>
1117
+ <AwInput
1118
+ v-model="template.name"
1119
+ label="Template Name"
1120
+ :error="template.errors.name"
1121
+ required
1122
+ />
1123
+
1124
+ <AwSelect
1125
+ v-model="template.channel"
1126
+ :options="['email', 'sms', 'push']"
1127
+ label="Channel"
1128
+ :error="template.errors.channel"
1129
+ required
1130
+ />
1131
+
1132
+ <AwInput
1133
+ v-model="template.subject"
1134
+ label="Subject"
1135
+ :error="template.errors.subject"
1136
+ class="col-span-2"
1137
+ />
1138
+
1139
+ <AwTextarea
1140
+ v-model="template.body"
1141
+ label="Message Body"
1142
+ :error="template.errors.body"
1143
+ :rows="8"
1144
+ class="col-span-2"
1145
+ required
1146
+ />
1147
+
1148
+ <AwSwitcher
1149
+ v-model="template.is_active"
1150
+ label="Active"
1151
+ />
1152
+ </AwGrid>
1153
+ </AwCard>
1154
+ </AwPageSingle>
1155
+ </template>
1156
+
1157
+ <script>
1158
+ import Template from '~/models/Template'
1159
+
1160
+ export default {
1161
+ middleware: 'auth',
1162
+
1163
+ data() {
1164
+ const uuid = this.$route.params.uuid
1165
+ return {
1166
+ template: new Template(
1167
+ uuid === 'new' ? {} : { uuid },
1168
+ null,
1169
+ { shop_uuid: this.$route.params.shop_uuid }
1170
+ )
1171
+ }
1172
+ },
1173
+
1174
+ computed: {
1175
+ pageTitle() {
1176
+ return this.template.isNew() ? 'Create Template' : 'Edit Template'
1177
+ },
1178
+
1179
+ saveButton() {
1180
+ return {
1181
+ text: this.template.isNew() ? 'Create' : 'Save',
1182
+ loading: this.template.saving
1183
+ }
1184
+ }
1185
+ },
1186
+
1187
+ async mounted() {
1188
+ if (this.$route.params.uuid !== 'new') {
1189
+ try {
1190
+ await this.template.fetch()
1191
+ } catch (error) {
1192
+ this.$notify({
1193
+ title: 'Template not found',
1194
+ type: 'error'
1195
+ })
1196
+ this.$router.push(`/${this.$route.params.shop_uuid}/templates`)
1197
+ }
1198
+ }
1199
+ },
1200
+
1201
+ methods: {
1202
+ async save() {
1203
+ try {
1204
+ await this.template.save()
1205
+
1206
+ this.$notify({
1207
+ title: `Template ${this.template.isNew() ? 'created' : 'updated'} successfully`,
1208
+ type: 'success'
1209
+ })
1210
+
1211
+ this.$router.push(`/${this.$route.params.shop_uuid}/templates`)
1212
+ } catch (error) {
1213
+ this.$notify({
1214
+ title: 'Failed to save template',
1215
+ type: 'error'
1216
+ })
1217
+ }
1218
+ }
1219
+ }
1220
+ }
1221
+ </script>
1222
+ ```
1223
+
1224
+ ### Complex Multi-Section Page
1225
+
1226
+ ```markup
1227
+ <template>
1228
+ <AwPageSingle
1229
+ hide-menu
1230
+ :title="pageTitle"
1231
+ :action="saveButton"
1232
+ @action="save"
1233
+ >
1234
+ <template #buttons>
1235
+ <AwPageMenuButtons
1236
+ v-if="!product.isNew()"
1237
+ :items="[
1238
+ {
1239
+ text: 'Delete',
1240
+ icon: 'awesio/delete',
1241
+ color: 'error',
1242
+ listeners: { click: deleteProduct }
1243
+ }
1244
+ ]"
1245
+ />
1246
+ </template>
1247
+
1248
+ <AwGrid :col="{ lg: 3 }">
1249
+ <!-- Main content -->
1250
+ <div span="{ lg: 2 }" class="space-y-6">
1251
+ <!-- Basic Details -->
1252
+ <AwCard title="Product Details">
1253
+ <AwGrid>
1254
+ <AwInput
1255
+ v-model="product.name"
1256
+ label="Product Name"
1257
+ :error="product.errors.name"
1258
+ required
1259
+ class="col-span-2"
1260
+ />
1261
+
1262
+ <AwInput
1263
+ v-model="product.sku"
1264
+ label="SKU"
1265
+ :error="product.errors.sku"
1266
+ />
1267
+
1268
+ <AwSelect
1269
+ v-model="product.category_id"
1270
+ :options="loadCategories"
1271
+ option-label="name"
1272
+ track-by="id"
1273
+ label="Category"
1274
+ :error="product.errors.category_id"
1275
+ />
1276
+
1277
+ <AwTextarea
1278
+ v-model="product.description"
1279
+ label="Description"
1280
+ :error="product.errors.description"
1281
+ :rows="6"
1282
+ class="col-span-2"
1283
+ />
1284
+ </AwGrid>
1285
+ </AwCard>
1286
+
1287
+ <!-- Pricing -->
1288
+ <AwCard title="Pricing">
1289
+ <AwGrid>
1290
+ <AwMoney
1291
+ v-model="product.price"
1292
+ label="Retail Price"
1293
+ :error="product.errors.price"
1294
+ required
1295
+ />
1296
+
1297
+ <AwMoney
1298
+ v-model="product.cost"
1299
+ label="Cost"
1300
+ :error="product.errors.cost"
1301
+ />
1302
+
1303
+ <AwInput
1304
+ v-model="product.compare_at_price"
1305
+ label="Compare at Price"
1306
+ type="number"
1307
+ :error="product.errors.compare_at_price"
1308
+ />
1309
+ </AwGrid>
1310
+ </AwCard>
1311
+
1312
+ <!-- Images -->
1313
+ <AwCard title="Images">
1314
+ <AwUploader
1315
+ v-model="product.images"
1316
+ label="Product Images"
1317
+ :error="product.errors.images"
1318
+ accept="image/*"
1319
+ multiple
1320
+ />
1321
+ </AwCard>
1322
+ </div>
1323
+
1324
+ <!-- Sidebar -->
1325
+ <div class="space-y-6">
1326
+ <!-- Status -->
1327
+ <AwCard title="Status">
1328
+ <div class="space-y-4">
1329
+ <AwSwitcher
1330
+ v-model="product.is_active"
1331
+ label="Active"
1332
+ />
1333
+ <AwSwitcher
1334
+ v-model="product.is_featured"
1335
+ label="Featured"
1336
+ />
1337
+ </div>
1338
+ </AwCard>
1339
+
1340
+ <!-- Inventory -->
1341
+ <AwCard title="Inventory">
1342
+ <AwGrid>
1343
+ <AwInput
1344
+ v-model="product.stock_quantity"
1345
+ label="Stock Quantity"
1346
+ type="number"
1347
+ :error="product.errors.stock_quantity"
1348
+ />
1349
+
1350
+ <AwSwitcher
1351
+ v-model="product.track_inventory"
1352
+ label="Track Inventory"
1353
+ />
1354
+ </AwGrid>
1355
+ </AwCard>
1356
+
1357
+ <!-- Shipping -->
1358
+ <AwCard title="Shipping">
1359
+ <AwGrid>
1360
+ <AwInput
1361
+ v-model="product.weight"
1362
+ label="Weight (kg)"
1363
+ type="number"
1364
+ :error="product.errors.weight"
1365
+ />
1366
+
1367
+ <AwSwitcher
1368
+ v-model="product.requires_shipping"
1369
+ label="Requires Shipping"
1370
+ />
1371
+ </AwGrid>
1372
+ </AwCard>
1373
+ </div>
1374
+ </AwGrid>
1375
+ </AwPageSingle>
1376
+ </template>
1377
+
1378
+ <script>
1379
+ import Product from '~/models/Product'
1380
+
1381
+ export default {
1382
+ middleware: 'auth',
1383
+
1384
+ data() {
1385
+ return {
1386
+ product: new Product(
1387
+ { id: this.$route.params.id },
1388
+ null,
1389
+ { shop_uuid: this.$route.params.shop_uuid }
1390
+ )
1391
+ }
1392
+ },
1393
+
1394
+ computed: {
1395
+ pageTitle() {
1396
+ return this.product.isNew() ? 'Create Product' : 'Edit Product'
1397
+ },
1398
+
1399
+ saveButton() {
1400
+ return {
1401
+ text: 'Save',
1402
+ loading: this.product.saving,
1403
+ color: 'accent'
1404
+ }
1405
+ }
1406
+ },
1407
+
1408
+ async mounted() {
1409
+ if (!this.product.isNew()) {
1410
+ try {
1411
+ await this.product.fetch()
1412
+ } catch (error) {
1413
+ this.$notify({
1414
+ title: 'Product not found',
1415
+ type: 'error'
1416
+ })
1417
+ this.$router.push(`/${this.$route.params.shop_uuid}/products`)
1418
+ }
1419
+ }
1420
+ },
1421
+
1422
+ methods: {
1423
+ loadCategories(search) {
1424
+ const shopUuid = this.$route.params.shop_uuid
1425
+ return `/api/shops/${shopUuid}/categories?search=${search}`
1426
+ },
1427
+
1428
+ async save() {
1429
+ try {
1430
+ await this.product.save()
1431
+
1432
+ this.$notify({
1433
+ title: `Product ${this.product.isNew() ? 'created' : 'updated'} successfully`,
1434
+ type: 'success'
1435
+ })
1436
+
1437
+ this.$router.push(`/${this.$route.params.shop_uuid}/products`)
1438
+ } catch (error) {
1439
+ this.$notify({
1440
+ title: 'Failed to save product',
1441
+ type: 'error'
1442
+ })
1443
+ }
1444
+ },
1445
+
1446
+ async deleteProduct() {
1447
+ try {
1448
+ await this.$confirm({
1449
+ title: 'Delete Product',
1450
+ message: `Are you sure you want to delete "${this.product.name}"?`
1451
+ })
1452
+
1453
+ await this.product.delete()
1454
+ this.$notify({
1455
+ message: 'Product deleted successfully',
1456
+ type: 'success'
1457
+ })
1458
+ this.$router.push(`/${this.$route.params.shop_uuid}/products`)
1459
+ } catch (error) {
1460
+ if (this.$confirm.isCancel(error)) {
1461
+ return
1462
+ }
1463
+
1464
+ this.$notify({
1465
+ message: 'Failed to delete product',
1466
+ type: 'error'
1467
+ })
1468
+ }
1469
+ }
1470
+ }
1471
+ }
1472
+ </script>
1473
+
1474
+ ## Best Practices
1475
+
1476
+ ### 1. Always Check isNew()
1477
+
1478
+ ```javascript
1479
+ computed: {
1480
+ pageTitle() {
1481
+ return this.model.isNew() ? 'Create' : 'Edit'
1482
+ }
1483
+ }
1484
+ ```
1485
+
1486
+ ### 2. Handle Fetch Errors
1487
+
1488
+ ```javascript
1489
+ async mounted() {
1490
+ if (!this.model.isNew()) {
1491
+ try {
1492
+ await this.model.fetch()
1493
+ } catch (error) {
1494
+ // Redirect on error
1495
+ this.$router.push('/list')
1496
+ }
1497
+ }
1498
+ }
1499
+ ```
1500
+
1501
+ ### 3. Handle Save Errors
1502
+
1503
+ The `save()` method throws on validation errors, so no need to check `errors` manually:
1504
+
1505
+ ```javascript
1506
+ try {
1507
+ await this.model.save()
1508
+ // Success path - only reached if save succeeds
1509
+ this.$notify({ title: 'Saved successfully', type: 'success' })
1510
+ } catch (error) {
1511
+ // Error path - handles both validation and network errors
1512
+ this.$notify({ title: 'Failed to save', type: 'error' })
1513
+ }
1514
+ ```
1515
+
1516
+ ### 4. Confirm Destructive Actions
1517
+
1518
+ Always wrap `$confirm` in try-catch and check for cancellation:
1519
+
1520
+ ```javascript
1521
+ async deleteItem() {
1522
+ try {
1523
+ await this.$confirm({
1524
+ title: 'Delete Item',
1525
+ message: `Are you sure you want to delete "${this.model.name}"?`
1526
+ })
1527
+
1528
+ await this.model.delete()
1529
+ this.$notify({ title: 'Deleted successfully', type: 'success' })
1530
+ this.$router.push('/list')
1531
+ } catch (error) {
1532
+ if (this.$confirm.isCancel(error)) {
1533
+ return // User cancelled, do nothing
1534
+ }
1535
+
1536
+ this.$notify({ title: 'Failed to delete', type: 'error' })
1537
+ }
1538
+ }
1539
+ ```
1540
+
1541
+ ### 5. Provide User Feedback
1542
+
1543
+ ```javascript
1544
+ this.$notify({
1545
+ message: 'Saved successfully',
1546
+ type: 'success'
1547
+ })
1548
+ ```
1549
+
1550
+ ## See Also
1551
+
1552
+ - [List Pages](./list-pages.md) - Table-based list pages
1553
+ - [Dashboard Pages](./dashboard-pages.md) - Metrics and overview pages
1554
+ - [AwPageSingle](../../components/pages/aw-page-single.md) - Page component reference
1555
+ - [Forms Guide](../forms-guide.md) - Form patterns and validation
1556
+ - [Error Handling Guide](../error-handling.md) - Error handling patterns