@awes-io/ui 2.142.0 → 2.143.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 (239) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/assets/css/components/_index.css +7 -1
  3. package/assets/css/components/animation.css +38 -32
  4. package/assets/css/components/content-placeholder.css +103 -0
  5. package/assets/css/components/empty-container.css +69 -1
  6. package/assets/css/components/filter-chosen.css +6 -0
  7. package/assets/css/components/filter-date-range.css +17 -1
  8. package/assets/css/components/filter-month.css +23 -17
  9. package/assets/css/components/filter-select.css +11 -0
  10. package/assets/css/components/layout.css +1 -32
  11. package/assets/css/components/modal.css +1 -1
  12. package/assets/css/components/number.css +12 -0
  13. package/assets/css/components/page-aside.css +54 -0
  14. package/assets/js/css.js +1 -1
  15. package/assets/js/icons/mono.js +59 -91
  16. package/assets/js/icons/multicolor.js +1 -31
  17. package/components/1_atoms/AwContentPlaceholder.vue +60 -0
  18. package/components/1_atoms/AwFlow.vue +21 -48
  19. package/components/1_atoms/AwLabel.vue +1 -1
  20. package/components/2_molecules/AwButton.vue +1 -1
  21. package/components/2_molecules/AwEmptyContainer.vue +74 -72
  22. package/components/2_molecules/AwNumber.vue +180 -0
  23. package/components/2_molecules/AwSelect.vue +11 -4
  24. package/components/3_organisms/AwFilterChosen.vue +73 -0
  25. package/components/3_organisms/AwFilterDateRange.vue +177 -0
  26. package/components/3_organisms/AwFilterMonth.vue +37 -40
  27. package/components/3_organisms/AwFilterSelect.vue +368 -0
  28. package/components/3_organisms/AwImageUpload.vue +1 -1
  29. package/components/3_organisms/AwMarkdownEditor.vue +0 -0
  30. package/components/3_organisms/AwMultiBlockBuilder.vue +1 -1
  31. package/components/3_organisms/AwTable/AwTableBuilder.vue +12 -60
  32. package/components/4_pages/AwPageAside.vue +108 -0
  33. package/components/5_layouts/AwLayoutCenter.vue +3 -8
  34. package/components/5_layouts/_AwUserMenu.vue +1 -1
  35. package/dist/css/aw-icons.css +26 -0
  36. package/dist/fonts/aw-icons.svg +18 -0
  37. package/dist/fonts/aw-icons.ttf +0 -0
  38. package/dist/fonts/aw-icons.woff +0 -0
  39. package/dist/fonts/aw-icons.woff2 +0 -0
  40. package/docs/_template.md +80 -0
  41. package/docs/components/atoms/aw-accordion-fold.md +91 -0
  42. package/docs/components/atoms/aw-action-card-body.md +67 -0
  43. package/docs/components/atoms/aw-action-card.md +94 -0
  44. package/docs/components/atoms/aw-action-icon.md +88 -0
  45. package/docs/components/atoms/aw-avatar.md +106 -0
  46. package/docs/components/atoms/aw-card.md +112 -0
  47. package/docs/components/atoms/aw-checkbox.md +112 -0
  48. package/docs/components/atoms/aw-content-placeholder.md +116 -0
  49. package/docs/components/atoms/aw-description.md +83 -0
  50. package/docs/components/atoms/aw-dock.md +84 -0
  51. package/docs/components/atoms/aw-dropdown-button.md +94 -0
  52. package/docs/components/atoms/aw-dropdown.md +128 -0
  53. package/docs/components/atoms/aw-file.md +73 -0
  54. package/docs/components/atoms/aw-flow.md +92 -0
  55. package/docs/components/atoms/aw-grid.md +91 -0
  56. package/docs/components/atoms/aw-headline.md +71 -0
  57. package/docs/components/atoms/aw-icon-system-color.md +121 -0
  58. package/docs/components/atoms/aw-icon-system-mono.md +205 -0
  59. package/docs/components/atoms/aw-icon.md +235 -0
  60. package/docs/components/atoms/aw-info.md +85 -0
  61. package/docs/components/atoms/aw-input.md +120 -0
  62. package/docs/components/atoms/aw-label.md +83 -0
  63. package/docs/components/atoms/aw-link.md +99 -0
  64. package/docs/components/atoms/aw-list.md +88 -0
  65. package/docs/components/atoms/aw-progress.md +70 -0
  66. package/docs/components/atoms/aw-radio.md +109 -0
  67. package/docs/components/atoms/aw-refresh-wrapper.md +81 -0
  68. package/docs/components/atoms/aw-select-native.md +106 -0
  69. package/docs/components/atoms/aw-slider.md +82 -0
  70. package/docs/components/atoms/aw-sub-headline.md +73 -0
  71. package/docs/components/atoms/aw-switcher.md +115 -0
  72. package/docs/components/atoms/aw-tag.md +80 -0
  73. package/docs/components/atoms/aw-title.md +70 -0
  74. package/docs/components/atoms/aw-toggler.md +69 -0
  75. package/docs/components/layouts/aw-layout-center.md +168 -0
  76. package/docs/components/layouts/aw-layout-error.md +153 -0
  77. package/docs/components/layouts/aw-layout-provider.md +238 -0
  78. package/docs/components/layouts/aw-layout.md +88 -0
  79. package/docs/components/molecules/aw-action-button.md +91 -0
  80. package/docs/components/molecules/aw-alert.md +96 -0
  81. package/docs/components/molecules/aw-badge.md +108 -0
  82. package/docs/components/molecules/aw-banner-text.md +90 -0
  83. package/docs/components/molecules/aw-button-nav.md +46 -0
  84. package/docs/components/molecules/aw-button.md +123 -0
  85. package/docs/components/molecules/aw-description-input.md +67 -0
  86. package/docs/components/molecules/aw-empty-container.md +86 -0
  87. package/docs/components/molecules/aw-island.md +234 -0
  88. package/docs/components/molecules/aw-number.md +138 -0
  89. package/docs/components/molecules/aw-select-object.md +401 -0
  90. package/docs/components/molecules/aw-select.md +215 -0
  91. package/docs/components/molecules/aw-tab-nav.md +108 -0
  92. package/docs/components/molecules/aw-tel.md +129 -0
  93. package/docs/components/molecules/aw-textarea.md +83 -0
  94. package/docs/components/molecules/aw-userpic.md +115 -0
  95. package/docs/components/organisms/aw-address-block.md +64 -0
  96. package/docs/components/organisms/aw-address.md +132 -0
  97. package/docs/components/organisms/aw-birthday-picker.md +73 -0
  98. package/docs/components/organisms/aw-bottom-bar.md +66 -0
  99. package/docs/components/organisms/aw-calendar-days.md +115 -0
  100. package/docs/components/organisms/aw-calendar-nav.md +98 -0
  101. package/docs/components/organisms/aw-calendar-view.md +98 -0
  102. package/docs/components/organisms/aw-calendar.md +166 -0
  103. package/docs/components/organisms/aw-chart.md +154 -0
  104. package/docs/components/organisms/aw-chip-select.md +164 -0
  105. package/docs/components/organisms/aw-chip.md +126 -0
  106. package/docs/components/organisms/aw-code-snippet.md +94 -0
  107. package/docs/components/organisms/aw-code.md +132 -0
  108. package/docs/components/organisms/aw-context-menu.md +117 -0
  109. package/docs/components/organisms/aw-cropper.md +151 -0
  110. package/docs/components/organisms/aw-date.md +161 -0
  111. package/docs/components/organisms/aw-display-date.md +33 -0
  112. package/docs/components/organisms/aw-download-link.md +46 -0
  113. package/docs/components/organisms/aw-fetch-data.md +161 -0
  114. package/docs/components/organisms/aw-filter-chosen.md +226 -0
  115. package/docs/components/organisms/aw-filter-date-range.md +205 -0
  116. package/docs/components/organisms/aw-filter-month.md +43 -0
  117. package/docs/components/organisms/aw-filter-select.md +225 -0
  118. package/docs/components/organisms/aw-form.md +174 -0
  119. package/docs/components/organisms/aw-gmap-marker.md +86 -0
  120. package/docs/components/organisms/aw-gmap.md +90 -0
  121. package/docs/components/organisms/aw-image-upload.md +56 -0
  122. package/docs/components/organisms/aw-island-avatar.md +87 -0
  123. package/docs/components/organisms/aw-markdown-editor.md +104 -0
  124. package/docs/components/organisms/aw-modal-buttons.md +57 -0
  125. package/docs/components/organisms/aw-modal.md +246 -0
  126. package/docs/components/organisms/aw-model-edit.md +74 -0
  127. package/docs/components/organisms/aw-money.md +53 -0
  128. package/docs/components/organisms/aw-multi-block-builder.md +165 -0
  129. package/docs/components/organisms/aw-pagination.md +121 -0
  130. package/docs/components/organisms/aw-password.md +103 -0
  131. package/docs/components/organisms/aw-preview-card.md +45 -0
  132. package/docs/components/organisms/aw-search.md +116 -0
  133. package/docs/components/organisms/aw-subnav.md +122 -0
  134. package/docs/components/organisms/aw-table-builder.md +165 -0
  135. package/docs/components/organisms/aw-table-col.md +123 -0
  136. package/docs/components/organisms/aw-table-head.md +92 -0
  137. package/docs/components/organisms/aw-table-row.md +91 -0
  138. package/docs/components/organisms/aw-table.md +172 -0
  139. package/docs/components/organisms/aw-tags.md +54 -0
  140. package/docs/components/organisms/aw-toggle-show-aside.md +43 -0
  141. package/docs/components/organisms/aw-uploader-files.md +125 -0
  142. package/docs/components/organisms/aw-uploader.md +163 -0
  143. package/docs/components/organisms/aw-user-menu.md +87 -0
  144. package/docs/components/pages/aw-page-aside.md +296 -0
  145. package/docs/components/pages/aw-page-menu-buttons.md +172 -0
  146. package/docs/components/pages/aw-page-modal.md +198 -0
  147. package/docs/components/pages/aw-page-single.md +253 -0
  148. package/docs/components/pages/aw-page.md +194 -0
  149. package/docs/configuration.md +493 -0
  150. package/docs/cookbook/advanced-patterns.md +1388 -0
  151. package/docs/cookbook/common-patterns.md +965 -0
  152. package/docs/cookbook/index.md +786 -0
  153. package/docs/getting-started.md +596 -0
  154. package/docs/guides/best-practices.md +1106 -0
  155. package/docs/guides/data-fetching.md +852 -0
  156. package/docs/guides/error-handling.md +1172 -0
  157. package/docs/guides/forms-guide.md +1329 -0
  158. package/docs/guides/mobile-subnavigation.md +359 -0
  159. package/docs/guides/page-patterns/aside-pages.md +1418 -0
  160. package/docs/guides/page-patterns/dashboard-pages.md +990 -0
  161. package/docs/guides/page-patterns/detail-pages.md +1493 -0
  162. package/docs/guides/page-patterns/list-pages.md +1094 -0
  163. package/docs/index.md +263 -1
  164. package/docs/integrations.md +870 -0
  165. package/docs/reference/menu.md +462 -0
  166. package/docs/reference/plugins.md +970 -0
  167. package/docs/reference/troubleshooting.md +945 -0
  168. package/nuxt/awes.config.js +9 -8
  169. package/nuxt/icons.css +26 -0
  170. package/nuxt/index.js +2 -2
  171. package/nuxt/pages/more.vue +1 -1
  172. package/package.json +5 -3
  173. package/readme.md +171 -1
  174. package/docs/aw-accordion-fold.md +0 -46
  175. package/docs/aw-address.md +0 -44
  176. package/docs/aw-avatar.md +0 -51
  177. package/docs/aw-badge.md +0 -32
  178. package/docs/aw-button-nav.md +0 -44
  179. package/docs/aw-button.md +0 -50
  180. package/docs/aw-calendar-days.md +0 -46
  181. package/docs/aw-calendar-nav.md +0 -25
  182. package/docs/aw-calendar-view.md +0 -12
  183. package/docs/aw-calendar.md +0 -59
  184. package/docs/aw-card.md +0 -48
  185. package/docs/aw-chart.md +0 -51
  186. package/docs/aw-checkbox.md +0 -56
  187. package/docs/aw-chip-select.md +0 -46
  188. package/docs/aw-chip.md +0 -53
  189. package/docs/aw-code-snippet.md +0 -18
  190. package/docs/aw-code.md +0 -56
  191. package/docs/aw-content-wrapper.md +0 -40
  192. package/docs/aw-context-menu.md +0 -31
  193. package/docs/aw-cropper.md +0 -60
  194. package/docs/aw-dashboard-card.md +0 -37
  195. package/docs/aw-dashboard-donut.md +0 -30
  196. package/docs/aw-dashboard-line.md +0 -20
  197. package/docs/aw-dashboard-progress.md +0 -33
  198. package/docs/aw-dashboard-section.md +0 -32
  199. package/docs/aw-dashboard-speed.md +0 -30
  200. package/docs/aw-date.md +0 -52
  201. package/docs/aw-dropdown-button.md +0 -31
  202. package/docs/aw-dropdown.md +0 -69
  203. package/docs/aw-fetch-data.md +0 -45
  204. package/docs/aw-form.md +0 -52
  205. package/docs/aw-grid.md +0 -48
  206. package/docs/aw-icon.md +0 -50
  207. package/docs/aw-info.md +0 -53
  208. package/docs/aw-input.md +0 -55
  209. package/docs/aw-layout-default.md +0 -30
  210. package/docs/aw-layout-frame-center.md +0 -29
  211. package/docs/aw-layout-simple.md +0 -49
  212. package/docs/aw-link.md +0 -54
  213. package/docs/aw-markdown-editor.md +0 -51
  214. package/docs/aw-modal.md +0 -63
  215. package/docs/aw-multi-block-builder.md +0 -66
  216. package/docs/aw-page.md +0 -36
  217. package/docs/aw-pagination.md +0 -54
  218. package/docs/aw-password.md +0 -48
  219. package/docs/aw-radio.md +0 -54
  220. package/docs/aw-search.md +0 -49
  221. package/docs/aw-select.md +0 -93
  222. package/docs/aw-slider.md +0 -40
  223. package/docs/aw-svg-image.md +0 -19
  224. package/docs/aw-switcher.md +0 -51
  225. package/docs/aw-tab-nav.md +0 -55
  226. package/docs/aw-table-builder.md +0 -58
  227. package/docs/aw-table-col.md +0 -33
  228. package/docs/aw-table-head.md +0 -28
  229. package/docs/aw-table-row.md +0 -33
  230. package/docs/aw-table.md +0 -59
  231. package/docs/aw-tel.md +0 -47
  232. package/docs/aw-textarea.md +0 -47
  233. package/docs/aw-timeline-builder.md +0 -50
  234. package/docs/aw-toggler.md +0 -41
  235. package/docs/aw-uploader-files.md +0 -20
  236. package/docs/aw-uploader.md +0 -60
  237. package/docs/aw-user-menu.md +0 -34
  238. package/docs/aw-userpic.md +0 -34
  239. /package/components/{3_organisms → 2_molecules}/AwTel.vue +0 -0
