@sdcorejs/angular 20.0.8 → 20.0.10
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/README.md +270 -140
- package/assets/scss/core/utilities/_grid.scss +25 -19
- package/components/document-builder/index.d.ts +2 -2
- package/components/form-generic/index.d.ts +45 -23
- package/components/import-excel/index.d.ts +2 -2
- package/components/mini-editor/index.d.ts +4 -4
- package/components/modal/index.d.ts +1 -1
- package/components/query-builder/index.d.ts +4 -4
- package/components/tab-router/index.d.ts +3 -3
- package/components/table/index.d.ts +8 -11
- package/components/view/index.d.ts +1 -1
- package/fesm2022/sdcorejs-angular-components-anchor.mjs +15 -18
- package/fesm2022/sdcorejs-angular-components-anchor.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-autoid-inspector.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-avatar.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-badge.mjs +0 -1
- package/fesm2022/sdcorejs-angular-components-badge.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-button.mjs +11 -12
- package/fesm2022/sdcorejs-angular-components-button.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-chart.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-ckeditor-styles.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-code-editor.mjs +1 -1
- package/fesm2022/sdcorejs-angular-components-code-editor.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-document-builder.mjs +17 -26
- package/fesm2022/sdcorejs-angular-components-document-builder.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-editor.mjs +7 -2
- package/fesm2022/sdcorejs-angular-components-editor.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-form-generic.mjs +712 -427
- package/fesm2022/sdcorejs-angular-components-form-generic.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-import-excel.mjs +35 -18
- package/fesm2022/sdcorejs-angular-components-import-excel.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-inform.mjs +0 -1
- package/fesm2022/sdcorejs-angular-components-inform.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-mini-editor.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-modal-resizable.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-modal.mjs +25 -14
- package/fesm2022/sdcorejs-angular-components-modal.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-operator.mjs +1 -1
- package/fesm2022/sdcorejs-angular-components-operator.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-org-chart.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-preview.mjs +33 -29
- package/fesm2022/sdcorejs-angular-components-preview.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-query-bar.mjs +15 -28
- package/fesm2022/sdcorejs-angular-components-query-bar.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-query-builder.mjs +2 -3
- package/fesm2022/sdcorejs-angular-components-query-builder.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-quick-action.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-section.mjs +0 -1
- package/fesm2022/sdcorejs-angular-components-section.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-side-drawer.mjs +3 -3
- package/fesm2022/sdcorejs-angular-components-side-drawer.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-splitter.mjs +5 -5
- package/fesm2022/sdcorejs-angular-components-splitter.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-stepper.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-tab-router.mjs +31 -31
- package/fesm2022/sdcorejs-angular-components-tab-router.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-tab.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-table.mjs +800 -855
- package/fesm2022/sdcorejs-angular-components-table.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-tree.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-upload-file.mjs +6 -6
- package/fesm2022/sdcorejs-angular-components-upload-file.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-view.mjs +3 -9
- package/fesm2022/sdcorejs-angular-components-view.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-configurations.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-directives.mjs +2 -7
- package/fesm2022/sdcorejs-angular-directives.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-autocomplete.mjs +0 -3
- package/fesm2022/sdcorejs-angular-forms-autocomplete.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-checkbox.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-chip-calendar.mjs +2 -2
- package/fesm2022/sdcorejs-angular-forms-chip-calendar.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-chip.mjs +2 -2
- package/fesm2022/sdcorejs-angular-forms-chip.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-date-range.mjs +11 -5
- package/fesm2022/sdcorejs-angular-forms-date-range.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-date.mjs +2 -4
- package/fesm2022/sdcorejs-angular-forms-date.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-datetime.mjs +2 -6
- package/fesm2022/sdcorejs-angular-forms-datetime.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-input-color.mjs +4 -4
- package/fesm2022/sdcorejs-angular-forms-input-color.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-input-number.mjs +1 -1
- package/fesm2022/sdcorejs-angular-forms-input-number.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-input.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-label.mjs +0 -1
- package/fesm2022/sdcorejs-angular-forms-label.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-models.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-radio.mjs +12 -2
- package/fesm2022/sdcorejs-angular-forms-radio.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-select.mjs +45 -14
- package/fesm2022/sdcorejs-angular-forms-select.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-switch.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-textarea.mjs +3 -3
- package/fesm2022/sdcorejs-angular-forms-textarea.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-handlers.mjs +3 -6
- package/fesm2022/sdcorejs-angular-handlers.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-i18n.mjs +11 -7
- package/fesm2022/sdcorejs-angular-i18n.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-interceptors.mjs +0 -2
- package/fesm2022/sdcorejs-angular-interceptors.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-modules-auth.mjs +14 -38
- package/fesm2022/sdcorejs-angular-modules-auth.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-modules-authom.mjs +14 -14
- package/fesm2022/sdcorejs-angular-modules-authom.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-modules-keycloak.mjs +22 -11
- package/fesm2022/sdcorejs-angular-modules-keycloak.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-modules-layout.mjs +30 -27
- package/fesm2022/sdcorejs-angular-modules-layout.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-modules-permission.mjs +5 -12
- package/fesm2022/sdcorejs-angular-modules-permission.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-pipes.mjs +71 -17
- package/fesm2022/sdcorejs-angular-pipes.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-services-api.mjs +0 -2
- package/fesm2022/sdcorejs-angular-services-api.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-services-cache.mjs +5 -11
- package/fesm2022/sdcorejs-angular-services-cache.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-services-confirm.mjs +16 -21
- package/fesm2022/sdcorejs-angular-services-confirm.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-services-docx.mjs +2 -8
- package/fesm2022/sdcorejs-angular-services-docx.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-services-excel.mjs +2 -2
- package/fesm2022/sdcorejs-angular-services-excel.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-services-license.mjs +5 -12
- package/fesm2022/sdcorejs-angular-services-license.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-services-notify.mjs +70 -87
- package/fesm2022/sdcorejs-angular-services-notify.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-services-storage.mjs +6 -15
- package/fesm2022/sdcorejs-angular-services-storage.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-utilities-data-state.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-utilities-extensions.mjs +47 -5
- package/fesm2022/sdcorejs-angular-utilities-extensions.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-utilities-models.mjs +18 -3
- package/fesm2022/sdcorejs-angular-utilities-models.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular.mjs.map +1 -1
- package/forms/chip/index.d.ts +2 -2
- package/forms/chip-calendar/index.d.ts +2 -2
- package/forms/date-range/index.d.ts +2 -0
- package/forms/input-color/index.d.ts +2 -1
- package/forms/select/index.d.ts +10 -2
- package/modules/auth/index.d.ts +7 -9
- package/modules/permission/index.d.ts +5 -6
- package/package.json +2 -2
- package/pipes/index.d.ts +22 -4
- package/services/confirm/index.d.ts +3 -3
- package/services/license/index.d.ts +2 -3
- package/services/notify/index.d.ts +4 -5
- package/services/storage/index.d.ts +2 -3
- package/utilities/extensions/index.d.ts +9 -1
package/README.md
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
- [Getting Started / Cài đặt](#getting-started--cài-đặt)
|
|
15
15
|
- [Theming / SCSS Customization](#theming--scss-customization)
|
|
16
|
+
- [AI Agent Usage Guardrails](#ai-agent-usage-guardrails)
|
|
16
17
|
- [Components](#components)
|
|
17
18
|
- [SdButton](#sdbutton)
|
|
18
19
|
- [SdBadge](#sdbadge)
|
|
@@ -31,12 +32,12 @@
|
|
|
31
32
|
|
|
32
33
|
### Prerequisites / Yêu cầu
|
|
33
34
|
|
|
34
|
-
| Dependency
|
|
35
|
-
|
|
36
|
-
| `@angular/core`
|
|
37
|
-
| `@angular/material`
|
|
35
|
+
| Dependency | Version |
|
|
36
|
+
| ------------------------------------ | ----------------------------------- |
|
|
37
|
+
| `@angular/core` | `^19.0.0 \|\| ^20.0.0 \|\| ^21.0.0` |
|
|
38
|
+
| `@angular/material` | `^19.0.0 \|\| ^20.0.0 \|\| ^21.0.0` |
|
|
38
39
|
| `@angular/material-date-fns-adapter` | `^19.0.0 \|\| ^20.0.0 \|\| ^21.0.0` |
|
|
39
|
-
| `date-fns`
|
|
40
|
+
| `date-fns` | `^3 \|\| ^4` |
|
|
40
41
|
|
|
41
42
|
### Installation / Cài đặt
|
|
42
43
|
|
|
@@ -53,9 +54,7 @@ Thêm vào `angular.json` (hoặc `styles.scss` của app):
|
|
|
53
54
|
```json
|
|
54
55
|
// angular.json
|
|
55
56
|
{
|
|
56
|
-
"styles": [
|
|
57
|
-
"node_modules/@sdcorejs/angular/assets/scss/sd-core.scss"
|
|
58
|
-
]
|
|
57
|
+
"styles": ["node_modules/@sdcorejs/angular/assets/scss/sd-core.scss"]
|
|
59
58
|
}
|
|
60
59
|
```
|
|
61
60
|
|
|
@@ -98,26 +97,26 @@ export const appConfig: ApplicationConfig = {
|
|
|
98
97
|
|
|
99
98
|
**Available color tokens / Các biến màu sắc:**
|
|
100
99
|
|
|
101
|
-
| Variable
|
|
102
|
-
|
|
103
|
-
| `--sd-primary`
|
|
104
|
-
| `--sd-primary-light`
|
|
105
|
-
| `--sd-primary-dark`
|
|
106
|
-
| `--sd-secondary`
|
|
107
|
-
| `--sd-secondary-light` | `#E9E9E9` | Màu phụ nhạt
|
|
108
|
-
| `--sd-success`
|
|
109
|
-
| `--sd-success-light`
|
|
110
|
-
| `--sd-warning`
|
|
111
|
-
| `--sd-warning-light`
|
|
112
|
-
| `--sd-error`
|
|
113
|
-
| `--sd-error-light`
|
|
114
|
-
| `--sd-info`
|
|
115
|
-
| `--sd-info-light`
|
|
116
|
-
| `--sd-black500`
|
|
117
|
-
| `--sd-black400`
|
|
118
|
-
| `--sd-black300`
|
|
119
|
-
| `--sd-black200`
|
|
120
|
-
| `--sd-black100`
|
|
100
|
+
| Variable | Default | Description |
|
|
101
|
+
| ---------------------- | --------- | --------------- |
|
|
102
|
+
| `--sd-primary` | `#2A66F4` | Màu chính |
|
|
103
|
+
| `--sd-primary-light` | `#EAF1FF` | Màu chính nhạt |
|
|
104
|
+
| `--sd-primary-dark` | `#1C4AD9` | Màu chính đậm |
|
|
105
|
+
| `--sd-secondary` | `#212121` | Màu phụ |
|
|
106
|
+
| `--sd-secondary-light` | `#E9E9E9` | Màu phụ nhạt |
|
|
107
|
+
| `--sd-success` | `#4CAF50` | Thành công |
|
|
108
|
+
| `--sd-success-light` | `#DBEFDC` | Thành công nhạt |
|
|
109
|
+
| `--sd-warning` | `#FF9600` | Cảnh báo |
|
|
110
|
+
| `--sd-warning-light` | `#FFEACC` | Cảnh báo nhạt |
|
|
111
|
+
| `--sd-error` | `#F82C13` | Lỗi |
|
|
112
|
+
| `--sd-error-light` | `#FED5D0` | Lỗi nhạt |
|
|
113
|
+
| `--sd-info` | `#2962FF` | Thông tin |
|
|
114
|
+
| `--sd-info-light` | `#E7E9FF` | Thông tin nhạt |
|
|
115
|
+
| `--sd-black500` | `#212121` | Xám đậm nhất |
|
|
116
|
+
| `--sd-black400` | `#757575` | Xám đậm |
|
|
117
|
+
| `--sd-black300` | `#BFBFBF` | Xám trung |
|
|
118
|
+
| `--sd-black200` | `#E6E6E6` | Xám nhạt |
|
|
119
|
+
| `--sd-black100` | `#F2F2F2` | Xám nhạt nhất |
|
|
121
120
|
|
|
122
121
|
### Custom Theme / Tuỳ chỉnh theme
|
|
123
122
|
|
|
@@ -128,13 +127,15 @@ Ghi đè theme mặc định bằng cách truyền map SCSS vào mixin `theme()`
|
|
|
128
127
|
@use '@sdcorejs/angular/assets/scss/themes/default' as default;
|
|
129
128
|
|
|
130
129
|
html {
|
|
131
|
-
@include default.theme(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
130
|
+
@include default.theme(
|
|
131
|
+
(
|
|
132
|
+
primary: #7c3aed,
|
|
133
|
+
primary-light: #ede9fe,
|
|
134
|
+
primary-dark: #5b21b6,
|
|
135
|
+
success: #10b981,
|
|
136
|
+
error: #ef4444,
|
|
137
|
+
)
|
|
138
|
+
);
|
|
138
139
|
}
|
|
139
140
|
```
|
|
140
141
|
|
|
@@ -173,6 +174,131 @@ Thư viện cung cấp sẵn các utility class:
|
|
|
173
174
|
|
|
174
175
|
---
|
|
175
176
|
|
|
177
|
+
## AI Agent Usage Guardrails
|
|
178
|
+
|
|
179
|
+
Use this section as a checklist when generating SDCoreJS Angular code from online docs.
|
|
180
|
+
|
|
181
|
+
### 1. Standalone imports are mandatory
|
|
182
|
+
|
|
183
|
+
Every component, directive, and pipe used in a standalone template must be listed in the host component's `imports`. This includes template-only directives such as `sdTableCellDef`, `sdTableFilterDef`, `sdSuffixDef`, `sdViewDef`, `sdItemDef`, and shared display pipes.
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
import { Component } from '@angular/core';
|
|
187
|
+
import { SdButton } from '@sdcorejs/angular/components/button';
|
|
188
|
+
import {
|
|
189
|
+
SdTable,
|
|
190
|
+
SdTableCellDefDirective,
|
|
191
|
+
SdTableFilterDefDirective,
|
|
192
|
+
SdTableFooterDefDirective,
|
|
193
|
+
SdTableTitleDefDirective,
|
|
194
|
+
} from '@sdcorejs/angular/components/table';
|
|
195
|
+
import { SdInput, SdInputNumber, SdSelect } from '@sdcorejs/angular/forms';
|
|
196
|
+
import { SdItemDefDefDirective, SdSuffixDefDirective, SdViewDefDirective } from '@sdcorejs/angular/forms/directives';
|
|
197
|
+
import { SdFormatDatePipe, SdFormatDatetimePipe, SdFormatNumberPipe, SdViewPipe } from '@sdcorejs/angular/pipes';
|
|
198
|
+
|
|
199
|
+
@Component({
|
|
200
|
+
standalone: true,
|
|
201
|
+
imports: [
|
|
202
|
+
SdButton,
|
|
203
|
+
SdTable,
|
|
204
|
+
SdTableCellDefDirective,
|
|
205
|
+
SdTableFilterDefDirective,
|
|
206
|
+
SdTableFooterDefDirective,
|
|
207
|
+
SdTableTitleDefDirective,
|
|
208
|
+
SdInput,
|
|
209
|
+
SdInputNumber,
|
|
210
|
+
SdSelect,
|
|
211
|
+
SdItemDefDefDirective,
|
|
212
|
+
SdSuffixDefDirective,
|
|
213
|
+
SdViewDefDirective,
|
|
214
|
+
SdFormatNumberPipe,
|
|
215
|
+
SdFormatDatePipe,
|
|
216
|
+
SdFormatDatetimePipe,
|
|
217
|
+
SdViewPipe,
|
|
218
|
+
],
|
|
219
|
+
templateUrl: './list.component.html',
|
|
220
|
+
})
|
|
221
|
+
export class ListComponent {}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Common missed imports:
|
|
225
|
+
|
|
226
|
+
| Template usage | Required import |
|
|
227
|
+
| ----------------------------------------- | --------------------------------------------------------------------- | ----------------------------------------------------- |
|
|
228
|
+
| `<ng-template sdTableCellDef="amount">` | `SdTableCellDefDirective` from `@sdcorejs/angular/components/table` |
|
|
229
|
+
| `<ng-template sdTableFilterDef="status">` | `SdTableFilterDefDirective` from `@sdcorejs/angular/components/table` |
|
|
230
|
+
| `<ng-template sdTableTitleDef="name">` | `SdTableTitleDefDirective` from `@sdcorejs/angular/components/table` |
|
|
231
|
+
| `<ng-template sdTableFooterDef="amount">` | `SdTableFooterDefDirective` from `@sdcorejs/angular/components/table` |
|
|
232
|
+
| `<ng-template sdTableExpandDef>` | `SdTableExpandDefDirective` from `@sdcorejs/angular/components/table` |
|
|
233
|
+
| `<ng-template sdSuffixDef>` | `SdSuffixDefDirective` from `@sdcorejs/angular/forms/directives` |
|
|
234
|
+
| `<ng-template sdViewDef>` | `SdViewDefDirective` from `@sdcorejs/angular/forms/directives` |
|
|
235
|
+
| `<ng-template sdItemDef>` | `SdItemDefDefDirective` from `@sdcorejs/angular/forms/directives` |
|
|
236
|
+
| ` | sdFormatNumber` | `SdFormatNumberPipe` from `@sdcorejs/angular/pipes` |
|
|
237
|
+
| ` | sdFormatDate` | `SdFormatDatePipe` from `@sdcorejs/angular/pipes` |
|
|
238
|
+
| ` | sdFormatDatetime` | `SdFormatDatetimePipe` from `@sdcorejs/angular/pipes` |
|
|
239
|
+
| ` | sdView` | `SdViewPipe` from `@sdcorejs/angular/pipes` |
|
|
240
|
+
|
|
241
|
+
### 2. Table cells use dense form controls
|
|
242
|
+
|
|
243
|
+
When rendering an editable SD form control inside `<sd-table>` cell templates or custom inline filter templates, always use `size="sm"` and `hideInlineError`. This keeps row height stable and surfaces validation through the compact error icon/tooltip instead of inserting inline error text inside the table row.
|
|
244
|
+
|
|
245
|
+
```html
|
|
246
|
+
<sd-table [option]="tableOption">
|
|
247
|
+
<ng-template sdTableCellDef="quantity" let-row>
|
|
248
|
+
<sd-input-number size="sm" hideInlineError type="positive" [precision]="0" [(model)]="row.quantity"> </sd-input-number>
|
|
249
|
+
</ng-template>
|
|
250
|
+
|
|
251
|
+
<ng-template sdTableCellDef="status" let-row>
|
|
252
|
+
<sd-select size="sm" hideInlineError [items]="statusList" valueField="code" displayField="name" [(model)]="row.status"> </sd-select>
|
|
253
|
+
</ng-template>
|
|
254
|
+
|
|
255
|
+
<ng-template sdTableFilterDef="keyword" let-filter let-update="update">
|
|
256
|
+
<sd-input size="sm" hideInlineError [(model)]="filter.keyword" (keyupEnter)="update()"> </sd-input>
|
|
257
|
+
</ng-template>
|
|
258
|
+
</sd-table>
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### 3. Prefer shared display pipes
|
|
262
|
+
|
|
263
|
+
Do not create app-local pipes for common display formatting.
|
|
264
|
+
|
|
265
|
+
```html
|
|
266
|
+
<!-- Good -->
|
|
267
|
+
{{ row.amount | sdFormatNumber : 0 | sdView }} {{ row.issueDate | sdFormatDate | sdView }} {{ row.updatedAt | sdFormatDatetime : 'dd/MM/yyyy
|
|
268
|
+
HH:mm' | sdView }} {{ row.tags | sdView }}
|
|
269
|
+
|
|
270
|
+
<!-- Avoid -->
|
|
271
|
+
{{ row.amount | customCurrency }} {{ row.issueDate | date : 'dd/MM/yyyy' }} {{ row.tags?.join(', ') || '--' }}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
- `sdFormatNumber` formats numbers using SDCoreJS locale configuration.
|
|
275
|
+
- `sdFormatDate` formats date-only values with default `dd/MM/yyyy`.
|
|
276
|
+
- `sdFormatDatetime` formats date-time values with default `dd/MM/yyyy HH:mm:ss`.
|
|
277
|
+
- `sdView` renders `--` for `null`, `undefined`, `''`, `NaN`, and empty arrays; primitive arrays render as `A, B`.
|
|
278
|
+
|
|
279
|
+
### 4. Command icons default to Material Symbols Outlined
|
|
280
|
+
|
|
281
|
+
Table row commands and child command menu items default to the outline Material icon font set. Only set `fontSet` when a specific icon family is required.
|
|
282
|
+
|
|
283
|
+
```ts
|
|
284
|
+
command: {
|
|
285
|
+
align: 'right',
|
|
286
|
+
commands: [
|
|
287
|
+
{ icon: 'edit', title: 'Edit', click: row => this.edit(row) },
|
|
288
|
+
{
|
|
289
|
+
icon: 'more_vert',
|
|
290
|
+
title: 'More',
|
|
291
|
+
children: [
|
|
292
|
+
{ icon: 'visibility', title: 'View', click: row => this.view(row) },
|
|
293
|
+
{ icon: 'delete', title: 'Delete', color: 'error', click: row => this.delete(row) },
|
|
294
|
+
],
|
|
295
|
+
},
|
|
296
|
+
],
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
176
302
|
## Components
|
|
177
303
|
|
|
178
304
|
> Tất cả component đều là **standalone** và sử dụng **Angular Signals** API.
|
|
@@ -186,18 +312,18 @@ import { SdButton } from '@sdcorejs/angular/components/button';
|
|
|
186
312
|
|
|
187
313
|
**Inputs:**
|
|
188
314
|
|
|
189
|
-
| Input
|
|
190
|
-
|
|
191
|
-
| `type`
|
|
192
|
-
| `color`
|
|
193
|
-
| `size`
|
|
194
|
-
| `title`
|
|
195
|
-
| `prefixIcon` | `string`
|
|
196
|
-
| `suffixIcon` | `string`
|
|
197
|
-
| `disabled`
|
|
198
|
-
| `loading`
|
|
199
|
-
| `tooltip`
|
|
200
|
-
| `width`
|
|
315
|
+
| Input | Type | Default | Description |
|
|
316
|
+
| ------------ | ------------------------------------------ | ------------- | ---------------------------------- |
|
|
317
|
+
| `type` | `'fill' \| 'light' \| 'outline' \| 'link'` | `'light'` | Kiểu nút |
|
|
318
|
+
| `color` | `Color` | `'secondary'` | Màu sắc |
|
|
319
|
+
| `size` | `'sm' \| 'md' \| 'lg'` | `'sm'` | Kích thước |
|
|
320
|
+
| `title` | `string` | — | Nhãn nút |
|
|
321
|
+
| `prefixIcon` | `string` | — | Icon Material trước text |
|
|
322
|
+
| `suffixIcon` | `string` | — | Icon Material sau text |
|
|
323
|
+
| `disabled` | `boolean` | `false` | Vô hiệu hoá |
|
|
324
|
+
| `loading` | `boolean` | `false` | Trạng thái loading (tự chặn click) |
|
|
325
|
+
| `tooltip` | `string` | — | Tooltip khi hover |
|
|
326
|
+
| `width` | `string` | — | CSS width tuỳ chỉnh |
|
|
201
327
|
|
|
202
328
|
**Output:** `(click): EventEmitter<Event>` — có throttle 300ms, tự chặn khi `disabled` hoặc `loading`.
|
|
203
329
|
|
|
@@ -218,14 +344,14 @@ import { SdBadge } from '@sdcorejs/angular/components/badge';
|
|
|
218
344
|
|
|
219
345
|
**Inputs:**
|
|
220
346
|
|
|
221
|
-
| Input
|
|
222
|
-
|
|
223
|
-
| `type`
|
|
224
|
-
| `color`
|
|
225
|
-
| `title`
|
|
226
|
-
| `icon`
|
|
227
|
-
| `size`
|
|
228
|
-
| `tooltip` | `string`
|
|
347
|
+
| Input | Type | Default | Description |
|
|
348
|
+
| --------- | ---------------------------- | ------------- | ----------------- |
|
|
349
|
+
| `type` | `'tag' \| 'round' \| 'icon'` | `'icon'` | Kiểu badge |
|
|
350
|
+
| `color` | `Color` | `'secondary'` | Màu sắc |
|
|
351
|
+
| `title` | `string \| number` | — | Nội dung hiển thị |
|
|
352
|
+
| `icon` | `string` | — | Icon Material |
|
|
353
|
+
| `size` | `Size` | `'sm'` | Kích thước |
|
|
354
|
+
| `tooltip` | `string` | — | Tooltip |
|
|
229
355
|
|
|
230
356
|
Shorthand color inputs (boolean): `primary`, `secondary`, `success`, `info`, `warning`, `error`.
|
|
231
357
|
|
|
@@ -245,15 +371,15 @@ import { SdSection } from '@sdcorejs/angular/components/section';
|
|
|
245
371
|
|
|
246
372
|
**Inputs:**
|
|
247
373
|
|
|
248
|
-
| Input
|
|
249
|
-
|
|
250
|
-
| `title`
|
|
251
|
-
| `subTitle`
|
|
252
|
-
| `icon`
|
|
253
|
-
| `iconColor`
|
|
254
|
-
| `collapsable` | `boolean` | `false`
|
|
255
|
-
| `collapsed`
|
|
256
|
-
| `hideHeader`
|
|
374
|
+
| Input | Type | Default | Description |
|
|
375
|
+
| ------------- | --------- | ----------- | -------------------------- |
|
|
376
|
+
| `title` | `string` | _required_ | Tiêu đề section |
|
|
377
|
+
| `subTitle` | `string` | — | Tiêu đề phụ |
|
|
378
|
+
| `icon` | `string` | — | Icon Material |
|
|
379
|
+
| `iconColor` | `Color` | `'primary'` | Màu icon |
|
|
380
|
+
| `collapsable` | `boolean` | `false` | Cho phép thu gọn |
|
|
381
|
+
| `collapsed` | `boolean` | `false` | Trạng thái ban đầu thu gọn |
|
|
382
|
+
| `hideHeader` | `boolean` | `false` | Ẩn phần header |
|
|
257
383
|
|
|
258
384
|
```html
|
|
259
385
|
<sd-section title="Thông tin cơ bản" icon="person" iconColor="primary">
|
|
@@ -275,18 +401,19 @@ import { SdModal } from '@sdcorejs/angular/components/modal';
|
|
|
275
401
|
|
|
276
402
|
**Inputs:**
|
|
277
403
|
|
|
278
|
-
| Input
|
|
279
|
-
|
|
280
|
-
| `title`
|
|
281
|
-
| `color`
|
|
282
|
-
| `width`
|
|
283
|
-
| `height`
|
|
284
|
-
| `view`
|
|
285
|
-
| `lazyLoadContent` | `boolean`
|
|
404
|
+
| Input | Type | Default | Description |
|
|
405
|
+
| ----------------- | ---------------------------------------- | ----------- | -------------------------------- |
|
|
406
|
+
| `title` | `string` | — | Tiêu đề modal |
|
|
407
|
+
| `color` | `Color` | `'primary'` | Màu header |
|
|
408
|
+
| `width` | `'sx' \| 'sm' \| 'md' \| 'lg' \| string` | `'md'` | Độ rộng (md = 60vw) |
|
|
409
|
+
| `height` | `string` | `'auto'` | Chiều cao |
|
|
410
|
+
| `view` | `'dialog' \| 'bottom-sheet'` | auto | Tự động bottom-sheet trên mobile |
|
|
411
|
+
| `lazyLoadContent` | `boolean` | `true` | Lazy render nội dung |
|
|
286
412
|
|
|
287
413
|
**Output:** `(sdClosed): EventEmitter` — phát ra khi modal đóng.
|
|
288
414
|
|
|
289
415
|
**Methods** (dùng qua `@ViewChild`):
|
|
416
|
+
|
|
290
417
|
- `modal.open()` — mở modal
|
|
291
418
|
- `modal.close()` — đóng modal
|
|
292
419
|
|
|
@@ -321,17 +448,17 @@ SdTable nhận một object `option` duy nhất kiểu `SdTableOption<T>`.
|
|
|
321
448
|
|
|
322
449
|
**Column types / Kiểu cột:**
|
|
323
450
|
|
|
324
|
-
| `type`
|
|
325
|
-
|
|
326
|
-
| `'string'`
|
|
327
|
-
| `'number'`
|
|
328
|
-
| `'boolean'`
|
|
329
|
-
| `'date'`
|
|
330
|
-
| `'datetime'`
|
|
331
|
-
| `'time'`
|
|
332
|
-
| `'values'`
|
|
333
|
-
| `'lazy-values'` | Enum load async
|
|
334
|
-
| `'children'`
|
|
451
|
+
| `type` | Mô tả |
|
|
452
|
+
| --------------- | ------------------------- |
|
|
453
|
+
| `'string'` | Văn bản |
|
|
454
|
+
| `'number'` | Số (tự format) |
|
|
455
|
+
| `'boolean'` | True/False |
|
|
456
|
+
| `'date'` | Ngày |
|
|
457
|
+
| `'datetime'` | Ngày giờ |
|
|
458
|
+
| `'time'` | Giờ |
|
|
459
|
+
| `'values'` | Enum từ danh sách cố định |
|
|
460
|
+
| `'lazy-values'` | Enum load async |
|
|
461
|
+
| `'children'` | Cột nhóm (multi-header) |
|
|
335
462
|
|
|
336
463
|
**Local table:**
|
|
337
464
|
|
|
@@ -340,11 +467,14 @@ option: SdTableOption<Product> = {
|
|
|
340
467
|
type: 'local',
|
|
341
468
|
items: () => this.products,
|
|
342
469
|
columns: [
|
|
343
|
-
{ field: 'code',
|
|
344
|
-
{ field: 'name',
|
|
345
|
-
{ field: 'price', type: 'number',
|
|
346
|
-
{
|
|
347
|
-
|
|
470
|
+
{ field: 'code', type: 'string', title: 'Mã', width: '120px' },
|
|
471
|
+
{ field: 'name', type: 'string', title: 'Tên', sortable: true },
|
|
472
|
+
{ field: 'price', type: 'number', title: 'Đơn giá', align: 'right' },
|
|
473
|
+
{
|
|
474
|
+
field: 'active',
|
|
475
|
+
type: 'boolean',
|
|
476
|
+
title: 'Kích hoạt',
|
|
477
|
+
useBadge: val => ({ color: val ? 'success' : 'secondary', title: val ? 'Có' : 'Không' }),
|
|
348
478
|
},
|
|
349
479
|
],
|
|
350
480
|
paginate: { pageSize: 20 },
|
|
@@ -368,15 +498,18 @@ option: SdTableOption<Product> = {
|
|
|
368
498
|
columns: [
|
|
369
499
|
{ field: 'code', type: 'string', title: 'Mã', width: '120px' },
|
|
370
500
|
{ field: 'name', type: 'string', title: 'Tên', sortable: true },
|
|
371
|
-
{
|
|
372
|
-
|
|
501
|
+
{
|
|
502
|
+
field: 'status',
|
|
503
|
+
type: 'values',
|
|
504
|
+
title: 'Trạng thái',
|
|
505
|
+
useBadge: val => ({ color: val === 'ACTIVE' ? 'success' : 'secondary', title: val }),
|
|
373
506
|
},
|
|
374
507
|
],
|
|
375
508
|
command: {
|
|
376
509
|
align: 'right',
|
|
377
510
|
commands: [
|
|
378
|
-
{ icon: 'edit',
|
|
379
|
-
{ icon: 'delete', color: 'error',
|
|
511
|
+
{ icon: 'edit', color: 'primary', title: 'Sửa', click: row => this.onEdit(row) },
|
|
512
|
+
{ icon: 'delete', color: 'error', title: 'Xoá', click: row => this.onDelete(row) },
|
|
380
513
|
],
|
|
381
514
|
},
|
|
382
515
|
};
|
|
@@ -414,28 +547,27 @@ import { SdAvatar } from '@sdcorejs/angular/components/avatar';
|
|
|
414
547
|
```
|
|
415
548
|
|
|
416
549
|
```html
|
|
417
|
-
<sd-avatar src="/api/avatar/123" name="Nguyễn Văn A" size="md" />
|
|
418
|
-
<sd-avatar name="NVA" color="primary" size="lg" />
|
|
550
|
+
<sd-avatar src="/api/avatar/123" name="Nguyễn Văn A" size="md" /> <sd-avatar name="NVA" color="primary" size="lg" />
|
|
419
551
|
```
|
|
420
552
|
|
|
421
553
|
---
|
|
422
554
|
|
|
423
555
|
### Other Components / Các component khác
|
|
424
556
|
|
|
425
|
-
| Component
|
|
426
|
-
|
|
427
|
-
| `SdTabRouter`
|
|
428
|
-
| `SdSideDrawer`
|
|
429
|
-
| `SdUploadFile`
|
|
430
|
-
| `SdQuickAction`
|
|
431
|
-
| `SdHistory`
|
|
432
|
-
| `SdImportExcel`
|
|
433
|
-
| `SdQueryBuilder`
|
|
434
|
-
| `SdCodeEditor`
|
|
435
|
-
| `SdMiniEditor`
|
|
436
|
-
| `SdDocumentBuilder` | `@sdcorejs/angular/components/document-builder` | Document builder
|
|
437
|
-
| `SdAnchorMain`
|
|
438
|
-
| `SdView`
|
|
557
|
+
| Component | Import | Mô tả |
|
|
558
|
+
| ------------------- | ----------------------------------------------- | --------------------------------- |
|
|
559
|
+
| `SdTabRouter` | `@sdcorejs/angular/components/tab-router` | Tab navigation với Angular Router |
|
|
560
|
+
| `SdSideDrawer` | `@sdcorejs/angular/components/side-drawer` | Drawer layout trái/phải |
|
|
561
|
+
| `SdUploadFile` | `@sdcorejs/angular/components/upload-file` | Upload file |
|
|
562
|
+
| `SdQuickAction` | `@sdcorejs/angular/components/quick-action` | Nút action dạng icon |
|
|
563
|
+
| `SdHistory` | `@sdcorejs/angular/components/history` | Lịch sử thay đổi |
|
|
564
|
+
| `SdImportExcel` | `@sdcorejs/angular/components/import-excel` | Wizard import Excel |
|
|
565
|
+
| `SdQueryBuilder` | `@sdcorejs/angular/components/query-builder` | Visual query builder |
|
|
566
|
+
| `SdCodeEditor` | `@sdcorejs/angular/components/code-editor` | Code editor (PrismJS) |
|
|
567
|
+
| `SdMiniEditor` | `@sdcorejs/angular/components/mini-editor` | Rich text editor nhỏ |
|
|
568
|
+
| `SdDocumentBuilder` | `@sdcorejs/angular/components/document-builder` | Document builder |
|
|
569
|
+
| `SdAnchorMain` | `@sdcorejs/angular/components/anchor` | Anchor / mục lục cuộn trang |
|
|
570
|
+
| `SdView` | `@sdcorejs/angular/components/view` | View wrapper read-only |
|
|
439
571
|
|
|
440
572
|
---
|
|
441
573
|
|
|
@@ -443,11 +575,11 @@ import { SdAvatar } from '@sdcorejs/angular/components/avatar';
|
|
|
443
575
|
|
|
444
576
|
```typescript
|
|
445
577
|
import {
|
|
446
|
-
SdInput,
|
|
447
|
-
SdInputNumber,
|
|
448
|
-
SdSelect,
|
|
578
|
+
SdInput, // Text input
|
|
579
|
+
SdInputNumber, // Number input
|
|
580
|
+
SdSelect, // Dropdown
|
|
449
581
|
SdAutocomplete, // Autocomplete
|
|
450
|
-
SdDate,
|
|
582
|
+
SdDate, // Date picker
|
|
451
583
|
} from '@sdcorejs/angular/forms';
|
|
452
584
|
```
|
|
453
585
|
|
|
@@ -456,8 +588,7 @@ import {
|
|
|
456
588
|
|
|
457
589
|
<sd-input-number [(model)]="form.price" label="Đơn giá" [min]="0" suffix="VNĐ" />
|
|
458
590
|
|
|
459
|
-
<sd-select [(model)]="form.status" label="Trạng thái"
|
|
460
|
-
[items]="statusList" valueField="value" displayField="label" />
|
|
591
|
+
<sd-select [(model)]="form.status" label="Trạng thái" [items]="statusList" valueField="value" displayField="label" />
|
|
461
592
|
|
|
462
593
|
<sd-date [(model)]="form.birthday" label="Ngày sinh" />
|
|
463
594
|
```
|
|
@@ -500,7 +631,7 @@ export class ProductListComponent implements OnInit {
|
|
|
500
631
|
isSaving = signal(false);
|
|
501
632
|
|
|
502
633
|
readonly STATUS_LIST = [
|
|
503
|
-
{ value: 'ACTIVE',
|
|
634
|
+
{ value: 'ACTIVE', label: 'Hoạt động' },
|
|
504
635
|
{ value: 'INACTIVE', label: 'Dừng' },
|
|
505
636
|
];
|
|
506
637
|
|
|
@@ -513,13 +644,15 @@ export class ProductListComponent implements OnInit {
|
|
|
513
644
|
type: 'server',
|
|
514
645
|
items: async (filter, paging) => this.service.search(filter, paging),
|
|
515
646
|
columns: [
|
|
516
|
-
{ field: 'code',
|
|
517
|
-
{ field: 'name',
|
|
518
|
-
{ field: 'price', type: 'number', title: 'Đơn giá',
|
|
647
|
+
{ field: 'code', type: 'string', title: 'Mã', width: '120px' },
|
|
648
|
+
{ field: 'name', type: 'string', title: 'Tên', sortable: true },
|
|
649
|
+
{ field: 'price', type: 'number', title: 'Đơn giá', align: 'right' },
|
|
519
650
|
{
|
|
520
|
-
field: 'status',
|
|
651
|
+
field: 'status',
|
|
652
|
+
type: 'values',
|
|
653
|
+
title: 'Trạng thái',
|
|
521
654
|
option: { items: this.STATUS_LIST, valueField: 'value', displayField: 'label' },
|
|
522
|
-
useBadge:
|
|
655
|
+
useBadge: val => ({
|
|
523
656
|
color: val === 'ACTIVE' ? 'success' : 'secondary',
|
|
524
657
|
title: this.STATUS_LIST.find(s => s.value === val)?.label,
|
|
525
658
|
}),
|
|
@@ -528,8 +661,8 @@ export class ProductListComponent implements OnInit {
|
|
|
528
661
|
command: {
|
|
529
662
|
align: 'right',
|
|
530
663
|
commands: [
|
|
531
|
-
{ icon: 'edit',
|
|
532
|
-
{ icon: 'delete', color: 'error',
|
|
664
|
+
{ icon: 'edit', color: 'primary', title: 'Sửa', click: row => this.openForm(row) },
|
|
665
|
+
{ icon: 'delete', color: 'error', title: 'Xoá', click: row => this.onDelete(row) },
|
|
533
666
|
],
|
|
534
667
|
},
|
|
535
668
|
paginate: { pageSize: 20 },
|
|
@@ -572,8 +705,7 @@ export class ProductListComponent implements OnInit {
|
|
|
572
705
|
<!-- product-list.component.html -->
|
|
573
706
|
<div class="d-flex justify-content-between align-items-center mb-16">
|
|
574
707
|
<h2>Danh sách sản phẩm</h2>
|
|
575
|
-
<sd-button type="fill" color="primary" title="Thêm mới"
|
|
576
|
-
prefixIcon="add" (click)="openForm()" />
|
|
708
|
+
<sd-button type="fill" color="primary" title="Thêm mới" prefixIcon="add" (click)="openForm()" />
|
|
577
709
|
</div>
|
|
578
710
|
|
|
579
711
|
<sd-table #sdTable [option]="option" />
|
|
@@ -587,8 +719,7 @@ export class ProductListComponent implements OnInit {
|
|
|
587
719
|
<sd-input [(model)]="formData.code" label="Mã" [required]="true" />
|
|
588
720
|
</div>
|
|
589
721
|
<div class="col-6">
|
|
590
|
-
<sd-select [(model)]="formData.status" label="Trạng thái"
|
|
591
|
-
[items]="STATUS_LIST" valueField="value" displayField="label" />
|
|
722
|
+
<sd-select [(model)]="formData.status" label="Trạng thái" [items]="STATUS_LIST" valueField="value" displayField="label" />
|
|
592
723
|
</div>
|
|
593
724
|
<div class="col-12">
|
|
594
725
|
<sd-input [(model)]="formData.name" label="Tên sản phẩm" [required]="true" />
|
|
@@ -601,8 +732,7 @@ export class ProductListComponent implements OnInit {
|
|
|
601
732
|
</div>
|
|
602
733
|
<div class="modal-footer d-flex justify-content-end gap-8 p-16">
|
|
603
734
|
<sd-button title="Huỷ" (click)="formModal.close()" />
|
|
604
|
-
<sd-button type="fill" color="primary" title="Lưu"
|
|
605
|
-
prefixIcon="save" [loading]="isSaving()" (click)="onSave()" />
|
|
735
|
+
<sd-button type="fill" color="primary" title="Lưu" prefixIcon="save" [loading]="isSaving()" (click)="onSave()" />
|
|
606
736
|
</div>
|
|
607
737
|
</ng-template>
|
|
608
738
|
</sd-modal>
|
|
@@ -682,14 +812,14 @@ import { Color } from '@sdcorejs/angular/utilities';
|
|
|
682
812
|
@Component({
|
|
683
813
|
selector: 'sd-my-component',
|
|
684
814
|
templateUrl: './my-component.component.html',
|
|
685
|
-
|
|
815
|
+
styleUrl: './my-component.component.scss',
|
|
686
816
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
687
817
|
standalone: true,
|
|
688
818
|
imports: [],
|
|
689
819
|
})
|
|
690
820
|
export class SdMyComponent extends SdBaseSecureComponent {
|
|
691
821
|
color = input<Color, Color | undefined | null>('primary', {
|
|
692
|
-
transform:
|
|
822
|
+
transform: value => value || 'primary',
|
|
693
823
|
});
|
|
694
824
|
|
|
695
825
|
title = input<string | undefined | null>(undefined);
|
|
@@ -705,16 +835,16 @@ export * from '@sdcorejs/angular/components/my-component';
|
|
|
705
835
|
|
|
706
836
|
### Quy ước / Conventions
|
|
707
837
|
|
|
708
|
-
| Mục
|
|
709
|
-
|
|
710
|
-
| Selector
|
|
711
|
-
| Class name
|
|
712
|
-
| Input
|
|
713
|
-
| Null safety
|
|
714
|
-
| Base class
|
|
715
|
-
| Change detection | Luôn dùng `ChangeDetectionStrategy.OnPush`
|
|
716
|
-
| Standalone
|
|
717
|
-
| Colors
|
|
838
|
+
| Mục | Quy ước |
|
|
839
|
+
| ---------------- | ------------------------------------------------------------- |
|
|
840
|
+
| Selector | `sd-<tên-component>` |
|
|
841
|
+
| Class name | `Sd<TênComponent>` (Pascal) |
|
|
842
|
+
| Input | Dùng `input<T>()` signal, **không** dùng `@Input()` decorator |
|
|
843
|
+
| Null safety | Input transform phải handle `null/undefined` |
|
|
844
|
+
| Base class | Extend `SdBaseSecureComponent` cho component có permission |
|
|
845
|
+
| Change detection | Luôn dùng `ChangeDetectionStrategy.OnPush` |
|
|
846
|
+
| Standalone | Luôn `standalone: true` |
|
|
847
|
+
| Colors | Dùng `Color` type, không hardcode màu |
|
|
718
848
|
|
|
719
849
|
### Build
|
|
720
850
|
|