@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.
- package/assets/css/components/_index.css +7 -1
- package/assets/css/components/action-card.css +1 -0
- package/assets/css/components/action-icon.css +2 -2
- package/assets/css/components/alert.css +28 -22
- package/assets/css/components/animation.css +52 -32
- package/assets/css/components/badge.css +1 -0
- package/assets/css/components/banner-text.css +15 -4
- package/assets/css/components/card.css +0 -1
- package/assets/css/components/content-placeholder.css +104 -0
- package/assets/css/components/dropdown.css +20 -7
- package/assets/css/components/empty-container.css +69 -1
- package/assets/css/components/filter-chosen.css +6 -0
- package/assets/css/components/filter-date-range.css +17 -1
- package/assets/css/components/filter-month.css +23 -17
- package/assets/css/components/filter-select.css +11 -0
- package/assets/css/components/icon-menu-item.css +12 -7
- package/assets/css/components/layout.css +1 -32
- package/assets/css/components/mobile-menu-nav.css +8 -4
- package/assets/css/components/modal.css +1 -1
- package/assets/css/components/number.css +12 -0
- package/assets/css/components/page-aside.css +54 -0
- package/assets/css/components/text-field.css +4 -0
- package/assets/js/css.js +1 -1
- package/assets/js/icons/mono.js +59 -91
- package/assets/js/icons/multicolor.js +1 -31
- package/components/1_atoms/AwActionIcon.vue +11 -2
- package/components/1_atoms/AwContentPlaceholder.vue +60 -0
- package/components/1_atoms/AwFlow.vue +37 -49
- package/components/1_atoms/AwGrid.vue +11 -3
- package/components/1_atoms/AwIcon/AwIcon.vue +5 -3
- package/components/1_atoms/AwIcon/AwIconSystemMono.vue +3 -2
- package/components/1_atoms/AwInput.vue +2 -2
- package/components/1_atoms/AwLabel.vue +1 -1
- package/components/1_atoms/AwList.vue +3 -1
- package/components/1_atoms/AwRadio.vue +1 -1
- package/components/1_atoms/AwSlider.vue +15 -1
- package/components/1_atoms/AwTag.vue +6 -1
- package/components/2_molecules/AwAlert.vue +63 -42
- package/components/2_molecules/AwBadge.vue +1 -1
- package/components/2_molecules/AwBannerText.vue +8 -2
- package/components/2_molecules/AwButton.vue +1 -1
- package/components/2_molecules/AwDescriptionInput.vue +19 -1
- package/components/2_molecules/AwEmptyContainer.vue +74 -72
- package/components/2_molecules/AwNumber.vue +180 -0
- package/components/2_molecules/AwSelect.vue +11 -4
- package/components/3_organisms/AwBottomBar.vue +22 -4
- package/components/3_organisms/AwFilterChosen.vue +73 -0
- package/components/3_organisms/AwFilterDateRange.vue +177 -0
- package/components/3_organisms/AwFilterMonth.vue +37 -40
- package/components/3_organisms/AwFilterSelect.vue +368 -0
- package/components/3_organisms/AwMultiBlockBuilder.vue +1 -1
- package/components/3_organisms/AwSubnav.vue +11 -1
- package/components/3_organisms/AwTable/AwTableBuilder.vue +20 -60
- package/components/3_organisms/AwTable/_AwTableCellDropdown.vue +6 -1
- package/components/3_organisms/AwTable/_AwTableRow.vue +2 -1
- package/components/4_pages/AwPage.vue +1 -0
- package/components/4_pages/AwPageAside.vue +108 -0
- package/components/5_layouts/AwLayoutCenter.vue +3 -8
- package/components/5_layouts/_AwMenuItemIcon.vue +9 -2
- package/components/5_layouts/_AwMobileMenuItem.vue +5 -3
- package/components/5_layouts/_AwUserMenu.vue +1 -1
- package/components/_config.js +26 -1
- package/docs/_template.md +80 -0
- package/docs/components/atoms/aw-accordion-fold.md +129 -0
- package/docs/components/atoms/aw-action-card-body.md +99 -0
- package/docs/components/atoms/aw-action-card.md +130 -0
- package/docs/components/atoms/aw-action-icon.md +126 -0
- package/docs/components/atoms/aw-avatar.md +106 -0
- package/docs/components/atoms/aw-card.md +137 -0
- package/docs/components/atoms/aw-checkbox.md +288 -0
- package/docs/components/atoms/aw-content-placeholder.md +147 -0
- package/docs/components/atoms/aw-description.md +83 -0
- package/docs/components/atoms/aw-dock.md +90 -0
- package/docs/components/atoms/aw-dropdown-button.md +94 -0
- package/docs/components/atoms/aw-dropdown.md +178 -0
- package/docs/components/atoms/aw-file.md +73 -0
- package/docs/components/atoms/aw-flow.md +140 -0
- package/docs/components/atoms/aw-grid.md +109 -0
- package/docs/components/atoms/aw-headline.md +71 -0
- package/docs/components/atoms/aw-icon-system-color.md +122 -0
- package/docs/components/atoms/aw-icon-system-mono.md +206 -0
- package/docs/components/atoms/aw-icon.md +235 -0
- package/docs/components/atoms/aw-info.md +123 -0
- package/docs/components/atoms/aw-input.md +212 -0
- package/docs/components/atoms/aw-label.md +136 -0
- package/docs/components/atoms/aw-link.md +151 -0
- package/docs/components/atoms/aw-list.md +152 -0
- package/docs/components/atoms/aw-progress.md +119 -0
- package/docs/components/atoms/aw-radio.md +182 -0
- package/docs/components/atoms/aw-refresh-wrapper.md +81 -0
- package/docs/components/atoms/aw-select-native.md +234 -0
- package/docs/components/atoms/aw-slider.md +189 -0
- package/docs/components/atoms/aw-sub-headline.md +73 -0
- package/docs/components/atoms/aw-switcher.md +192 -0
- package/docs/components/atoms/aw-tag.md +144 -0
- package/docs/components/atoms/aw-title.md +70 -0
- package/docs/components/atoms/aw-toggler.md +90 -0
- package/docs/components/layouts/aw-layout-center.md +168 -0
- package/docs/components/layouts/aw-layout-error.md +153 -0
- package/docs/components/layouts/aw-layout-provider.md +238 -0
- package/docs/components/layouts/aw-layout.md +88 -0
- package/docs/components/molecules/aw-action-button.md +138 -0
- package/docs/components/molecules/aw-alert.md +191 -0
- package/docs/components/molecules/aw-badge.md +129 -0
- package/docs/components/molecules/aw-banner-text.md +156 -0
- package/docs/components/molecules/aw-button-nav.md +111 -0
- package/docs/components/molecules/aw-button.md +193 -0
- package/docs/components/molecules/aw-description-input.md +124 -0
- package/docs/components/molecules/aw-empty-container.md +235 -0
- package/docs/components/molecules/aw-island.md +506 -0
- package/docs/components/molecules/aw-number.md +138 -0
- package/docs/components/molecules/aw-select-object.md +401 -0
- package/docs/components/molecules/aw-select.md +215 -0
- package/docs/components/molecules/aw-tab-nav.md +108 -0
- package/docs/components/molecules/aw-tel.md +129 -0
- package/docs/components/molecules/aw-textarea.md +83 -0
- package/docs/components/molecules/aw-userpic.md +115 -0
- package/docs/components/organisms/aw-address-block.md +64 -0
- package/docs/components/organisms/aw-address.md +132 -0
- package/docs/components/organisms/aw-birthday-picker.md +73 -0
- package/docs/components/organisms/aw-bottom-bar.md +66 -0
- package/docs/components/organisms/aw-calendar-days.md +115 -0
- package/docs/components/organisms/aw-calendar-nav.md +98 -0
- package/docs/components/organisms/aw-calendar-view.md +98 -0
- package/docs/components/organisms/aw-calendar.md +166 -0
- package/docs/components/organisms/aw-chart.md +154 -0
- package/docs/components/organisms/aw-chip-select.md +164 -0
- package/docs/components/organisms/aw-chip.md +126 -0
- package/docs/components/organisms/aw-code-snippet.md +94 -0
- package/docs/components/organisms/aw-code.md +132 -0
- package/docs/components/organisms/aw-context-menu.md +117 -0
- package/docs/components/organisms/aw-cropper.md +151 -0
- package/docs/components/organisms/aw-date.md +161 -0
- package/docs/components/organisms/aw-display-date.md +33 -0
- package/docs/components/organisms/aw-download-link.md +46 -0
- package/docs/components/organisms/aw-fetch-data.md +161 -0
- package/docs/components/organisms/aw-filter-chosen.md +226 -0
- package/docs/components/organisms/aw-filter-date-range.md +205 -0
- package/docs/components/organisms/aw-filter-month.md +43 -0
- package/docs/components/organisms/aw-filter-select.md +239 -0
- package/docs/components/organisms/aw-form.md +174 -0
- package/docs/components/organisms/aw-gmap-marker.md +86 -0
- package/docs/components/organisms/aw-gmap.md +90 -0
- package/docs/components/organisms/aw-image-upload.md +56 -0
- package/docs/components/organisms/aw-island-avatar.md +87 -0
- package/docs/components/organisms/aw-markdown-editor.md +104 -0
- package/docs/components/organisms/aw-modal-buttons.md +57 -0
- package/docs/components/organisms/aw-modal.md +246 -0
- package/docs/components/organisms/aw-model-edit.md +74 -0
- package/docs/components/organisms/aw-money.md +53 -0
- package/docs/components/organisms/aw-multi-block-builder.md +165 -0
- package/docs/components/organisms/aw-pagination.md +121 -0
- package/docs/components/organisms/aw-password.md +103 -0
- package/docs/components/organisms/aw-preview-card.md +45 -0
- package/docs/components/organisms/aw-search.md +116 -0
- package/docs/components/organisms/aw-subnav.md +122 -0
- package/docs/components/organisms/aw-table-builder.md +165 -0
- package/docs/components/organisms/aw-table-col.md +123 -0
- package/docs/components/organisms/aw-table-head.md +92 -0
- package/docs/components/organisms/aw-table-row.md +91 -0
- package/docs/components/organisms/aw-table.md +172 -0
- package/docs/components/organisms/aw-tags.md +54 -0
- package/docs/components/organisms/aw-toggle-show-aside.md +43 -0
- package/docs/components/organisms/aw-uploader-files.md +125 -0
- package/docs/components/organisms/aw-uploader.md +163 -0
- package/docs/components/organisms/aw-user-menu.md +87 -0
- package/docs/components/pages/aw-page-aside.md +296 -0
- package/docs/components/pages/aw-page-menu-buttons.md +172 -0
- package/docs/components/pages/aw-page-modal.md +198 -0
- package/docs/components/pages/aw-page-single.md +300 -0
- package/docs/components/pages/aw-page.md +194 -0
- package/docs/configuration.md +493 -0
- package/docs/cookbook/advanced-patterns.md +1388 -0
- package/docs/cookbook/common-patterns.md +965 -0
- package/docs/cookbook/index.md +786 -0
- package/docs/getting-started.md +596 -0
- package/docs/guides/best-practices.md +1106 -0
- package/docs/guides/data-fetching.md +852 -0
- package/docs/guides/error-handling.md +1172 -0
- package/docs/guides/forms-guide.md +1329 -0
- package/docs/guides/mobile-subnavigation.md +359 -0
- package/docs/guides/page-patterns/aside-pages.md +1418 -0
- package/docs/guides/page-patterns/dashboard-pages.md +990 -0
- package/docs/guides/page-patterns/detail-pages.md +1556 -0
- package/docs/guides/page-patterns/list-pages.md +1242 -0
- package/docs/index.md +263 -1
- package/docs/integrations.md +870 -0
- package/docs/reference/colors.md +232 -0
- package/docs/reference/icons.md +163 -0
- package/docs/reference/menu.md +462 -0
- package/docs/reference/plugins.md +970 -0
- package/docs/reference/troubleshooting.md +964 -0
- package/nuxt/awes.config.js +9 -8
- package/nuxt/index.js +2 -2
- package/nuxt/pages/more.vue +1 -1
- package/package.json +5 -3
- package/readme.md +171 -1
- package/store/awesIo.js +11 -0
- package/CHANGELOG.md +0 -4520
- package/docs/aw-accordion-fold.md +0 -46
- package/docs/aw-address.md +0 -44
- package/docs/aw-avatar.md +0 -51
- package/docs/aw-badge.md +0 -32
- package/docs/aw-button-nav.md +0 -44
- package/docs/aw-button.md +0 -50
- package/docs/aw-calendar-days.md +0 -46
- package/docs/aw-calendar-nav.md +0 -25
- package/docs/aw-calendar-view.md +0 -12
- package/docs/aw-calendar.md +0 -59
- package/docs/aw-card.md +0 -48
- package/docs/aw-chart.md +0 -51
- package/docs/aw-checkbox.md +0 -56
- package/docs/aw-chip-select.md +0 -46
- package/docs/aw-chip.md +0 -53
- package/docs/aw-code-snippet.md +0 -18
- package/docs/aw-code.md +0 -56
- package/docs/aw-content-wrapper.md +0 -40
- package/docs/aw-context-menu.md +0 -31
- package/docs/aw-cropper.md +0 -60
- package/docs/aw-dashboard-card.md +0 -37
- package/docs/aw-dashboard-donut.md +0 -30
- package/docs/aw-dashboard-line.md +0 -20
- package/docs/aw-dashboard-progress.md +0 -33
- package/docs/aw-dashboard-section.md +0 -32
- package/docs/aw-dashboard-speed.md +0 -30
- package/docs/aw-date.md +0 -52
- package/docs/aw-dropdown-button.md +0 -31
- package/docs/aw-dropdown.md +0 -69
- package/docs/aw-fetch-data.md +0 -45
- package/docs/aw-form.md +0 -52
- package/docs/aw-grid.md +0 -48
- package/docs/aw-icon.md +0 -50
- package/docs/aw-info.md +0 -53
- package/docs/aw-input.md +0 -55
- package/docs/aw-layout-default.md +0 -30
- package/docs/aw-layout-frame-center.md +0 -29
- package/docs/aw-layout-simple.md +0 -49
- package/docs/aw-link.md +0 -54
- package/docs/aw-markdown-editor.md +0 -51
- package/docs/aw-modal.md +0 -63
- package/docs/aw-multi-block-builder.md +0 -66
- package/docs/aw-page.md +0 -36
- package/docs/aw-pagination.md +0 -54
- package/docs/aw-password.md +0 -48
- package/docs/aw-radio.md +0 -54
- package/docs/aw-search.md +0 -49
- package/docs/aw-select.md +0 -93
- package/docs/aw-slider.md +0 -40
- package/docs/aw-svg-image.md +0 -19
- package/docs/aw-switcher.md +0 -51
- package/docs/aw-tab-nav.md +0 -55
- package/docs/aw-table-builder.md +0 -58
- package/docs/aw-table-col.md +0 -33
- package/docs/aw-table-head.md +0 -28
- package/docs/aw-table-row.md +0 -33
- package/docs/aw-table.md +0 -59
- package/docs/aw-tel.md +0 -47
- package/docs/aw-textarea.md +0 -47
- package/docs/aw-toggler.md +0 -41
- package/docs/aw-uploader-files.md +0 -20
- package/docs/aw-uploader.md +0 -60
- package/docs/aw-user-menu.md +0 -34
- package/docs/aw-userpic.md +0 -34
- /package/components/{3_organisms → 2_molecules}/AwTel.vue +0 -0
|
@@ -0,0 +1,1106 @@
|
|
|
1
|
+
# Best Practices Guide
|
|
2
|
+
|
|
3
|
+
Comprehensive best practices for building AwesCode UI applications with UI components, vue-mc models, and Laravel backend integration.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
1. [Component Communication](#component-communication)
|
|
8
|
+
2. [Loading States & Progress](#loading-states--progress)
|
|
9
|
+
3. [Data Management](#data-management)
|
|
10
|
+
4. [Error Handling](#error-handling)
|
|
11
|
+
5. [User Feedback](#user-feedback)
|
|
12
|
+
6. [Date Formatting](#date-formatting)
|
|
13
|
+
7. [Component Imports](#component-imports)
|
|
14
|
+
8. [Accessibility](#accessibility)
|
|
15
|
+
9. [Performance](#performance)
|
|
16
|
+
10. [Styling](#styling)
|
|
17
|
+
11. [Table Patterns](#table-patterns)
|
|
18
|
+
12. [Navigation](#navigation)
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Component Communication
|
|
23
|
+
|
|
24
|
+
### Event-Driven Architecture
|
|
25
|
+
|
|
26
|
+
Use events for parent-child communication with specific, descriptive event names.
|
|
27
|
+
|
|
28
|
+
#### ✅ Good: Specific Events with Clear Data
|
|
29
|
+
|
|
30
|
+
```markup
|
|
31
|
+
<!-- Child Component -->
|
|
32
|
+
<script>
|
|
33
|
+
export default {
|
|
34
|
+
methods: {
|
|
35
|
+
selectDescription(description) {
|
|
36
|
+
this.$emit('description-selected', description)
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
updateTranslations(translations) {
|
|
40
|
+
this.$emit('translations-generated', translations)
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
updateStatus(generating, text) {
|
|
44
|
+
this.$emit('ai-status-changed', {
|
|
45
|
+
generating,
|
|
46
|
+
statusText: text
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<!-- Parent Component -->
|
|
54
|
+
<template>
|
|
55
|
+
<AIAssistant
|
|
56
|
+
@description-selected="handleDescription"
|
|
57
|
+
@translations-generated="handleTranslations"
|
|
58
|
+
@ai-status-changed="handleStatus"
|
|
59
|
+
/>
|
|
60
|
+
</template>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
#### ❌ Bad: Generic Events with Unclear Data
|
|
64
|
+
|
|
65
|
+
```javascript
|
|
66
|
+
// ❌ WRONG - Generic event name
|
|
67
|
+
this.$emit('change', someData)
|
|
68
|
+
|
|
69
|
+
// ❌ WRONG - Type discrimination in payload
|
|
70
|
+
this.$emit('update', { type: 'description', value: description })
|
|
71
|
+
|
|
72
|
+
// ❌ WRONG - No data context
|
|
73
|
+
this.$emit('done')
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Props vs Events
|
|
77
|
+
|
|
78
|
+
**Props down, events up:**
|
|
79
|
+
- Use props to pass data to child components
|
|
80
|
+
- Use events to notify parent of changes
|
|
81
|
+
- Never mutate props directly in child
|
|
82
|
+
|
|
83
|
+
```markup
|
|
84
|
+
<!-- ✅ GOOD -->
|
|
85
|
+
<script>
|
|
86
|
+
export default {
|
|
87
|
+
props: {
|
|
88
|
+
value: String
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
methods: {
|
|
92
|
+
updateValue(newValue) {
|
|
93
|
+
this.$emit('input', newValue) // Emit event
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
</script>
|
|
98
|
+
|
|
99
|
+
<!-- ❌ BAD -->
|
|
100
|
+
<script>
|
|
101
|
+
export default {
|
|
102
|
+
props: {
|
|
103
|
+
value: String
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
methods: {
|
|
107
|
+
updateValue(newValue) {
|
|
108
|
+
this.value = newValue // ❌ Never mutate props
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
</script>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Loading States & Progress
|
|
118
|
+
|
|
119
|
+
### Button Loading States
|
|
120
|
+
|
|
121
|
+
Always use the `:loading` prop for AwButton.
|
|
122
|
+
|
|
123
|
+
#### ✅ Good: Simple Loading State
|
|
124
|
+
|
|
125
|
+
```markup
|
|
126
|
+
<AwButton
|
|
127
|
+
:loading="isLoading"
|
|
128
|
+
@click="handleAction"
|
|
129
|
+
text="Save"
|
|
130
|
+
/>
|
|
131
|
+
|
|
132
|
+
<script>
|
|
133
|
+
export default {
|
|
134
|
+
data() {
|
|
135
|
+
return {
|
|
136
|
+
isLoading: false
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
methods: {
|
|
141
|
+
async handleAction() {
|
|
142
|
+
this.isLoading = true
|
|
143
|
+
try {
|
|
144
|
+
await this.performAction()
|
|
145
|
+
} finally {
|
|
146
|
+
this.isLoading = false
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
</script>
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
#### ❌ Bad: Conditional Text Inside Button
|
|
155
|
+
|
|
156
|
+
```markup
|
|
157
|
+
<!-- ❌ WRONG - Button handles loading text automatically -->
|
|
158
|
+
<AwButton :loading="isLoading">
|
|
159
|
+
<span v-if="isLoading">Loading...</span>
|
|
160
|
+
<span v-else>Save</span>
|
|
161
|
+
</AwButton>
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Custom Button Styles with Loading
|
|
165
|
+
|
|
166
|
+
When styling buttons, exclude loading state from custom CSS:
|
|
167
|
+
|
|
168
|
+
```scss
|
|
169
|
+
/* ✅ GOOD - Excludes loading state */
|
|
170
|
+
.custom-btn:not([disabled]):not(.loading) {
|
|
171
|
+
background-color: var(--c-accent) !important;
|
|
172
|
+
border-color: var(--c-accent) !important;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.custom-btn:hover:not([disabled]):not(.loading) {
|
|
176
|
+
filter: brightness(1.1) !important;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/* ❌ BAD - Overrides loading styles */
|
|
180
|
+
.custom-btn {
|
|
181
|
+
background: red !important;
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Model Loading States
|
|
186
|
+
|
|
187
|
+
Vue-mc models provide automatic loading states:
|
|
188
|
+
|
|
189
|
+
```markup
|
|
190
|
+
<template>
|
|
191
|
+
<AwPageSingle
|
|
192
|
+
:title="pageTitle"
|
|
193
|
+
:action="saveButton"
|
|
194
|
+
@action="save"
|
|
195
|
+
>
|
|
196
|
+
<!-- Form content -->
|
|
197
|
+
</AwPageSingle>
|
|
198
|
+
</template>
|
|
199
|
+
|
|
200
|
+
<script>
|
|
201
|
+
export default {
|
|
202
|
+
computed: {
|
|
203
|
+
saveButton() {
|
|
204
|
+
return {
|
|
205
|
+
text: 'Save',
|
|
206
|
+
loading: this.model.saving // ✅ Automatic from vue-mc
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
</script>
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Page Progress Indicator
|
|
215
|
+
|
|
216
|
+
Use the built-in `progress` prop for long operations:
|
|
217
|
+
|
|
218
|
+
```markup
|
|
219
|
+
<template>
|
|
220
|
+
<AwPageSingle
|
|
221
|
+
:title="pageTitle"
|
|
222
|
+
:action="saveButton"
|
|
223
|
+
:progress="isProcessing ? uploadProgress : null"
|
|
224
|
+
@action="save"
|
|
225
|
+
>
|
|
226
|
+
<!-- Page content -->
|
|
227
|
+
</AwPageSingle>
|
|
228
|
+
</template>
|
|
229
|
+
|
|
230
|
+
<script>
|
|
231
|
+
export default {
|
|
232
|
+
data() {
|
|
233
|
+
return {
|
|
234
|
+
uploadProgress: 0,
|
|
235
|
+
isProcessing: false
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
computed: {
|
|
240
|
+
saveButton() {
|
|
241
|
+
return {
|
|
242
|
+
key: 'save',
|
|
243
|
+
label: 'Save',
|
|
244
|
+
loading: this.model.saving,
|
|
245
|
+
color: 'accent'
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
|
|
250
|
+
methods: {
|
|
251
|
+
async handleAction(action) {
|
|
252
|
+
if (action.key === 'save') {
|
|
253
|
+
await this.save()
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
</script>
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
**Note:** For custom status indicators (AI generating, etc.), you can still use the `#buttons` slot alongside the action prop for non-action UI elements.
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## Data Management
|
|
266
|
+
|
|
267
|
+
### Collection Initialization
|
|
268
|
+
|
|
269
|
+
Collections always take 2 arguments: `(models, options)`
|
|
270
|
+
|
|
271
|
+
#### ✅ Good: Proper Collection Initialization
|
|
272
|
+
|
|
273
|
+
```javascript
|
|
274
|
+
import Customers from '~/collections/Customers'
|
|
275
|
+
|
|
276
|
+
export default {
|
|
277
|
+
data() {
|
|
278
|
+
return {
|
|
279
|
+
customers: new Customers([], {
|
|
280
|
+
shop_uuid: this.$route.params.shop_uuid
|
|
281
|
+
})
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
#### ❌ Bad: Missing Options
|
|
288
|
+
|
|
289
|
+
```javascript
|
|
290
|
+
// ❌ WRONG - Missing shop_uuid
|
|
291
|
+
customers: new Customers([])
|
|
292
|
+
|
|
293
|
+
// ❌ WRONG - Only one argument
|
|
294
|
+
customers: new Customers()
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### Model Initialization
|
|
298
|
+
|
|
299
|
+
Models take 3 arguments: `(attributes, collection, options)`
|
|
300
|
+
|
|
301
|
+
#### ✅ Good: Proper Model Initialization
|
|
302
|
+
|
|
303
|
+
```javascript
|
|
304
|
+
import Customer from '~/models/Customer'
|
|
305
|
+
|
|
306
|
+
export default {
|
|
307
|
+
data() {
|
|
308
|
+
return {
|
|
309
|
+
// New model
|
|
310
|
+
customer: new Customer(
|
|
311
|
+
{}, // Empty attributes
|
|
312
|
+
null, // No parent collection
|
|
313
|
+
{ shop_uuid: this.$route.params.shop_uuid }
|
|
314
|
+
),
|
|
315
|
+
|
|
316
|
+
// Existing model
|
|
317
|
+
customer: new Customer(
|
|
318
|
+
{ id: this.$route.params.id }, // ID from route
|
|
319
|
+
null,
|
|
320
|
+
{ shop_uuid: this.$route.params.shop_uuid }
|
|
321
|
+
)
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
#### ❌ Bad: Wrong Number of Arguments
|
|
328
|
+
|
|
329
|
+
```javascript
|
|
330
|
+
// ❌ WRONG - Only one argument
|
|
331
|
+
customer: new Customer({ id: 1 })
|
|
332
|
+
|
|
333
|
+
// ❌ WRONG - Missing options
|
|
334
|
+
customer: new Customer({ id: 1 }, null)
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### Always Include shop_uuid
|
|
338
|
+
|
|
339
|
+
```javascript
|
|
340
|
+
// ✅ GOOD - Shop UUID in options
|
|
341
|
+
customers: new Customers([], {
|
|
342
|
+
shop_uuid: this.$route.params.shop_uuid
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
// ❌ BAD - No shop UUID
|
|
346
|
+
customers: new Customers([])
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### Use Standard vue-mc Methods
|
|
350
|
+
|
|
351
|
+
Don't create custom methods that duplicate vue-mc functionality:
|
|
352
|
+
|
|
353
|
+
```javascript
|
|
354
|
+
// ✅ GOOD - Use standard vue-mc methods
|
|
355
|
+
if (this.customers.isEmpty()) {
|
|
356
|
+
// Collection is empty
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (Object.keys(this.model.errors).length > 0) {
|
|
360
|
+
// Model has validation errors
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ❌ BAD - Custom methods
|
|
364
|
+
if (this.customers.hasItems()) { // ❌ Use isEmpty() instead
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (this.model.hasErrors()) { // ❌ Use Object.keys(errors).length > 0 instead
|
|
368
|
+
}
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### AwTableBuilder Automatically Fetches
|
|
372
|
+
|
|
373
|
+
Don't manually fetch when using AwTableBuilder:
|
|
374
|
+
|
|
375
|
+
```markup
|
|
376
|
+
<!-- ✅ GOOD - Let AwTableBuilder handle fetching -->
|
|
377
|
+
<template>
|
|
378
|
+
<AwTableBuilder :collection="customers">
|
|
379
|
+
<AwTableCol field="name" title="Name" />
|
|
380
|
+
</AwTableBuilder>
|
|
381
|
+
</template>
|
|
382
|
+
|
|
383
|
+
<script>
|
|
384
|
+
export default {
|
|
385
|
+
data() {
|
|
386
|
+
return {
|
|
387
|
+
customers: new Customers([], {
|
|
388
|
+
shop_uuid: this.$route.params.shop_uuid
|
|
389
|
+
})
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
// No fetch() or mounted() needed!
|
|
393
|
+
}
|
|
394
|
+
</script>
|
|
395
|
+
|
|
396
|
+
<!-- ❌ BAD - Manual fetch not needed -->
|
|
397
|
+
<script>
|
|
398
|
+
export default {
|
|
399
|
+
async mounted() {
|
|
400
|
+
await this.customers.fetch() // ❌ Unnecessary!
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
</script>
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
---
|
|
407
|
+
|
|
408
|
+
## Error Handling
|
|
409
|
+
|
|
410
|
+
### Field-Level Validation
|
|
411
|
+
|
|
412
|
+
Always bind `:error` prop to model errors:
|
|
413
|
+
|
|
414
|
+
```markup
|
|
415
|
+
<AwInput
|
|
416
|
+
v-model="model.name"
|
|
417
|
+
label="Name"
|
|
418
|
+
:error="model.errors.name"
|
|
419
|
+
required
|
|
420
|
+
/>
|
|
421
|
+
|
|
422
|
+
<AwInput
|
|
423
|
+
v-model="model.email"
|
|
424
|
+
label="Email"
|
|
425
|
+
:error="model.errors.email"
|
|
426
|
+
required
|
|
427
|
+
/>
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
### Check Errors After Save
|
|
431
|
+
|
|
432
|
+
Always check for validation errors after saving:
|
|
433
|
+
|
|
434
|
+
```javascript
|
|
435
|
+
async save() {
|
|
436
|
+
try {
|
|
437
|
+
await this.model.save()
|
|
438
|
+
|
|
439
|
+
// ✅ GOOD - Check for errors
|
|
440
|
+
if (Object.keys(this.model.errors).length > 0) {
|
|
441
|
+
this.$notify({
|
|
442
|
+
message: 'Please fix validation errors',
|
|
443
|
+
type: 'error'
|
|
444
|
+
})
|
|
445
|
+
return
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Success path
|
|
449
|
+
this.$notify({
|
|
450
|
+
message: 'Saved successfully',
|
|
451
|
+
type: 'success'
|
|
452
|
+
})
|
|
453
|
+
this.$router.push('/list')
|
|
454
|
+
} catch (error) {
|
|
455
|
+
this.$notify({
|
|
456
|
+
message: 'Failed to save',
|
|
457
|
+
type: 'error'
|
|
458
|
+
})
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
### Handle Fetch Errors
|
|
464
|
+
|
|
465
|
+
Redirect or show error when fetch fails:
|
|
466
|
+
|
|
467
|
+
```javascript
|
|
468
|
+
async mounted() {
|
|
469
|
+
if (!this.model.isNew()) {
|
|
470
|
+
try {
|
|
471
|
+
await this.model.fetch()
|
|
472
|
+
} catch (error) {
|
|
473
|
+
// 404 - Record not found
|
|
474
|
+
if (error.response?.status === 404) {
|
|
475
|
+
this.$notify({
|
|
476
|
+
message: 'Record not found',
|
|
477
|
+
type: 'error'
|
|
478
|
+
})
|
|
479
|
+
this.$router.push('/list')
|
|
480
|
+
return
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Other errors
|
|
484
|
+
this.$notify({
|
|
485
|
+
message: 'Failed to load data',
|
|
486
|
+
type: 'error'
|
|
487
|
+
})
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
### Try-Catch for API Calls
|
|
494
|
+
|
|
495
|
+
Always wrap API calls in try-catch:
|
|
496
|
+
|
|
497
|
+
```javascript
|
|
498
|
+
async performAction() {
|
|
499
|
+
try {
|
|
500
|
+
const { data } = await this.$axios.post('/api/action', this.payload)
|
|
501
|
+
this.handleSuccess(data)
|
|
502
|
+
} catch (error) {
|
|
503
|
+
console.error('Action failed:', error)
|
|
504
|
+
|
|
505
|
+
// User-friendly error message
|
|
506
|
+
this.$notify({
|
|
507
|
+
message: error.response?.data?.message || 'Action failed. Please try again.',
|
|
508
|
+
type: 'error'
|
|
509
|
+
})
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
---
|
|
515
|
+
|
|
516
|
+
## User Feedback
|
|
517
|
+
|
|
518
|
+
### Notifications
|
|
519
|
+
|
|
520
|
+
Use `$notify` for user feedback:
|
|
521
|
+
|
|
522
|
+
```javascript
|
|
523
|
+
// Success
|
|
524
|
+
this.$notify({
|
|
525
|
+
message: 'Customer created successfully',
|
|
526
|
+
type: 'success'
|
|
527
|
+
})
|
|
528
|
+
|
|
529
|
+
// Error
|
|
530
|
+
this.$notify({
|
|
531
|
+
message: 'Failed to delete item',
|
|
532
|
+
type: 'error'
|
|
533
|
+
})
|
|
534
|
+
|
|
535
|
+
// Warning
|
|
536
|
+
this.$notify({
|
|
537
|
+
message: 'Changes not saved',
|
|
538
|
+
type: 'warning'
|
|
539
|
+
})
|
|
540
|
+
|
|
541
|
+
// Info
|
|
542
|
+
this.$notify({
|
|
543
|
+
message: 'Email sent',
|
|
544
|
+
type: 'info'
|
|
545
|
+
})
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
### Confirmations
|
|
549
|
+
|
|
550
|
+
Use `$confirm` for destructive actions:
|
|
551
|
+
|
|
552
|
+
```javascript
|
|
553
|
+
async deleteItem(item) {
|
|
554
|
+
const confirmed = await this.$confirm({
|
|
555
|
+
title: 'Delete Customer',
|
|
556
|
+
message: `Are you sure you want to delete "${item.name}"?`
|
|
557
|
+
})
|
|
558
|
+
|
|
559
|
+
if (!confirmed) return
|
|
560
|
+
|
|
561
|
+
try {
|
|
562
|
+
await this.$axios.delete(`/api/customers/${item.id}`)
|
|
563
|
+
this.$notify({
|
|
564
|
+
message: 'Customer deleted successfully',
|
|
565
|
+
type: 'success'
|
|
566
|
+
})
|
|
567
|
+
this.customers.fetch()
|
|
568
|
+
} catch (error) {
|
|
569
|
+
this.$notify({
|
|
570
|
+
message: 'Failed to delete customer',
|
|
571
|
+
type: 'error'
|
|
572
|
+
})
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
### Success Feedback After Actions
|
|
578
|
+
|
|
579
|
+
Always provide feedback after user actions:
|
|
580
|
+
|
|
581
|
+
```javascript
|
|
582
|
+
// ✅ GOOD - Clear feedback
|
|
583
|
+
async save() {
|
|
584
|
+
await this.model.save()
|
|
585
|
+
|
|
586
|
+
if (Object.keys(this.model.errors).length > 0) {
|
|
587
|
+
this.$notify({
|
|
588
|
+
message: 'Please fix validation errors',
|
|
589
|
+
type: 'error'
|
|
590
|
+
})
|
|
591
|
+
return
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
this.$notify({
|
|
595
|
+
message: `Customer ${this.model.isNew() ? 'created' : 'updated'} successfully`,
|
|
596
|
+
type: 'success'
|
|
597
|
+
})
|
|
598
|
+
|
|
599
|
+
this.$router.push('/customers')
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// ❌ BAD - No feedback
|
|
603
|
+
async save() {
|
|
604
|
+
await this.model.save()
|
|
605
|
+
this.$router.push('/customers') // User doesn't know if it succeeded
|
|
606
|
+
}
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
---
|
|
610
|
+
|
|
611
|
+
## Date Formatting
|
|
612
|
+
|
|
613
|
+
### Always Use $dayjs
|
|
614
|
+
|
|
615
|
+
Use `$dayjs` in templates (without `this`):
|
|
616
|
+
|
|
617
|
+
```markup
|
|
618
|
+
<!-- ✅ GOOD - Using $dayjs in templates -->
|
|
619
|
+
<template>
|
|
620
|
+
<div>
|
|
621
|
+
<!-- Short date -->
|
|
622
|
+
{{ $dayjs(cell.created_at).format('ll') }}
|
|
623
|
+
|
|
624
|
+
<!-- Date with time -->
|
|
625
|
+
{{ $dayjs(cell.updated_at).format('lll') }}
|
|
626
|
+
|
|
627
|
+
<!-- Month and year -->
|
|
628
|
+
{{ $dayjs(date).format('MMMM YYYY') }}
|
|
629
|
+
|
|
630
|
+
<!-- Relative time -->
|
|
631
|
+
{{ $dayjs(cell.created_at).fromNow() }}
|
|
632
|
+
</div>
|
|
633
|
+
</template>
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
Use `this.$dayjs` in methods (with `this`):
|
|
637
|
+
|
|
638
|
+
```javascript
|
|
639
|
+
export default {
|
|
640
|
+
methods: {
|
|
641
|
+
// ✅ GOOD - Using this.$dayjs in methods
|
|
642
|
+
formatMonth(monthString) {
|
|
643
|
+
return this.$dayjs(monthString).format('MMMM YYYY')
|
|
644
|
+
},
|
|
645
|
+
|
|
646
|
+
isRecent(date) {
|
|
647
|
+
return this.$dayjs(date).isAfter(
|
|
648
|
+
this.$dayjs().subtract(7, 'days')
|
|
649
|
+
)
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
### ❌ Bad: Native Date Methods
|
|
656
|
+
|
|
657
|
+
```markup
|
|
658
|
+
<!-- ❌ WRONG - Using native Date -->
|
|
659
|
+
{{ new Date(cell.created_at).toLocaleDateString() }}
|
|
660
|
+
|
|
661
|
+
<!-- ❌ WRONG - Using toLocaleDateString -->
|
|
662
|
+
{{ date.toLocaleDateString('en-US', { year: 'numeric', month: 'short' }) }}
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
### Common Date Formats
|
|
666
|
+
|
|
667
|
+
| Format | Code | Example |
|
|
668
|
+
|--------|------|---------|
|
|
669
|
+
| Short date | `$dayjs(date).format('ll')` | Jan 15, 2024 |
|
|
670
|
+
| Date with time | `$dayjs(date).format('lll')` | Jan 15, 2024 10:30 AM |
|
|
671
|
+
| Long date | `$dayjs(date).format('LL')` | January 15, 2024 |
|
|
672
|
+
| Month and year | `$dayjs(date).format('MMMM YYYY')` | January 2024 |
|
|
673
|
+
| Short month/year | `$dayjs(date).format('MMM YYYY')` | Jan 2024 |
|
|
674
|
+
| Relative time | `$dayjs(date).fromNow()` | 2 hours ago |
|
|
675
|
+
|
|
676
|
+
---
|
|
677
|
+
|
|
678
|
+
## Component Imports
|
|
679
|
+
|
|
680
|
+
### Auto-Imported Components
|
|
681
|
+
|
|
682
|
+
Atoms and molecules are globally registered:
|
|
683
|
+
|
|
684
|
+
```markup
|
|
685
|
+
<!-- ✅ GOOD - No import needed for global components -->
|
|
686
|
+
<template>
|
|
687
|
+
<div>
|
|
688
|
+
<AwButton text="Click me" />
|
|
689
|
+
<AwInput v-model="value" />
|
|
690
|
+
<AwCard title="Card Title">
|
|
691
|
+
Content
|
|
692
|
+
</AwCard>
|
|
693
|
+
</div>
|
|
694
|
+
</template>
|
|
695
|
+
|
|
696
|
+
<!-- No imports needed! -->
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
### Manual Imports
|
|
700
|
+
|
|
701
|
+
Collections, models, and utilities must be imported:
|
|
702
|
+
|
|
703
|
+
```markup
|
|
704
|
+
<script>
|
|
705
|
+
// ✅ GOOD - Import collections and models
|
|
706
|
+
import Customers from '~/collections/Customers'
|
|
707
|
+
import Customer from '~/models/Customer'
|
|
708
|
+
|
|
709
|
+
export default {
|
|
710
|
+
data() {
|
|
711
|
+
return {
|
|
712
|
+
customers: new Customers([], {
|
|
713
|
+
shop_uuid: this.$route.params.shop_uuid
|
|
714
|
+
}),
|
|
715
|
+
customer: new Customer()
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
</script>
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
### Components from /components/
|
|
723
|
+
|
|
724
|
+
Components in your project's `/components/` folder are auto-imported by Nuxt:
|
|
725
|
+
|
|
726
|
+
```markup
|
|
727
|
+
<!-- ✅ GOOD - Auto-imported from /components/ -->
|
|
728
|
+
<template>
|
|
729
|
+
<div>
|
|
730
|
+
<CustomerCard :customer="customer" />
|
|
731
|
+
<OrderSummary :order="order" />
|
|
732
|
+
</div>
|
|
733
|
+
</template>
|
|
734
|
+
|
|
735
|
+
<!-- No imports needed for project components -->
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
---
|
|
739
|
+
|
|
740
|
+
## Accessibility
|
|
741
|
+
|
|
742
|
+
### Semantic HTML
|
|
743
|
+
|
|
744
|
+
Use semantic HTML elements:
|
|
745
|
+
|
|
746
|
+
```markup
|
|
747
|
+
<!-- ✅ GOOD - Semantic HTML -->
|
|
748
|
+
<nav>
|
|
749
|
+
<ul>
|
|
750
|
+
<li><a href="/home">Home</a></li>
|
|
751
|
+
<li><a href="/about">About</a></li>
|
|
752
|
+
</ul>
|
|
753
|
+
</nav>
|
|
754
|
+
|
|
755
|
+
<!-- ❌ BAD - Non-semantic -->
|
|
756
|
+
<div class="nav">
|
|
757
|
+
<div class="nav-item">Home</div>
|
|
758
|
+
<div class="nav-item">About</div>
|
|
759
|
+
</div>
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
### ARIA Labels
|
|
763
|
+
|
|
764
|
+
Provide ARIA labels for interactive elements:
|
|
765
|
+
|
|
766
|
+
```markup
|
|
767
|
+
<!-- ✅ GOOD - ARIA labels -->
|
|
768
|
+
<AwButton
|
|
769
|
+
@click="deleteItem"
|
|
770
|
+
icon="awesio/delete"
|
|
771
|
+
aria-label="Delete customer"
|
|
772
|
+
/>
|
|
773
|
+
|
|
774
|
+
<AwInput
|
|
775
|
+
v-model="search"
|
|
776
|
+
placeholder="Search..."
|
|
777
|
+
aria-label="Search customers"
|
|
778
|
+
/>
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
### Keyboard Navigation
|
|
782
|
+
|
|
783
|
+
Ensure keyboard navigation works:
|
|
784
|
+
|
|
785
|
+
```markup
|
|
786
|
+
<AwButton
|
|
787
|
+
@click="handleAction"
|
|
788
|
+
@keydown.enter="handleAction"
|
|
789
|
+
@keydown.space.prevent="handleAction"
|
|
790
|
+
>
|
|
791
|
+
Action
|
|
792
|
+
</AwButton>
|
|
793
|
+
```
|
|
794
|
+
|
|
795
|
+
---
|
|
796
|
+
|
|
797
|
+
## Performance
|
|
798
|
+
|
|
799
|
+
### Computed Properties Over Watchers
|
|
800
|
+
|
|
801
|
+
Use computed properties for derived state:
|
|
802
|
+
|
|
803
|
+
```javascript
|
|
804
|
+
// ✅ GOOD - Computed property
|
|
805
|
+
export default {
|
|
806
|
+
computed: {
|
|
807
|
+
fullName() {
|
|
808
|
+
return `${this.firstName} ${this.lastName}`
|
|
809
|
+
},
|
|
810
|
+
|
|
811
|
+
hasErrors() {
|
|
812
|
+
return Object.keys(this.model.errors).length > 0
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// ❌ BAD - Watcher for derived state
|
|
818
|
+
export default {
|
|
819
|
+
data() {
|
|
820
|
+
return {
|
|
821
|
+
fullName: ''
|
|
822
|
+
}
|
|
823
|
+
},
|
|
824
|
+
|
|
825
|
+
watch: {
|
|
826
|
+
firstName() {
|
|
827
|
+
this.fullName = `${this.firstName} ${this.lastName}`
|
|
828
|
+
},
|
|
829
|
+
lastName() {
|
|
830
|
+
this.fullName = `${this.firstName} ${this.lastName}`
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
```
|
|
835
|
+
|
|
836
|
+
### Proper Key Attributes
|
|
837
|
+
|
|
838
|
+
Always use unique keys in v-for:
|
|
839
|
+
|
|
840
|
+
```markup
|
|
841
|
+
<!-- ✅ GOOD - Unique key -->
|
|
842
|
+
<div v-for="customer in customers" :key="customer.id">
|
|
843
|
+
{{ customer.name }}
|
|
844
|
+
</div>
|
|
845
|
+
|
|
846
|
+
<!-- ❌ BAD - Index as key -->
|
|
847
|
+
<div v-for="(customer, index) in customers" :key="index">
|
|
848
|
+
{{ customer.name }}
|
|
849
|
+
</div>
|
|
850
|
+
|
|
851
|
+
<!-- ❌ BAD - No key -->
|
|
852
|
+
<div v-for="customer in customers">
|
|
853
|
+
{{ customer.name }}
|
|
854
|
+
</div>
|
|
855
|
+
```
|
|
856
|
+
|
|
857
|
+
### Efficient Re-rendering
|
|
858
|
+
|
|
859
|
+
Avoid unnecessary re-renders:
|
|
860
|
+
|
|
861
|
+
```javascript
|
|
862
|
+
// ✅ GOOD - Computed property caches result
|
|
863
|
+
computed: {
|
|
864
|
+
expensiveComputation() {
|
|
865
|
+
return this.items.filter(item => {
|
|
866
|
+
// Complex filtering logic
|
|
867
|
+
})
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// ❌ BAD - Method recalculates every render
|
|
872
|
+
methods: {
|
|
873
|
+
expensiveComputation() {
|
|
874
|
+
return this.items.filter(item => {
|
|
875
|
+
// Complex filtering logic
|
|
876
|
+
})
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
```
|
|
880
|
+
|
|
881
|
+
---
|
|
882
|
+
|
|
883
|
+
## Styling
|
|
884
|
+
|
|
885
|
+
### CSS Custom Properties
|
|
886
|
+
|
|
887
|
+
Use CSS custom properties for theming:
|
|
888
|
+
|
|
889
|
+
```scss
|
|
890
|
+
/* ✅ GOOD - CSS custom properties */
|
|
891
|
+
.button-accent {
|
|
892
|
+
background-color: var(--c-accent);
|
|
893
|
+
border-color: var(--c-accent);
|
|
894
|
+
color: var(--c-on-accent);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
.button-accent:hover {
|
|
898
|
+
filter: brightness(1.1);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/* ❌ BAD - Hardcoded colors */
|
|
902
|
+
.button-accent {
|
|
903
|
+
background-color: #6366f1;
|
|
904
|
+
border-color: #6366f1;
|
|
905
|
+
color: white;
|
|
906
|
+
}
|
|
907
|
+
```
|
|
908
|
+
|
|
909
|
+
### Responsive Design
|
|
910
|
+
|
|
911
|
+
Use AwGrid component for responsive layouts:
|
|
912
|
+
|
|
913
|
+
```markup
|
|
914
|
+
<!-- ✅ GOOD - Mobile-first responsive with AwGrid -->
|
|
915
|
+
<AwGrid :col="{ md: 2, lg: 4 }">
|
|
916
|
+
<AwCard>Card 1</AwCard>
|
|
917
|
+
<AwCard>Card 2</AwCard>
|
|
918
|
+
<AwCard>Card 3</AwCard>
|
|
919
|
+
<AwCard>Card 4</AwCard>
|
|
920
|
+
</AwGrid>
|
|
921
|
+
```
|
|
922
|
+
|
|
923
|
+
### Hover Effects
|
|
924
|
+
|
|
925
|
+
Add appropriate hover states:
|
|
926
|
+
|
|
927
|
+
```scss
|
|
928
|
+
/* ✅ GOOD - Hover effects */
|
|
929
|
+
.card:hover {
|
|
930
|
+
transform: translateY(-2px);
|
|
931
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
932
|
+
transition: all 0.2s ease;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
.link:hover {
|
|
936
|
+
color: var(--c-accent);
|
|
937
|
+
text-decoration: underline;
|
|
938
|
+
}
|
|
939
|
+
```
|
|
940
|
+
|
|
941
|
+
---
|
|
942
|
+
|
|
943
|
+
## Table Patterns
|
|
944
|
+
|
|
945
|
+
### Understanding field Prop
|
|
946
|
+
|
|
947
|
+
**With field:** `cell` = field value only
|
|
948
|
+
**Without field:** `cell` = entire row object
|
|
949
|
+
|
|
950
|
+
```markup
|
|
951
|
+
<!-- WITH field: cell is just the value -->
|
|
952
|
+
<AwTableCol title="Name" field="name">
|
|
953
|
+
<template #default="{ cell }">
|
|
954
|
+
{{ cell }} <!-- cell is the string value of 'name' -->
|
|
955
|
+
</template>
|
|
956
|
+
</AwTableCol>
|
|
957
|
+
|
|
958
|
+
<!-- WITHOUT field: cell is full object -->
|
|
959
|
+
<AwTableCol title="Name">
|
|
960
|
+
<template #default="{ cell }">
|
|
961
|
+
{{ cell.name }} <!-- cell is the full row object -->
|
|
962
|
+
{{ cell.email }} <!-- can access other fields -->
|
|
963
|
+
</template>
|
|
964
|
+
</AwTableCol>
|
|
965
|
+
```
|
|
966
|
+
|
|
967
|
+
### Can't Use @click:row and #dropdown Together
|
|
968
|
+
|
|
969
|
+
```markup
|
|
970
|
+
<!-- ❌ BAD - Conflict! -->
|
|
971
|
+
<AwTableBuilder
|
|
972
|
+
:collection="items"
|
|
973
|
+
@click:row="viewItem"
|
|
974
|
+
>
|
|
975
|
+
<template #dropdown="{ cell }">
|
|
976
|
+
<AwDropdownButton text="Edit" />
|
|
977
|
+
</template>
|
|
978
|
+
</AwTableBuilder>
|
|
979
|
+
|
|
980
|
+
<!-- ✅ GOOD - Choose one approach -->
|
|
981
|
+
<AwTableBuilder :collection="items">
|
|
982
|
+
<template #dropdown="{ cell }">
|
|
983
|
+
<AwDropdownButton text="View" @click="viewItem(cell)" />
|
|
984
|
+
<AwDropdownButton text="Edit" @click="editItem(cell)" />
|
|
985
|
+
</template>
|
|
986
|
+
</AwTableBuilder>
|
|
987
|
+
```
|
|
988
|
+
|
|
989
|
+
### AwDropdownButton Usage
|
|
990
|
+
|
|
991
|
+
```markup
|
|
992
|
+
<template #dropdown="{ cell }">
|
|
993
|
+
<AwDropdownButton
|
|
994
|
+
text="View"
|
|
995
|
+
@click="viewItem(cell)"
|
|
996
|
+
/>
|
|
997
|
+
<AwDropdownButton
|
|
998
|
+
text="Edit"
|
|
999
|
+
@click="editItem(cell)"
|
|
1000
|
+
/>
|
|
1001
|
+
<AwDropdownButton
|
|
1002
|
+
text="Delete"
|
|
1003
|
+
color="error"
|
|
1004
|
+
@click="deleteItem(cell)"
|
|
1005
|
+
/>
|
|
1006
|
+
</template>
|
|
1007
|
+
```
|
|
1008
|
+
|
|
1009
|
+
---
|
|
1010
|
+
|
|
1011
|
+
## Navigation
|
|
1012
|
+
|
|
1013
|
+
### Always Include shop_uuid in Routes
|
|
1014
|
+
|
|
1015
|
+
```javascript
|
|
1016
|
+
// ✅ GOOD - Include shop_uuid
|
|
1017
|
+
this.$router.push({
|
|
1018
|
+
name: 'customers-edit',
|
|
1019
|
+
params: {
|
|
1020
|
+
shop_uuid: this.$route.params.shop_uuid,
|
|
1021
|
+
id: customer.id
|
|
1022
|
+
}
|
|
1023
|
+
})
|
|
1024
|
+
|
|
1025
|
+
// ❌ BAD - Missing shop_uuid
|
|
1026
|
+
this.$router.push(`/customers/${customer.id}/edit`)
|
|
1027
|
+
```
|
|
1028
|
+
|
|
1029
|
+
### AwButton href vs @click
|
|
1030
|
+
|
|
1031
|
+
Use `href` for navigation, `@click` for actions:
|
|
1032
|
+
|
|
1033
|
+
```markup
|
|
1034
|
+
<!-- ✅ GOOD - href for navigation -->
|
|
1035
|
+
<AwButton
|
|
1036
|
+
:href="`/${shopUuid}/customers/${customer.id}`"
|
|
1037
|
+
text="View Customer"
|
|
1038
|
+
/>
|
|
1039
|
+
|
|
1040
|
+
<!-- ✅ GOOD - @click for actions -->
|
|
1041
|
+
<AwButton
|
|
1042
|
+
@click="deleteCustomer(customer)"
|
|
1043
|
+
text="Delete"
|
|
1044
|
+
color="error"
|
|
1045
|
+
/>
|
|
1046
|
+
```
|
|
1047
|
+
|
|
1048
|
+
### Mobile Breadcrumbs
|
|
1049
|
+
|
|
1050
|
+
Add breadcrumbs for mobile navigation:
|
|
1051
|
+
|
|
1052
|
+
```markup
|
|
1053
|
+
<template>
|
|
1054
|
+
<AwPage title="Email Templates">
|
|
1055
|
+
<template #mobile-breadcrumbs>
|
|
1056
|
+
<AwButton
|
|
1057
|
+
:href="`/${$route.params.shop_uuid}/notifications`"
|
|
1058
|
+
theme="text"
|
|
1059
|
+
icon="arrow-left"
|
|
1060
|
+
text="Back to Notifications"
|
|
1061
|
+
/>
|
|
1062
|
+
</template>
|
|
1063
|
+
|
|
1064
|
+
<!-- Page content -->
|
|
1065
|
+
</AwPage>
|
|
1066
|
+
</template>
|
|
1067
|
+
```
|
|
1068
|
+
|
|
1069
|
+
---
|
|
1070
|
+
|
|
1071
|
+
## Summary Checklist
|
|
1072
|
+
|
|
1073
|
+
### Data Management
|
|
1074
|
+
- ✅ Collections: 2 arguments `(models, options)`
|
|
1075
|
+
- ✅ Models: 3 arguments `(attributes, collection, options)`
|
|
1076
|
+
- ✅ Always include `shop_uuid` in options
|
|
1077
|
+
- ✅ Use standard vue-mc methods (`.isEmpty()`, `.isNew()`)
|
|
1078
|
+
- ✅ Let AwTableBuilder handle fetching (no manual `fetch()`)
|
|
1079
|
+
|
|
1080
|
+
### Error Handling
|
|
1081
|
+
- ✅ Bind `:error` to `model.errors.field`
|
|
1082
|
+
- ✅ Check `Object.keys(model.errors).length > 0` after save
|
|
1083
|
+
- ✅ Wrap API calls in try-catch
|
|
1084
|
+
- ✅ Redirect on fetch errors (404)
|
|
1085
|
+
|
|
1086
|
+
### User Feedback
|
|
1087
|
+
- ✅ Use `$notify` for success/error messages
|
|
1088
|
+
- ✅ Use `$confirm` for destructive actions
|
|
1089
|
+
- ✅ Show loading states (`:loading` prop)
|
|
1090
|
+
|
|
1091
|
+
### Date Formatting
|
|
1092
|
+
- ✅ Always use `$dayjs` in templates
|
|
1093
|
+
- ✅ Always use `this.$dayjs` in methods
|
|
1094
|
+
- ✅ Never use native Date methods
|
|
1095
|
+
|
|
1096
|
+
### Navigation
|
|
1097
|
+
- ✅ Always include `shop_uuid` in routes
|
|
1098
|
+
- ✅ Use `href` for navigation, `@click` for actions
|
|
1099
|
+
- ✅ Add mobile breadcrumbs for subpages
|
|
1100
|
+
|
|
1101
|
+
## See Also
|
|
1102
|
+
|
|
1103
|
+
- [Page Patterns](./page-patterns/) - List, detail, and dashboard patterns
|
|
1104
|
+
- [Forms Guide](./forms-guide.md) - Form validation and submission
|
|
1105
|
+
- [Data Fetching Guide](./data-fetching.md) - Working with collections
|
|
1106
|
+
- [Error Handling Guide](./error-handling.md) - Error patterns
|