@@ -0,0 +1,1418 @@
1
+ # Pages with Aside Sidebar Pattern
2
+
3
+ Complete guide to building pages with persistent sidebar using AwPageAside for forms, configuration, and detail pages with contextual information.
4
+
5
+ ## When to Use AwPageAside
6
+
7
+ Use `AwPageAside` for:
8
+ - **Edit pages with summary** - Forms with pricing, totals, or status sidebar
9
+ - **Configuration pages** - Settings with marketing/info in sidebar
10
+ - **Booking/order pages** - Main content with summary and actions in sidebar
11
+ - **Detail pages with actions** - View details with related info and quick actions
12
+ - **Complex workflows** - Multi-step processes with progress or help in sidebar
13
+
14
+ **Key characteristics:**
15
+ - Fixed sidebar on desktop (right side)
16
+ - Responsive layout (sidebar becomes card on mobile)
17
+ - Sticky action buttons at bottom of sidebar
18
+ - Pass-through support for all AwPage props
19
+ - `isDesktop` prop available in slots for responsive content
20
+
21
+ ## Basic Page with Aside
22
+
23
+ ### Minimal Example
24
+
25
+ ```markup
26
+ <template>
27
+ <AwPageAside title="Edit Booking">
28
+ <template #default>
29
+ <AwCard title="Booking Details">
30
+ <AwGrid>
31
+ <AwDate
32
+ v-model="booking.date"
33
+ label="Date"
34
+ :error="booking.errors.date"
35
+ required
36
+ />
37
+
38
+ <AwInput
39
+ v-model="booking.client_name"
40
+ label="Client Name"
41
+ :error="booking.errors.client_name"
42
+ required
43
+ />
44
+
45
+ <AwSelect
46
+ v-model="booking.service_id"
47
+ :options="services"
48
+ track-by="id"
49
+ option-text="name"
50
+ label="Service"
51
+ :error="booking.errors.service_id"
52
+ required
53
+ />
54
+ </AwGrid>
55
+ </AwCard>
56
+ </template>
57
+
58
+ <template #aside>
59
+ <h3 class="text-lg font-semibold mb-4">Summary</h3>
60
+ <div class="space-y-4">
61
+ <div class="flex justify-between">
62
+ <span class="text-secondary">Service</span>
63
+ <span class="font-semibold">{{ selectedService?.name }}</span>
64
+ </div>
65
+ <div class="flex justify-between">
66
+ <span class="text-secondary">Price</span>
67
+ <span class="font-semibold">{{ formatPrice(selectedService?.price) }}</span>
68
+ </div>
69
+ <hr />
70
+ <div class="flex justify-between text-lg">
71
+ <span class="font-semibold">Total</span>
72
+ <span class="font-bold text-accent">{{ formatPrice(total) }}</span>
73
+ </div>
74
+ </div>
75
+ </template>
76
+
77
+ <template #aside-buttons>
78
+ <AwButton
79
+ @click="save"
80
+ :loading="booking.saving"
81
+ cta
82
+ block
83
+ >
84
+ Save Booking
85
+ </AwButton>
86
+ </template>
87
+ </AwPageAside>
88
+ </template>
89
+
90
+ <script>
91
+ import Booking from '~/models/Booking'
92
+
93
+ export default {
94
+ middleware: 'auth',
95
+
96
+ data() {
97
+ return {
98
+ booking: new Booking(
99
+ { id: this.$route.params.id },
100
+ null,
101
+ { shop_uuid: this.$route.params.shop_uuid }
102
+ ),
103
+ services: []
104
+ }
105
+ },
106
+
107
+ computed: {
108
+ selectedService() {
109
+ return this.services.find(s => s.id === this.booking.service_id)
110
+ },
111
+
112
+ total() {
113
+ return this.selectedService?.price || 0
114
+ }
115
+ },
116
+
117
+ async mounted() {
118
+ await this.loadServices()
119
+
120
+ if (!this.booking.isNew()) {
121
+ await this.booking.fetch()
122
+ }
123
+ },
124
+
125
+ methods: {
126
+ async loadServices() {
127
+ const shopUuid = this.$route.params.shop_uuid
128
+ const response = await this.$axios.get(`/api/shops/${shopUuid}/services`)
129
+ this.services = response.data.data
130
+ },
131
+
132
+ formatPrice(price) {
133
+ return new Intl.NumberFormat('en-US', {
134
+ style: 'currency',
135
+ currency: 'USD'
136
+ }).format(price || 0)
137
+ },
138
+
139
+ async save() {
140
+ try {
141
+ await this.booking.save()
142
+
143
+ if (Object.keys(this.booking.errors).length > 0) {
144
+ this.$notify({
145
+ message: 'Please fix validation errors',
146
+ type: 'error'
147
+ })
148
+ return
149
+ }
150
+
151
+ this.$notify({
152
+ message: 'Booking saved successfully',
153
+ type: 'success'
154
+ })
155
+
156
+ this.$router.push(`/${this.$route.params.shop_uuid}/bookings`)
157
+ } catch (error) {
158
+ this.$notify({
159
+ message: 'Failed to save booking',
160
+ type: 'error'
161
+ })
162
+ }
163
+ },
164
+
165
+ cancel() {
166
+ this.$router.back()
167
+ }
168
+ }
169
+ }
170
+ </script>
171
+ ```
172
+
173
+ **What happens:**
174
+ 1. ✅ Main content area shows booking form
175
+ 2. ✅ Aside shows pricing summary
176
+ 3. ✅ Action buttons fixed at bottom of aside
177
+ 4. ✅ Responsive: sidebar becomes card below content on mobile
178
+ 5. ✅ Total updates when service changes
179
+
180
+ ## Common Patterns
181
+
182
+ ### 1. Booking/Order Page with Dynamic Summary
183
+
184
+ Form with services, client info, and total in sidebar:
185
+
186
+ ```markup
187
+ <template>
188
+ <AwPageAside
189
+ title="New Booking"
190
+ :breadcrumb="{ href: `/bookings`, title: 'Bookings' }"
191
+ >
192
+ <template #default>
193
+ <!-- Service Selection -->
194
+ <AwCard title="Services">
195
+ <div class="space-y-4">
196
+ <div
197
+ v-for="service in services"
198
+ :key="service.id"
199
+ class="border rounded-lg p-4 cursor-pointer hover:border-accent"
200
+ :class="{ 'border-accent bg-accent-50': isSelected(service) }"
201
+ @click="toggleService(service)"
202
+ >
203
+ <div class="flex justify-between items-start">
204
+ <div>
205
+ <h3 class="font-semibold">{{ service.name }}</h3>
206
+ <p class="text-sm text-secondary">{{ service.description }}</p>
207
+ <p class="text-sm text-secondary mt-1">{{ service.duration }} min</p>
208
+ </div>
209
+ <div class="text-right">
210
+ <p class="font-semibold text-accent">{{ formatPrice(service.price) }}</p>
211
+ </div>
212
+ </div>
213
+ </div>
214
+ </div>
215
+ </AwCard>
216
+
217
+ <!-- Client Information -->
218
+ <AwCard title="Client Information">
219
+ <AwGrid>
220
+ <AwInput
221
+ v-model="booking.client_name"
222
+ label="Name"
223
+ :error="booking.errors.client_name"
224
+ required
225
+ />
226
+
227
+ <AwInput
228
+ v-model="booking.client_email"
229
+ label="Email"
230
+ type="email"
231
+ :error="booking.errors.client_email"
232
+ required
233
+ />
234
+
235
+ <AwTel
236
+ v-model="booking.client_phone"
237
+ label="Phone"
238
+ :error="booking.errors.client_phone"
239
+ />
240
+ </AwGrid>
241
+ </AwCard>
242
+
243
+ <!-- Date & Time -->
244
+ <AwCard title="Date & Time">
245
+ <AwGrid>
246
+ <AwDate
247
+ v-model="booking.date"
248
+ label="Date"
249
+ :error="booking.errors.date"
250
+ required
251
+ />
252
+
253
+ <AwSelect
254
+ v-model="booking.time_slot"
255
+ :options="availableSlots"
256
+ label="Time"
257
+ :error="booking.errors.time_slot"
258
+ required
259
+ />
260
+ </AwGrid>
261
+ </AwCard>
262
+
263
+ <!-- Notes -->
264
+ <AwCard title="Additional Notes">
265
+ <AwTextarea
266
+ v-model="booking.notes"
267
+ label="Notes"
268
+ :error="booking.errors.notes"
269
+ :rows="4"
270
+ placeholder="Any special requests or notes..."
271
+ />
272
+ </AwCard>
273
+ </template>
274
+
275
+ <template #aside>
276
+ <h3 class="text-lg font-semibold mb-4">Booking Summary</h3>
277
+
278
+ <!-- Client Info -->
279
+ <div class="mb-6">
280
+ <h4 class="text-sm font-semibold text-secondary mb-2">Client</h4>
281
+ <p class="font-medium">{{ booking.client_name || 'Not specified' }}</p>
282
+ <p class="text-sm text-secondary">{{ booking.client_email || '-' }}</p>
283
+ <p class="text-sm text-secondary">{{ booking.client_phone || '-' }}</p>
284
+ </div>
285
+
286
+ <!-- Date & Time -->
287
+ <div class="mb-6" v-if="booking.date">
288
+ <h4 class="text-sm font-semibold text-secondary mb-2">Date & Time</h4>
289
+ <p class="font-medium">{{ formatDate(booking.date) }}</p>
290
+ <p class="text-sm text-secondary">{{ booking.time_slot || '-' }}</p>
291
+ </div>
292
+
293
+ <!-- Selected Services -->
294
+ <div class="mb-6">
295
+ <h4 class="text-sm font-semibold text-secondary mb-2">Services</h4>
296
+ <div v-if="selectedServices.length === 0" class="text-sm text-secondary">
297
+ No services selected
298
+ </div>
299
+ <div v-else class="space-y-3">
300
+ <div
301
+ v-for="service in selectedServices"
302
+ :key="service.id"
303
+ class="flex justify-between items-start"
304
+ >
305
+ <div class="flex-1">
306
+ <p class="font-medium">{{ service.name }}</p>
307
+ <p class="text-xs text-secondary">{{ service.duration }} min</p>
308
+ </div>
309
+ <div class="text-right">
310
+ <p class="font-medium">{{ formatPrice(service.price) }}</p>
311
+ <button
312
+ @click="removeService(service)"
313
+ class="text-xs text-error hover:underline"
314
+ >
315
+ Remove
316
+ </button>
317
+ </div>
318
+ </div>
319
+ </div>
320
+ </div>
321
+
322
+ <!-- Totals -->
323
+ <hr class="my-4" />
324
+ <div class="space-y-2">
325
+ <div class="flex justify-between text-sm">
326
+ <span class="text-secondary">Subtotal</span>
327
+ <span>{{ formatPrice(subtotal) }}</span>
328
+ </div>
329
+ <div class="flex justify-between text-sm">
330
+ <span class="text-secondary">Duration</span>
331
+ <span>{{ totalDuration }} min</span>
332
+ </div>
333
+ <hr class="my-2" />
334
+ <div class="flex justify-between items-center">
335
+ <span class="text-lg font-semibold">Total</span>
336
+ <span class="text-2xl font-bold text-accent">{{ formatPrice(total) }}</span>
337
+ </div>
338
+ </div>
339
+ </template>
340
+
341
+ <template #aside-buttons>
342
+ <AwButton
343
+ @click="save"
344
+ :loading="booking.saving"
345
+ :disabled="!canSave"
346
+ cta
347
+ block
348
+ >
349
+ Confirm Booking
350
+ </AwButton>
351
+ <AwButton
352
+ @click="cancel"
353
+ theme="outline"
354
+ block
355
+ >
356
+ Cancel
357
+ </AwButton>
358
+ </template>
359
+ </AwPageAside>
360
+ </template>
361
+
362
+ <script>
363
+ import Booking from '~/models/Booking'
364
+
365
+ export default {
366
+ middleware: 'auth',
367
+
368
+ data() {
369
+ return {
370
+ booking: new Booking({}, null, {
371
+ shop_uuid: this.$route.params.shop_uuid
372
+ }),
373
+ services: [],
374
+ selectedServiceIds: [],
375
+ availableSlots: []
376
+ }
377
+ },
378
+
379
+ computed: {
380
+ selectedServices() {
381
+ return this.services.filter(s => this.selectedServiceIds.includes(s.id))
382
+ },
383
+
384
+ subtotal() {
385
+ return this.selectedServices.reduce((sum, s) => sum + s.price, 0)
386
+ },
387
+
388
+ total() {
389
+ return this.subtotal
390
+ },
391
+
392
+ totalDuration() {
393
+ return this.selectedServices.reduce((sum, s) => sum + s.duration, 0)
394
+ },
395
+
396
+ canSave() {
397
+ return this.selectedServices.length > 0 &&
398
+ this.booking.client_name &&
399
+ this.booking.client_email &&
400
+ this.booking.date &&
401
+ this.booking.time_slot
402
+ }
403
+ },
404
+
405
+ async mounted() {
406
+ await this.loadServices()
407
+ await this.loadAvailableSlots()
408
+ },
409
+
410
+ methods: {
411
+ async loadServices() {
412
+ const shopUuid = this.$route.params.shop_uuid
413
+ const response = await this.$axios.get(`/api/shops/${shopUuid}/services`)
414
+ this.services = response.data.data
415
+ },
416
+
417
+ async loadAvailableSlots() {
418
+ const shopUuid = this.$route.params.shop_uuid
419
+ const response = await this.$axios.get(`/api/shops/${shopUuid}/time-slots`)
420
+ this.availableSlots = response.data.data
421
+ },
422
+
423
+ isSelected(service) {
424
+ return this.selectedServiceIds.includes(service.id)
425
+ },
426
+
427
+ toggleService(service) {
428
+ const index = this.selectedServiceIds.indexOf(service.id)
429
+ if (index > -1) {
430
+ this.selectedServiceIds.splice(index, 1)
431
+ } else {
432
+ this.selectedServiceIds.push(service.id)
433
+ }
434
+ },
435
+
436
+ removeService(service) {
437
+ const index = this.selectedServiceIds.indexOf(service.id)
438
+ if (index > -1) {
439
+ this.selectedServiceIds.splice(index, 1)
440
+ }
441
+ },
442
+
443
+ formatPrice(price) {
444
+ return new Intl.NumberFormat('en-US', {
445
+ style: 'currency',
446
+ currency: 'USD'
447
+ }).format(price || 0)
448
+ },
449
+
450
+ formatDate(date) {
451
+ return this.$dayjs(date).format('MMMM D, YYYY')
452
+ },
453
+
454
+ async save() {
455
+ this.booking.service_ids = this.selectedServiceIds
456
+
457
+ try {
458
+ await this.booking.save()
459
+
460
+ if (Object.keys(this.booking.errors).length > 0) {
461
+ this.$notify({
462
+ message: 'Please fix validation errors',
463
+ type: 'error'
464
+ })
465
+ return
466
+ }
467
+
468
+ this.$notify({
469
+ message: 'Booking created successfully',
470
+ type: 'success'
471
+ })
472
+
473
+ this.$router.push(`/${this.$route.params.shop_uuid}/bookings`)
474
+ } catch (error) {
475
+ this.$notify({
476
+ message: 'Failed to create booking',
477
+ type: 'error'
478
+ })
479
+ }
480
+ },
481
+
482
+ cancel() {
483
+ this.$router.back()
484
+ }
485
+ }
486
+ }
487
+ </script>
488
+ ```
489
+
490
+ ### 2. Widget Configuration with Marketing Info
491
+
492
+ Configuration form with promotional content in sidebar:
493
+
494
+ ```markup
495
+ <template>
496
+ <AwPageAside
497
+ title="Configure Widget"
498
+ :breadcrumb="{ href: `/widgets`, title: 'Widgets' }"
499
+ >
500
+ <template #default>
501
+ <!-- Widget Preview -->
502
+ <AwCard title="Widget Preview">
503
+ <div class="border rounded-lg p-6 bg-gray-50">
504
+ <div
505
+ class="widget-preview"
506
+ :style="{
507
+ backgroundColor: widget.background_color,
508
+ color: widget.text_color,
509
+ fontSize: `${widget.font_size}px`
510
+ }"
511
+ >
512
+ <h3>{{ widget.title || 'Widget Title' }}</h3>
513
+ <p>{{ widget.message || 'Widget message will appear here' }}</p>
514
+ </div>
515
+ </div>
516
+ </AwCard>
517
+
518
+ <!-- Appearance Settings -->
519
+ <AwCard title="Appearance">
520
+ <AwGrid>
521
+ <AwInput
522
+ v-model="widget.title"
523
+ label="Title"
524
+ :error="widget.errors.title"
525
+ required
526
+ />
527
+
528
+ <AwTextarea
529
+ v-model="widget.message"
530
+ label="Message"
531
+ :error="widget.errors.message"
532
+ :rows="3"
533
+ required
534
+ />
535
+
536
+ <AwInput
537
+ v-model="widget.background_color"
538
+ label="Background Color"
539
+ type="color"
540
+ :error="widget.errors.background_color"
541
+ />
542
+
543
+ <AwInput
544
+ v-model="widget.text_color"
545
+ label="Text Color"
546
+ type="color"
547
+ :error="widget.errors.text_color"
548
+ />
549
+
550
+ <AwSlider
551
+ v-model="widget.font_size"
552
+ label="Font Size"
553
+ :min="12"
554
+ :max="24"
555
+ :error="widget.errors.font_size"
556
+ />
557
+ </AwGrid>
558
+ </AwCard>
559
+
560
+ <!-- Behavior Settings -->
561
+ <AwCard title="Behavior">
562
+ <AwGrid>
563
+ <AwSelect
564
+ v-model="widget.position"
565
+ :options="['top-left', 'top-right', 'bottom-left', 'bottom-right']"
566
+ label="Position"
567
+ :error="widget.errors.position"
568
+ />
569
+
570
+ <AwNumber
571
+ v-model="widget.delay"
572
+ label="Delay (seconds)"
573
+ :min="0"
574
+ :max="60"
575
+ :error="widget.errors.delay"
576
+ />
577
+
578
+ <AwSwitcher
579
+ v-model="widget.show_close_button"
580
+ label="Show Close Button"
581
+ />
582
+
583
+ <AwSwitcher
584
+ v-model="widget.auto_hide"
585
+ label="Auto Hide"
586
+ />
587
+
588
+ <AwNumber
589
+ v-if="widget.auto_hide"
590
+ v-model="widget.auto_hide_delay"
591
+ label="Auto Hide Delay (seconds)"
592
+ :min="1"
593
+ :max="60"
594
+ :error="widget.errors.auto_hide_delay"
595
+ />
596
+ </AwGrid>
597
+ </AwCard>
598
+
599
+ <!-- Targeting -->
600
+ <AwCard title="Targeting">
601
+ <AwGrid>
602
+ <AwSelect
603
+ v-model="widget.pages"
604
+ :options="pageOptions"
605
+ label="Show on Pages"
606
+ multiple
607
+ :error="widget.errors.pages"
608
+ />
609
+
610
+ <AwSelect
611
+ v-model="widget.devices"
612
+ :options="['desktop', 'tablet', 'mobile']"
613
+ label="Show on Devices"
614
+ multiple
615
+ :error="widget.errors.devices"
616
+ />
617
+ </AwGrid>
618
+ </AwCard>
619
+ </template>
620
+
621
+ <template #aside>
622
+ <!-- Marketing Info -->
623
+ <div class="mb-6">
624
+ <h3 class="text-lg font-semibold mb-4">Why Use Widgets?</h3>
625
+ <div class="space-y-4 text-sm">
626
+ <div class="flex items-start gap-3">
627
+ <AwIcon name="awesio/check-circle" class="text-success mt-0.5" />
628
+ <div>
629
+ <h4 class="font-semibold mb-1">Increase Engagement</h4>
630
+ <p class="text-secondary">
631
+ Capture visitor attention with timely, personalized messages
632
+ </p>
633
+ </div>
634
+ </div>
635
+
636
+ <div class="flex items-start gap-3">
637
+ <AwIcon name="awesio/check-circle" class="text-success mt-0.5" />
638
+ <div>
639
+ <h4 class="font-semibold mb-1">Boost Conversions</h4>
640
+ <p class="text-secondary">
641
+ Drive actions with targeted calls-to-action and special offers
642
+ </p>
643
+ </div>
644
+ </div>
645
+
646
+ <div class="flex items-start gap-3">
647
+ <AwIcon name="awesio/check-circle" class="text-success mt-0.5" />
648
+ <div>
649
+ <h4 class="font-semibold mb-1">Easy to Customize</h4>
650
+ <p class="text-secondary">
651
+ Match your brand with flexible design options
652
+ </p>
653
+ </div>
654
+ </div>
655
+
656
+ <hr class="my-4" />
657
+
658
+ <div class="bg-accent-50 rounded-lg p-4">
659
+ <h4 class="font-semibold text-accent mb-2">Pro Tip</h4>
660
+ <p class="text-secondary text-xs">
661
+ Widgets with a delay of 3-5 seconds have 40% higher engagement
662
+ than immediate popups.
663
+ </p>
664
+ </div>
665
+ </div>
666
+ </div>
667
+
668
+ <!-- Stats (if editing) -->
669
+ <div v-if="!widget.isNew()">
670
+ <h3 class="text-lg font-semibold mb-4">Performance</h3>
671
+ <div class="space-y-3">
672
+ <div class="flex justify-between items-center">
673
+ <span class="text-sm text-secondary">Impressions</span>
674
+ <span class="font-semibold">{{ widget.stats?.impressions || 0 }}</span>
675
+ </div>
676
+ <div class="flex justify-between items-center">
677
+ <span class="text-sm text-secondary">Clicks</span>
678
+ <span class="font-semibold">{{ widget.stats?.clicks || 0 }}</span>
679
+ </div>
680
+ <div class="flex justify-between items-center">
681
+ <span class="text-sm text-secondary">Conversion Rate</span>
682
+ <span class="font-semibold text-accent">
683
+ {{ formatPercentage(widget.stats?.conversion_rate) }}
684
+ </span>
685
+ </div>
686
+ </div>
687
+ </div>
688
+ </template>
689
+
690
+ <template #aside-buttons>
691
+ <AwButton
692
+ @click="save"
693
+ :loading="widget.saving"
694
+ cta
695
+ block
696
+ >
697
+ {{ widget.isNew() ? 'Create Widget' : 'Update Widget' }}
698
+ </AwButton>
699
+ <AwButton
700
+ v-if="!widget.isNew()"
701
+ @click="testWidget"
702
+ theme="outline"
703
+ block
704
+ >
705
+ Test Widget
706
+ </AwButton>
707
+ </template>
708
+ </AwPageAside>
709
+ </template>
710
+
711
+ <script>
712
+ import Widget from '~/models/Widget'
713
+
714
+ export default {
715
+ middleware: 'auth',
716
+
717
+ data() {
718
+ return {
719
+ widget: new Widget(
720
+ { id: this.$route.params.id },
721
+ null,
722
+ { shop_uuid: this.$route.params.shop_uuid }
723
+ ),
724
+ pageOptions: [
725
+ 'home',
726
+ 'products',
727
+ 'checkout',
728
+ 'cart',
729
+ 'account'
730
+ ]
731
+ }
732
+ },
733
+
734
+ async mounted() {
735
+ if (!this.widget.isNew()) {
736
+ await this.widget.fetch()
737
+ } else {
738
+ // Set defaults for new widget
739
+ this.widget.background_color = '#3B82F6'
740
+ this.widget.text_color = '#FFFFFF'
741
+ this.widget.font_size = 16
742
+ this.widget.position = 'bottom-right'
743
+ this.widget.delay = 3
744
+ this.widget.show_close_button = true
745
+ this.widget.devices = ['desktop', 'tablet', 'mobile']
746
+ }
747
+ },
748
+
749
+ methods: {
750
+ async save() {
751
+ try {
752
+ await this.widget.save()
753
+
754
+ if (Object.keys(this.widget.errors).length > 0) {
755
+ this.$notify({
756
+ message: 'Please fix validation errors',
757
+ type: 'error'
758
+ })
759
+ return
760
+ }
761
+
762
+ this.$notify({
763
+ message: `Widget ${this.widget.isNew() ? 'created' : 'updated'} successfully`,
764
+ type: 'success'
765
+ })
766
+
767
+ this.$router.push(`/${this.$route.params.shop_uuid}/widgets`)
768
+ } catch (error) {
769
+ this.$notify({
770
+ message: 'Failed to save widget',
771
+ type: 'error'
772
+ })
773
+ }
774
+ },
775
+
776
+ testWidget() {
777
+ // Open test preview in new window
778
+ const url = `/${this.$route.params.shop_uuid}/widgets/${this.widget.id}/preview`
779
+ window.open(url, '_blank')
780
+ },
781
+
782
+ formatPercentage(value) {
783
+ return `${(value || 0).toFixed(1)}%`
784
+ }
785
+ }
786
+ }
787
+ </script>
788
+ ```
789
+
790
+ ### 3. Edit Form with Dynamic Aside Content
791
+
792
+ Form where aside content changes based on interaction:
793
+
794
+ ```markup
795
+ <template>
796
+ <AwPageAside title="Edit Product">
797
+ <template #default>
798
+ <AwCard title="Product Details">
799
+ <AwGrid>
800
+ <AwInput
801
+ v-model="product.name"
802
+ label="Name"
803
+ :error="product.errors.name"
804
+ required
805
+ />
806
+
807
+ <AwInput
808
+ v-model="product.sku"
809
+ label="SKU"
810
+ :error="product.errors.sku"
811
+ />
812
+
813
+ <AwSelect
814
+ v-model="product.category_id"
815
+ :options="categories"
816
+ track-by="id"
817
+ option-text="name"
818
+ label="Category"
819
+ :error="product.errors.category_id"
820
+ @input="onCategoryChange"
821
+ />
822
+
823
+ <AwTextarea
824
+ v-model="product.description"
825
+ label="Description"
826
+ :error="product.errors.description"
827
+ :rows="4"
828
+ class="col-span-2"
829
+ />
830
+ </AwGrid>
831
+ </AwCard>
832
+
833
+ <AwCard title="Pricing">
834
+ <AwGrid>
835
+ <AwMoney
836
+ v-model="product.price"
837
+ label="Price"
838
+ :error="product.errors.price"
839
+ required
840
+ @input="onPriceChange"
841
+ />
842
+
843
+ <AwMoney
844
+ v-model="product.cost"
845
+ label="Cost"
846
+ :error="product.errors.cost"
847
+ @input="onPriceChange"
848
+ />
849
+ </AwGrid>
850
+ </AwCard>
851
+ </template>
852
+
853
+ <template #aside>
854
+ <!-- Dynamic aside based on editMode -->
855
+ <div v-if="editMode === null">
856
+ <h3 class="text-lg font-semibold mb-4">Product Info</h3>
857
+ <div class="space-y-4">
858
+ <div>
859
+ <span class="text-sm text-secondary">Category</span>
860
+ <p class="font-medium">{{ selectedCategory?.name || '-' }}</p>
861
+ <AwButton
862
+ @click="editMode = 'category'"
863
+ theme="text"
864
+ size="sm"
865
+ class="mt-1"
866
+ >
867
+ Change Category
868
+ </AwButton>
869
+ </div>
870
+
871
+ <hr />
872
+
873
+ <div>
874
+ <span class="text-sm text-secondary">Pricing</span>
875
+ <div class="mt-2 space-y-2">
876
+ <div class="flex justify-between">
877
+ <span class="text-sm">Price</span>
878
+ <span class="font-medium">{{ formatPrice(product.price) }}</span>
879
+ </div>
880
+ <div class="flex justify-between">
881
+ <span class="text-sm">Cost</span>
882
+ <span class="font-medium">{{ formatPrice(product.cost) }}</span>
883
+ </div>
884
+ <div class="flex justify-between text-accent">
885
+ <span class="text-sm font-semibold">Profit</span>
886
+ <span class="font-semibold">{{ formatPrice(profit) }}</span>
887
+ </div>
888
+ <div class="flex justify-between">
889
+ <span class="text-sm">Margin</span>
890
+ <span class="font-medium">{{ marginPercentage }}%</span>
891
+ </div>
892
+ </div>
893
+ </div>
894
+
895
+ <hr />
896
+
897
+ <div>
898
+ <span class="text-sm text-secondary">Status</span>
899
+ <div class="mt-2">
900
+ <AwLabel :color="product.is_active ? 'success' : 'mono'">
901
+ {{ product.is_active ? 'Active' : 'Inactive' }}
902
+ </AwLabel>
903
+ </div>
904
+ </div>
905
+ </div>
906
+ </div>
907
+
908
+ <!-- Category Edit Mode -->
909
+ <div v-else-if="editMode === 'category'">
910
+ <h3 class="text-lg font-semibold mb-4">Select Category</h3>
911
+ <div class="space-y-2">
912
+ <div
913
+ v-for="category in categories"
914
+ :key="category.id"
915
+ class="p-3 border rounded-lg cursor-pointer hover:border-accent"
916
+ :class="{
917
+ 'border-accent bg-accent-50': product.category_id === category.id
918
+ }"
919
+ @click="selectCategory(category)"
920
+ >
921
+ <div class="font-medium">{{ category.name }}</div>
922
+ <div class="text-xs text-secondary">{{ category.description }}</div>
923
+ </div>
924
+ </div>
925
+ </div>
926
+ </template>
927
+
928
+ <template #aside-buttons>
929
+ <AwButton
930
+ v-if="editMode === null"
931
+ @click="save"
932
+ :loading="product.saving"
933
+ cta
934
+ block
935
+ >
936
+ Save Changes
937
+ </AwButton>
938
+ <AwButton
939
+ v-else
940
+ @click="editMode = null"
941
+ theme="outline"
942
+ block
943
+ >
944
+ Done
945
+ </AwButton>
946
+ </template>
947
+ </AwPageAside>
948
+ </template>
949
+
950
+ <script>
951
+ import Product from '~/models/Product'
952
+
953
+ export default {
954
+ middleware: 'auth',
955
+
956
+ data() {
957
+ return {
958
+ product: new Product(
959
+ { id: this.$route.params.id },
960
+ null,
961
+ { shop_uuid: this.$route.params.shop_uuid }
962
+ ),
963
+ categories: [],
964
+ editMode: null // null, 'category', etc.
965
+ }
966
+ },
967
+
968
+ computed: {
969
+ selectedCategory() {
970
+ return this.categories.find(c => c.id === this.product.category_id)
971
+ },
972
+
973
+ profit() {
974
+ return (this.product.price || 0) - (this.product.cost || 0)
975
+ },
976
+
977
+ marginPercentage() {
978
+ if (!this.product.price || this.product.price === 0) return 0
979
+ return ((this.profit / this.product.price) * 100).toFixed(1)
980
+ }
981
+ },
982
+
983
+ async mounted() {
984
+ await this.loadCategories()
985
+ if (!this.product.isNew()) {
986
+ await this.product.fetch()
987
+ }
988
+ },
989
+
990
+ methods: {
991
+ async loadCategories() {
992
+ const shopUuid = this.$route.params.shop_uuid
993
+ const response = await this.$axios.get(`/api/shops/${shopUuid}/categories`)
994
+ this.categories = response.data.data
995
+ },
996
+
997
+ selectCategory(category) {
998
+ this.product.category_id = category.id
999
+ this.editMode = null
1000
+ },
1001
+
1002
+ onCategoryChange() {
1003
+ // Category changed in main form
1004
+ },
1005
+
1006
+ onPriceChange() {
1007
+ // Prices changed, profit/margin will auto-update via computed
1008
+ },
1009
+
1010
+ formatPrice(price) {
1011
+ return new Intl.NumberFormat('en-US', {
1012
+ style: 'currency',
1013
+ currency: 'USD'
1014
+ }).format(price || 0)
1015
+ },
1016
+
1017
+ async save() {
1018
+ try {
1019
+ await this.product.save()
1020
+
1021
+ if (Object.keys(this.product.errors).length > 0) {
1022
+ this.$notify({
1023
+ message: 'Please fix validation errors',
1024
+ type: 'error'
1025
+ })
1026
+ return
1027
+ }
1028
+
1029
+ this.$notify({
1030
+ message: 'Product updated successfully',
1031
+ type: 'success'
1032
+ })
1033
+
1034
+ this.$router.push(`/${this.$route.params.shop_uuid}/products`)
1035
+ } catch (error) {
1036
+ this.$notify({
1037
+ message: 'Failed to save product',
1038
+ type: 'error'
1039
+ })
1040
+ }
1041
+ }
1042
+ }
1043
+ }
1044
+ </script>
1045
+ ```
1046
+
1047
+ ## Responsive Content
1048
+
1049
+ ### Using isDesktop Prop
1050
+
1051
+ All slots receive `isDesktop` prop for responsive rendering:
1052
+
1053
+ ```markup
1054
+ <template>
1055
+ <AwPageAside title="Responsive Page">
1056
+ <template #aside="{ isDesktop }">
1057
+ <!-- Desktop: Full details -->
1058
+ <div v-if="isDesktop">
1059
+ <h3 class="text-lg font-semibold mb-4">Details</h3>
1060
+ <div class="space-y-4">
1061
+ <!-- Full details here -->
1062
+ </div>
1063
+ </div>
1064
+
1065
+ <!-- Mobile: Compact accordion -->
1066
+ <AwAccordionFold v-else title="Details">
1067
+ <div class="space-y-2">
1068
+ <!-- Compact details here -->
1069
+ </div>
1070
+ </AwAccordionFold>
1071
+ </template>
1072
+ </AwPageAside>
1073
+ </template>
1074
+ ```
1075
+
1076
+ ### Custom Mobile Aside Wrapper
1077
+
1078
+ ```markup
1079
+ <template>
1080
+ <AwPageAside title="Custom Mobile">
1081
+ <template #aside>
1082
+ <h3 class="text-lg font-semibold mb-4">Info</h3>
1083
+ <p>Content here</p>
1084
+ </template>
1085
+
1086
+ <!-- Custom wrapper for mobile aside -->
1087
+ <template #mobile-aside="{ isDesktop }">
1088
+ <div v-if="!isDesktop" class="bg-gray-50 rounded-lg p-4">
1089
+ <slot name="aside" />
1090
+ </div>
1091
+ </template>
1092
+ </AwPageAside>
1093
+ </template>
1094
+ ```
1095
+
1096
+ ### Custom Desktop Breakpoint
1097
+
1098
+ ```markup
1099
+ <template>
1100
+ <AwPageAside
1101
+ title="Custom Breakpoint"
1102
+ desktop-from="xl"
1103
+ >
1104
+ <!-- Switches to desktop layout at xl breakpoint instead of lg -->
1105
+ </AwPageAside>
1106
+ </template>
1107
+ ```
1108
+
1109
+ ## Best Practices
1110
+
1111
+ ### 1. Use Aside for Contextual Information
1112
+
1113
+ **Good:**
1114
+ ```markup
1115
+ <!-- Summary, totals, status, quick actions -->
1116
+ <template #aside>
1117
+ <h3 class="text-lg font-semibold mb-4">Order Summary</h3>
1118
+ <div>Total: {{ total }}</div>
1119
+ </template>
1120
+ ```
1121
+
1122
+ **Avoid:**
1123
+ ```markup
1124
+ <!-- Don't put primary form fields in aside -->
1125
+ <template #aside>
1126
+ <AwInput v-model="product.name" label="Name" />
1127
+ </template>
1128
+ ```
1129
+
1130
+ ### 2. Sticky Buttons for Primary Actions
1131
+
1132
+ ```markup
1133
+ <template #aside-buttons>
1134
+ <AwButton @click="save" cta block>Save</AwButton>
1135
+ <AwButton @click="cancel" theme="outline" block>Cancel</AwButton>
1136
+ </template>
1137
+ ```
1138
+
1139
+ ### 3. Update Aside Reactively
1140
+
1141
+ ```markup
1142
+ <script>
1143
+ export default {
1144
+ computed: {
1145
+ total() {
1146
+ // Reactive calculation
1147
+ return this.items.reduce((sum, item) => sum + item.price, 0)
1148
+ }
1149
+ }
1150
+ }
1151
+ </script>
1152
+
1153
+ <template>
1154
+ <template #aside>
1155
+ <div>Total: {{ total }}</div> <!-- Updates automatically -->
1156
+ </template>
1157
+ </template>
1158
+ ```
1159
+
1160
+ ### 4. Hide Mobile Aside When Not Needed
1161
+
1162
+ ```markup
1163
+ <AwPageAside
1164
+ title="Desktop Only Sidebar"
1165
+ hide-mobile-aside
1166
+ >
1167
+ <!-- Aside only shows on desktop -->
1168
+ </AwPageAside>
1169
+ ```
1170
+
1171
+ ### 5. Visual Separator on Desktop
1172
+
1173
+ ```markup
1174
+ <AwPageAside
1175
+ title="Page Title"
1176
+ modifiers="line"
1177
+ >
1178
+ <!-- Adds vertical line between main and aside on desktop -->
1179
+ </AwPageAside>
1180
+ ```
1181
+
1182
+ ## Complete Example: Order Edit Page
1183
+
1184
+ ```markup
1185
+ <template>
1186
+ <AwPageAside
1187
+ :title="`Order #${order.order_number || 'New'}`"
1188
+ :breadcrumb="{ href: '/orders', title: 'Orders' }"
1189
+ modifiers="line"
1190
+ >
1191
+ <template #default>
1192
+ <!-- Order Items -->
1193
+ <AwCard title="Order Items">
1194
+ <AwTableBuilder
1195
+ :collection="order.items"
1196
+ :fields="itemFields"
1197
+ />
1198
+ <AwButton
1199
+ @click="addItem"
1200
+ theme="text"
1201
+ icon="awesio/plus"
1202
+ class="mt-4"
1203
+ >
1204
+ Add Item
1205
+ </AwButton>
1206
+ </AwCard>
1207
+
1208
+ <!-- Customer Information -->
1209
+ <AwCard title="Customer">
1210
+ <AwGrid>
1211
+ <AwInput
1212
+ v-model="order.customer_name"
1213
+ label="Name"
1214
+ :error="order.errors.customer_name"
1215
+ required
1216
+ />
1217
+ <AwInput
1218
+ v-model="order.customer_email"
1219
+ label="Email"
1220
+ :error="order.errors.customer_email"
1221
+ required
1222
+ />
1223
+ <AwTel
1224
+ v-model="order.customer_phone"
1225
+ label="Phone"
1226
+ :error="order.errors.customer_phone"
1227
+ />
1228
+ </AwGrid>
1229
+ </AwCard>
1230
+
1231
+ <!-- Shipping Address -->
1232
+ <AwCard title="Shipping Address">
1233
+ <AwAddress
1234
+ v-model="order.shipping_address"
1235
+ :error="order.errors.shipping_address"
1236
+ />
1237
+ </AwCard>
1238
+ </template>
1239
+
1240
+ <template #aside>
1241
+ <div class="mb-6">
1242
+ <h3 class="text-lg font-semibold mb-4">Order Summary</h3>
1243
+
1244
+ <!-- Items -->
1245
+ <div class="space-y-3 mb-4">
1246
+ <div
1247
+ v-for="item in order.items"
1248
+ :key="item.id"
1249
+ class="flex justify-between text-sm"
1250
+ >
1251
+ <div>
1252
+ <div class="font-medium">{{ item.name }}</div>
1253
+ <div class="text-secondary">Qty: {{ item.quantity }}</div>
1254
+ </div>
1255
+ <div class="text-right">
1256
+ {{ formatPrice(item.price * item.quantity) }}
1257
+ </div>
1258
+ </div>
1259
+ </div>
1260
+
1261
+ <!-- Totals -->
1262
+ <hr class="my-4" />
1263
+ <div class="space-y-2">
1264
+ <div class="flex justify-between text-sm">
1265
+ <span class="text-secondary">Subtotal</span>
1266
+ <span>{{ formatPrice(subtotal) }}</span>
1267
+ </div>
1268
+ <div class="flex justify-between text-sm">
1269
+ <span class="text-secondary">Tax</span>
1270
+ <span>{{ formatPrice(tax) }}</span>
1271
+ </div>
1272
+ <div class="flex justify-between text-sm">
1273
+ <span class="text-secondary">Shipping</span>
1274
+ <span>{{ formatPrice(shipping) }}</span>
1275
+ </div>
1276
+ <hr class="my-2" />
1277
+ <div class="flex justify-between">
1278
+ <span class="font-semibold">Total</span>
1279
+ <span class="text-xl font-bold text-accent">
1280
+ {{ formatPrice(total) }}
1281
+ </span>
1282
+ </div>
1283
+ </div>
1284
+ </div>
1285
+
1286
+ <!-- Status -->
1287
+ <div>
1288
+ <h3 class="text-lg font-semibold mb-4">Status</h3>
1289
+ <AwSelect
1290
+ v-model="order.status"
1291
+ :options="['pending', 'processing', 'shipped', 'delivered', 'cancelled']"
1292
+ label="Order Status"
1293
+ :error="order.errors.status"
1294
+ />
1295
+ </div>
1296
+ </template>
1297
+
1298
+ <template #aside-buttons>
1299
+ <AwButton
1300
+ @click="save"
1301
+ :loading="order.saving"
1302
+ cta
1303
+ block
1304
+ >
1305
+ {{ order.isNew() ? 'Create Order' : 'Update Order' }}
1306
+ </AwButton>
1307
+ <AwButton
1308
+ v-if="!order.isNew()"
1309
+ @click="printInvoice"
1310
+ theme="outline"
1311
+ block
1312
+ >
1313
+ Print Invoice
1314
+ </AwButton>
1315
+ </template>
1316
+ </AwPageAside>
1317
+ </template>
1318
+
1319
+ <script>
1320
+ import Order from '~/models/Order'
1321
+
1322
+ export default {
1323
+ middleware: 'auth',
1324
+
1325
+ data() {
1326
+ return {
1327
+ order: new Order(
1328
+ { id: this.$route.params.id },
1329
+ null,
1330
+ { shop_uuid: this.$route.params.shop_uuid }
1331
+ ),
1332
+ itemFields: [
1333
+ { key: 'name', label: 'Product' },
1334
+ { key: 'quantity', label: 'Qty' },
1335
+ { key: 'price', label: 'Price', format: this.formatPrice }
1336
+ ]
1337
+ }
1338
+ },
1339
+
1340
+ computed: {
1341
+ subtotal() {
1342
+ return this.order.items?.reduce((sum, item) =>
1343
+ sum + (item.price * item.quantity), 0
1344
+ ) || 0
1345
+ },
1346
+
1347
+ tax() {
1348
+ return this.subtotal * 0.1 // 10% tax
1349
+ },
1350
+
1351
+ shipping() {
1352
+ return 10 // Fixed shipping
1353
+ },
1354
+
1355
+ total() {
1356
+ return this.subtotal + this.tax + this.shipping
1357
+ }
1358
+ },
1359
+
1360
+ async mounted() {
1361
+ if (!this.order.isNew()) {
1362
+ await this.order.fetch()
1363
+ }
1364
+ },
1365
+
1366
+ methods: {
1367
+ formatPrice(price) {
1368
+ return new Intl.NumberFormat('en-US', {
1369
+ style: 'currency',
1370
+ currency: 'USD'
1371
+ }).format(price || 0)
1372
+ },
1373
+
1374
+ addItem() {
1375
+ // Open modal to add item
1376
+ },
1377
+
1378
+ async save() {
1379
+ try {
1380
+ await this.order.save()
1381
+
1382
+ if (Object.keys(this.order.errors).length > 0) {
1383
+ this.$notify({
1384
+ message: 'Please fix validation errors',
1385
+ type: 'error'
1386
+ })
1387
+ return
1388
+ }
1389
+
1390
+ this.$notify({
1391
+ message: `Order ${this.order.isNew() ? 'created' : 'updated'} successfully`,
1392
+ type: 'success'
1393
+ })
1394
+
1395
+ this.$router.push(`/${this.$route.params.shop_uuid}/orders`)
1396
+ } catch (error) {
1397
+ this.$notify({
1398
+ message: 'Failed to save order',
1399
+ type: 'error'
1400
+ })
1401
+ }
1402
+ },
1403
+
1404
+ printInvoice() {
1405
+ window.print()
1406
+ }
1407
+ }
1408
+ }
1409
+ </script>
1410
+ ```
1411
+
1412
+ ## See Also
1413
+
1414
+ - [Detail Pages](./detail-pages.md) - Single-focus edit pages with AwPageSingle
1415
+ - [List Pages](./list-pages.md) - Table-based list pages
1416
+ - [AwPageAside](../../components/pages/aw-page-aside.md) - Component reference
1417
+ - [AwPage](../../components/pages/aw-page.md) - Base page component
1418
+ - [Forms Guide](../forms-guide.md) - Form patterns and validation