@fui-org/fui-cli 0.1.1 → 0.3.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 (212) hide show
  1. package/README.md +18 -3
  2. package/dist/fui.js +160 -43
  3. package/package.json +4 -2
  4. package/skills/fui/SKILL.md +131 -66
  5. package/skills/fui-skill/README.md +112 -0
  6. package/skills/fui-skill/SKILL.md +269 -0
  7. package/skills/fui-skill/assets/projectdefaultstyle.css +518 -0
  8. package/skills/fui-skill/design-md/airbnb/DESIGN.md +545 -0
  9. package/skills/fui-skill/design-md/airbnb/README.md +5 -0
  10. package/skills/fui-skill/design-md/airtable/DESIGN.md +554 -0
  11. package/skills/fui-skill/design-md/airtable/README.md +5 -0
  12. package/skills/fui-skill/design-md/apple/DESIGN.md +562 -0
  13. package/skills/fui-skill/design-md/apple/README.md +5 -0
  14. package/skills/fui-skill/design-md/asu/DESIGN.md +179 -0
  15. package/skills/fui-skill/design-md/asu/README.md +107 -0
  16. package/skills/fui-skill/design-md/binance/DESIGN.md +634 -0
  17. package/skills/fui-skill/design-md/binance/README.md +5 -0
  18. package/skills/fui-skill/design-md/bmw/DESIGN.md +544 -0
  19. package/skills/fui-skill/design-md/bmw/README.md +5 -0
  20. package/skills/fui-skill/design-md/bmw-m/DESIGN.md +503 -0
  21. package/skills/fui-skill/design-md/bmw-m/README.md +5 -0
  22. package/skills/fui-skill/design-md/bugatti/DESIGN.md +454 -0
  23. package/skills/fui-skill/design-md/bugatti/README.md +5 -0
  24. package/skills/fui-skill/design-md/cal/DESIGN.md +542 -0
  25. package/skills/fui-skill/design-md/cal/README.md +5 -0
  26. package/skills/fui-skill/design-md/claude/DESIGN.md +589 -0
  27. package/skills/fui-skill/design-md/claude/README.md +5 -0
  28. package/skills/fui-skill/design-md/clay/DESIGN.md +541 -0
  29. package/skills/fui-skill/design-md/clay/README.md +5 -0
  30. package/skills/fui-skill/design-md/clickhouse/DESIGN.md +544 -0
  31. package/skills/fui-skill/design-md/clickhouse/README.md +5 -0
  32. package/skills/fui-skill/design-md/cohere/DESIGN.md +451 -0
  33. package/skills/fui-skill/design-md/cohere/README.md +5 -0
  34. package/skills/fui-skill/design-md/coinbase/DESIGN.md +570 -0
  35. package/skills/fui-skill/design-md/coinbase/README.md +5 -0
  36. package/skills/fui-skill/design-md/composio/DESIGN.md +506 -0
  37. package/skills/fui-skill/design-md/composio/README.md +5 -0
  38. package/skills/fui-skill/design-md/cursor/DESIGN.md +537 -0
  39. package/skills/fui-skill/design-md/cursor/README.md +5 -0
  40. package/skills/fui-skill/design-md/elevenlabs/DESIGN.md +504 -0
  41. package/skills/fui-skill/design-md/elevenlabs/README.md +5 -0
  42. package/skills/fui-skill/design-md/expo/DESIGN.md +526 -0
  43. package/skills/fui-skill/design-md/expo/README.md +5 -0
  44. package/skills/fui-skill/design-md/ferrari/DESIGN.md +531 -0
  45. package/skills/fui-skill/design-md/ferrari/README.md +5 -0
  46. package/skills/fui-skill/design-md/figma/DESIGN.md +578 -0
  47. package/skills/fui-skill/design-md/figma/README.md +5 -0
  48. package/skills/fui-skill/design-md/framer/DESIGN.md +544 -0
  49. package/skills/fui-skill/design-md/framer/README.md +5 -0
  50. package/skills/fui-skill/design-md/fui/DESIGN.md +532 -0
  51. package/skills/fui-skill/design-md/hashicorp/DESIGN.md +575 -0
  52. package/skills/fui-skill/design-md/hashicorp/README.md +5 -0
  53. package/skills/fui-skill/design-md/ibm/DESIGN.md +550 -0
  54. package/skills/fui-skill/design-md/ibm/README.md +5 -0
  55. package/skills/fui-skill/design-md/intercom/DESIGN.md +546 -0
  56. package/skills/fui-skill/design-md/intercom/README.md +5 -0
  57. package/skills/fui-skill/design-md/kraken/DESIGN.md +125 -0
  58. package/skills/fui-skill/design-md/kraken/README.md +5 -0
  59. package/skills/fui-skill/design-md/lamborghini/DESIGN.md +288 -0
  60. package/skills/fui-skill/design-md/lamborghini/README.md +5 -0
  61. package/skills/fui-skill/design-md/linear.app/DESIGN.md +548 -0
  62. package/skills/fui-skill/design-md/linear.app/README.md +5 -0
  63. package/skills/fui-skill/design-md/lovable/DESIGN.md +298 -0
  64. package/skills/fui-skill/design-md/lovable/README.md +5 -0
  65. package/skills/fui-skill/design-md/mastercard/DESIGN.md +365 -0
  66. package/skills/fui-skill/design-md/mastercard/README.md +5 -0
  67. package/skills/fui-skill/design-md/meta/DESIGN.md +683 -0
  68. package/skills/fui-skill/design-md/meta/README.md +5 -0
  69. package/skills/fui-skill/design-md/minimax/DESIGN.md +746 -0
  70. package/skills/fui-skill/design-md/minimax/README.md +5 -0
  71. package/skills/fui-skill/design-md/mintlify/DESIGN.md +852 -0
  72. package/skills/fui-skill/design-md/mintlify/README.md +5 -0
  73. package/skills/fui-skill/design-md/miro/DESIGN.md +825 -0
  74. package/skills/fui-skill/design-md/miro/README.md +5 -0
  75. package/skills/fui-skill/design-md/mistral.ai/DESIGN.md +773 -0
  76. package/skills/fui-skill/design-md/mistral.ai/README.md +5 -0
  77. package/skills/fui-skill/design-md/mongodb/DESIGN.md +767 -0
  78. package/skills/fui-skill/design-md/mongodb/README.md +5 -0
  79. package/skills/fui-skill/design-md/nike/DESIGN.md +575 -0
  80. package/skills/fui-skill/design-md/nike/README.md +5 -0
  81. package/skills/fui-skill/design-md/notion/DESIGN.md +821 -0
  82. package/skills/fui-skill/design-md/notion/README.md +5 -0
  83. package/skills/fui-skill/design-md/nvidia/DESIGN.md +640 -0
  84. package/skills/fui-skill/design-md/nvidia/README.md +5 -0
  85. package/skills/fui-skill/design-md/ollama/DESIGN.md +539 -0
  86. package/skills/fui-skill/design-md/ollama/README.md +5 -0
  87. package/skills/fui-skill/design-md/opencode.ai/DESIGN.md +521 -0
  88. package/skills/fui-skill/design-md/opencode.ai/README.md +5 -0
  89. package/skills/fui-skill/design-md/pinterest/DESIGN.md +597 -0
  90. package/skills/fui-skill/design-md/pinterest/README.md +5 -0
  91. package/skills/fui-skill/design-md/playstation/DESIGN.md +661 -0
  92. package/skills/fui-skill/design-md/playstation/README.md +5 -0
  93. package/skills/fui-skill/design-md/posthog/DESIGN.md +690 -0
  94. package/skills/fui-skill/design-md/posthog/README.md +5 -0
  95. package/skills/fui-skill/design-md/raycast/DESIGN.md +669 -0
  96. package/skills/fui-skill/design-md/raycast/README.md +5 -0
  97. package/skills/fui-skill/design-md/renault/DESIGN.md +589 -0
  98. package/skills/fui-skill/design-md/renault/README.md +5 -0
  99. package/skills/fui-skill/design-md/replicate/DESIGN.md +616 -0
  100. package/skills/fui-skill/design-md/replicate/README.md +5 -0
  101. package/skills/fui-skill/design-md/resend/DESIGN.md +585 -0
  102. package/skills/fui-skill/design-md/resend/README.md +5 -0
  103. package/skills/fui-skill/design-md/revolut/DESIGN.md +636 -0
  104. package/skills/fui-skill/design-md/revolut/README.md +5 -0
  105. package/skills/fui-skill/design-md/runwayml/DESIGN.md +244 -0
  106. package/skills/fui-skill/design-md/runwayml/README.md +5 -0
  107. package/skills/fui-skill/design-md/sanity/DESIGN.md +357 -0
  108. package/skills/fui-skill/design-md/sanity/README.md +5 -0
  109. package/skills/fui-skill/design-md/sentry/DESIGN.md +551 -0
  110. package/skills/fui-skill/design-md/sentry/README.md +5 -0
  111. package/skills/fui-skill/design-md/shopify/DESIGN.md +516 -0
  112. package/skills/fui-skill/design-md/shopify/README.md +5 -0
  113. package/skills/fui-skill/design-md/slack/DESIGN.md +482 -0
  114. package/skills/fui-skill/design-md/spacex/DESIGN.md +363 -0
  115. package/skills/fui-skill/design-md/spacex/README.md +5 -0
  116. package/skills/fui-skill/design-md/spotify/DESIGN.md +246 -0
  117. package/skills/fui-skill/design-md/spotify/README.md +5 -0
  118. package/skills/fui-skill/design-md/starbucks/DESIGN.md +580 -0
  119. package/skills/fui-skill/design-md/starbucks/README.md +5 -0
  120. package/skills/fui-skill/design-md/stripe/DESIGN.md +487 -0
  121. package/skills/fui-skill/design-md/stripe/README.md +5 -0
  122. package/skills/fui-skill/design-md/supabase/DESIGN.md +462 -0
  123. package/skills/fui-skill/design-md/supabase/README.md +5 -0
  124. package/skills/fui-skill/design-md/superhuman/DESIGN.md +448 -0
  125. package/skills/fui-skill/design-md/superhuman/README.md +5 -0
  126. package/skills/fui-skill/design-md/tesla/DESIGN.md +286 -0
  127. package/skills/fui-skill/design-md/tesla/README.md +5 -0
  128. package/skills/fui-skill/design-md/theverge/DESIGN.md +339 -0
  129. package/skills/fui-skill/design-md/theverge/README.md +5 -0
  130. package/skills/fui-skill/design-md/together.ai/DESIGN.md +633 -0
  131. package/skills/fui-skill/design-md/together.ai/README.md +5 -0
  132. package/skills/fui-skill/design-md/uber/DESIGN.md +636 -0
  133. package/skills/fui-skill/design-md/uber/README.md +5 -0
  134. package/skills/fui-skill/design-md/vercel/DESIGN.md +736 -0
  135. package/skills/fui-skill/design-md/vercel/README.md +5 -0
  136. package/skills/fui-skill/design-md/vodafone/DESIGN.md +538 -0
  137. package/skills/fui-skill/design-md/vodafone/README.md +5 -0
  138. package/skills/fui-skill/design-md/voltagent/DESIGN.md +521 -0
  139. package/skills/fui-skill/design-md/voltagent/README.md +5 -0
  140. package/skills/fui-skill/design-md/warp/DESIGN.md +526 -0
  141. package/skills/fui-skill/design-md/warp/README.md +5 -0
  142. package/skills/fui-skill/design-md/webflow/DESIGN.md +588 -0
  143. package/skills/fui-skill/design-md/webflow/README.md +5 -0
  144. package/skills/fui-skill/design-md/wired/DESIGN.md +497 -0
  145. package/skills/fui-skill/design-md/wired/README.md +5 -0
  146. package/skills/fui-skill/design-md/wise/DESIGN.md +544 -0
  147. package/skills/fui-skill/design-md/wise/README.md +5 -0
  148. package/skills/fui-skill/design-md/x.ai/DESIGN.md +465 -0
  149. package/skills/fui-skill/design-md/x.ai/README.md +5 -0
  150. package/skills/fui-skill/design-md/zapier/DESIGN.md +537 -0
  151. package/skills/fui-skill/design-md/zapier/README.md +5 -0
  152. package/skills/fui-skill/examples/component.vue +162 -0
  153. package/skills/fui-skill/examples/f-table-patterns.json +331 -0
  154. package/skills/fui-skill/examples/module-patterns.json +973 -0
  155. package/skills/fui-skill/examples/project-patterns.json +222 -0
  156. package/skills/fui-skill/metadata.json +75 -0
  157. package/skills/fui-skill/references/INDEX.md +144 -0
  158. package/skills/fui-skill/references/advanced-techniques.md +160 -0
  159. package/skills/fui-skill/references/coding-standards.md +112 -0
  160. package/skills/fui-skill/references/component-design.md +455 -0
  161. package/skills/fui-skill/references/component-quickref.md +77 -0
  162. package/skills/fui-skill/references/component-table.md +276 -0
  163. package/skills/fui-skill/references/components-dialog.md +192 -0
  164. package/skills/fui-skill/references/components-display.md +147 -0
  165. package/skills/fui-skill/references/components-echart.md +391 -0
  166. package/skills/fui-skill/references/components-input.md +359 -0
  167. package/skills/fui-skill/references/controls-patterns.md +847 -0
  168. package/skills/fui-skill/references/controls-styling-vocabulary.md +140 -0
  169. package/skills/fui-skill/references/db-table-design.md +77 -0
  170. package/skills/fui-skill/references/db-workflow.md +504 -0
  171. package/skills/fui-skill/references/default-function.md +415 -0
  172. package/skills/fui-skill/references/design-modes.md +85 -0
  173. package/skills/fui-skill/references/echart-templates.md +481 -0
  174. package/skills/fui-skill/references/fastproject.md +97 -0
  175. package/skills/fui-skill/references/fsheet.md +218 -0
  176. package/skills/fui-skill/references/fullstack-workflow.md +351 -0
  177. package/skills/fui-skill/references/module-data-patterns.md +126 -0
  178. package/skills/fui-skill/references/module-json-anatomy.md +137 -0
  179. package/skills/fui-skill/references/module-structure.md +260 -0
  180. package/skills/fui-skill/references/new-session.md +108 -0
  181. package/skills/fui-skill/references/pdfmake.md +60 -0
  182. package/skills/fui-skill/references/permission-system.md +169 -0
  183. package/skills/fui-skill/references/platform-architecture.md +294 -0
  184. package/skills/fui-skill/references/project-config.md +335 -0
  185. package/skills/fui-skill/references/project-provisioning.md +383 -0
  186. package/skills/fui-skill/references/script-map.md +296 -0
  187. package/skills/fui-skill/references/sql-clr-functions.md +224 -0
  188. package/skills/fui-skill/references/system-design.md +116 -0
  189. package/skills/fui-skill/references/tapi-file-api.md +191 -0
  190. package/skills/fui-skill/references/tapi-permission-patterns.md +158 -0
  191. package/skills/fui-skill/references/tapi-reference.md +549 -0
  192. package/skills/fui-skill/references/tools-registry.md +460 -0
  193. package/skills/fui-skill/references/ui-crosswindow-patterns.md +317 -0
  194. package/skills/fui-skill/references/ui-dialog-patterns.md +229 -0
  195. package/skills/fui-skill/references/ui-layout-patterns.md +176 -0
  196. package/skills/fui-skill/references/ui-patterns.md +315 -0
  197. package/skills/fui-skill/references/ui-screenshot-review.md +94 -0
  198. package/skills/fui-skill/references/ui-table-cell-patterns.md +316 -0
  199. package/skills/fui-skill/references/ui-templates.md +29 -0
  200. package/skills/fui-skill/references/verification.md +246 -0
  201. package/skills/fui-skill/references/watcher-patterns.md +196 -0
  202. package/skills/fui-skill/references/websocket-realtime.md +254 -0
  203. package/skills/fui-skill/scripts/component-3.0.js +2549 -0
  204. package/skills/fui-skill/scripts/component.js +3142 -0
  205. package/skills/fui-skill/scripts/componentTable-3.0.js +909 -0
  206. package/skills/fui-skill/scripts/componentTable.js +769 -0
  207. package/skills/fui-skill/scripts/defaultfunction-3.0.js +781 -0
  208. package/skills/fui-skill/scripts/defaultfunction.js +966 -0
  209. package/skills/fui-skill/scripts/fastproject-3.0.js +870 -0
  210. package/skills/fui-skill/scripts/fastproject.js +828 -0
  211. package/skills/fui-skill/scripts/fechart.js +890 -0
  212. package/skills/fui-skill/scripts/fsheet.js +1330 -0
@@ -0,0 +1,847 @@
1
+ # Controls Patterns & Logic
2
+
3
+ > File này sở hữu: **value resolution (backtick / `{{ }}` / JS eval), action engine (API/IN/OUT/CALLBACK/CONFIRM/MESS/CALL/IF/EXE), key notation, grid wrapper `container > rows > cols`, control object, layout rules**. Bộ class/style được duyệt xem [controls-styling-vocabulary.md](controls-styling-vocabulary.md); tổng quan 4 thành phần module.json xem [module-json-anatomy.md](module-json-anatomy.md). *(Tier A — luôn nạp cùng `skill_get`.)*
4
+
5
+ Guide to defining logic and layout in `module.json`.
6
+
7
+ ---
8
+
9
+ ## Ưu tiên FUI components — kiểm tra trước khi dùng `v-*`
10
+
11
+ FUI đã xây dựng sẵn các `f-*` component với nhiều tính năng mặc định (sort, search, CRUD, inline action, form validation, ...) — dùng `f-*` sẽ ít code hơn và tận dụng được toàn bộ tích hợp runtime. Chỉ dùng `v-*` khi FUI chưa có equivalent phù hợp.
12
+
13
+ | Ưu tiên dùng | Thay vì | Lý do ưu tiên |
14
+ |---|---|---|
15
+ | `f-table` | `v-data-table` (CRUD lẫn readonly) | sort/search/inline action/CRUD tích hợp sẵn; readonly = không khai `:update-api`; tự build headers khi bỏ `:headers`; hàng tổng qua `sum-format` |
16
+ | `f-search` | `v-autocomplete` | API search, debounce, loading state có sẵn |
17
+ | `f-button` | `v-btn` (khi cần action FUI) | hỗ trợ CALL, hotkey, form validation |
18
+ | `f-date` / `f-time` | `v-text-field` + date parsing | picker + typed mask tích hợp sẵn |
19
+ | `f-menu` | `v-menu + v-btn` dropdown | cấu hình đơn giản hơn, tích hợp icon/action/link |
20
+ | `f-echart` | `<canvas>`, `v-chart`, lib khác | ECharts V5 — hỗ trợ mọi loại chart |
21
+
22
+ > Trước khi dùng `v-*`, kiểm tra [component-quickref.md](component-quickref.md) — nếu có `f-*` equivalent thì ưu tiên dùng `f-*`.
23
+
24
+ **Default attrs tự động** — FUI inject sẵn default props cho mỗi `f-*` component (xem `defaultControlAttr` trong `scripts/component.js`). Chỉ khai báo prop khi muốn **override** giá trị mặc định, không cần lặp lại default:
25
+
26
+ | Component | Default đã có sẵn (không cần khai báo lại) |
27
+ |---|---|
28
+ | `f-table` | `:dense true` · `:show-search true` · `:fixed-header true` · `:items-per-page 50` · `:hide-default-footer true` · `mobile-breakpoint 0` |
29
+ | `f-button` | `color primary` · `elevation 0` |
30
+ | `f-date` / `f-time` | `:dense true` · `:outlined true` · `:hide-details true` |
31
+ | `f-search` | `:dense true` · `:outlined true` · `:hide-details true` · `:auto-select-first true` |
32
+ | `f-menu` | `color primary` · `:depressed true` |
33
+
34
+ > **Cảnh báo CRUD:** Không dùng `f-button` + `f-dialog` để mở form thêm mới — không hoạt động ổn định trong FUI V2. Dùng `ctrl-update` của `f-table` (`:update-form` + `:update-api`) cho mọi CRUD inline. Khi cần dialog riêng (form có logic đặc biệt, dynamic controls, multi-step...) → dựng `v-dialog` thủ công theo anatomy chuẩn phong cách f-dialog (`design_read("fui")` §4, `ui-dialog-patterns.md`).
35
+
36
+ ---
37
+
38
+ ## 0. Value Resolution Rules (Fundamental)
39
+
40
+ FUI dùng hàm `getVueData(value, src)` để giải thích mọi string value trong `data[]` và `IN` mapping. Hiểu đúng quy tắc này tránh lỗi set data sai.
41
+
42
+ ### Quy tắc xác định kiểu value
43
+
44
+ | Dạng value | Cách xử lý | Kết quả |
45
+ |---|---|---|
46
+ | Primitive (number, boolean, object, array) | Lưu nguyên | Giá trị gốc |
47
+ | String bắt đầu bằng `` ` `` | Lodash template (literal + `{{ }}` interpolation) | String sau khi render |
48
+ | String có `{{ }}` | Lodash template với interpolation | String sau khi render |
49
+ | String có spaces (không có backtick, không có `{{ }}`) | Lodash template → trả về literal | String đó |
50
+ | String không có spaces, không backtick, không `{{ }}` | **Evaluated as JS expression** | Giá trị của expression |
51
+ | `""` (empty string) | Empty string | `""` |
52
+
53
+ ### Ví dụ thực tế
54
+
55
+ ```json
56
+ "data": [
57
+ {
58
+ "a": "Hang So", // has space → literal string "Hang So"
59
+ "b": 3, // number → 3
60
+ "c": true, // boolean → true
61
+ "d": null, // null → null
62
+ "e": "val1", // no space → JS eval: giá trị của biến val1
63
+ "f": "val4.prop2", // JS eval: vueData.val4.prop2
64
+ "g": "val5[1]", // JS eval: phần tử thứ 2 của array val5
65
+ "h": "[val1]", // JS eval: tạo array [giá_trị_val1]
66
+ "i": "[1,2,3].map(x=>x*2)", // no space → JS eval → [2,4,6]
67
+ "j": "`Xin chào {{user.UserName}}", // backtick → lodash template → "Xin chào Tuấn"
68
+ "k": "tổng là {{a+b}} item", // {{ }} → lodash template → "tổng là 3 item"
69
+ "l": "tuimuonghep{{val4.prop3}}chuoi" // {{ }} → "tuimuonghep10chuoi"
70
+ }
71
+ ]
72
+ ```
73
+
74
+ > **Quan trọng**: `"val1"` (no spaces) → FUI lấy GIÁ TRỊ của biến `val1`, KHÔNG phải string "val1". Muốn literal string "val1", phải dùng `` "`val1" ``.
75
+
76
+ > ⚠️ **Bảng trên KHÔNG áp cho `:attr`.** Chỉ `data[]` và `IN` mapping mới đi qua `getVueData()`. Giá trị của một binding (`:option`, `:config`, mọi `v-bind:`) đi **thẳng vào template compiler của Vue**, nên luật khác hẳn: chỉ nhận **một biểu thức thuần**, cấm `function(){}`/`var`/`return`/`;`. Vi phạm ⇒ trang trắng với 0 JS error, 0 `[Vue warn]`. Luật đầy đủ + cách chuyển logic sang `script.js` + `EXE`: [advanced-techniques.md](advanced-techniques.md) §1.
77
+
78
+ ### String interpolation với `{{ }}`
79
+
80
+ `{{ }}` dùng lodash template — có thể chứa bất kỳ JS expression nào:
81
+
82
+ ```json
83
+ "label": "Còn {{items.length}} bản ghi",
84
+ "title": "Ngày {{moment().format('DD/MM/YYYY')}}",
85
+ "flag": "Là array: {{Array.isArray(myList)}}"
86
+ ```
87
+
88
+ ---
89
+
90
+ ## 1. Khai báo và gán dữ liệu trong `data[]`
91
+
92
+ ### Key notation — Dot, Bracket, và Cross-window
93
+
94
+ Key trong `data[]` mapping không chỉ là tên biến đơn giản — FUI's `setValue` hỗ trợ đầy đủ path notation:
95
+
96
+ ```json
97
+ "data": [
98
+ {
99
+ "val": "Hang So", // biến đơn
100
+ "obj.prop": "val1", // dot notation → vueData.obj.prop
101
+ "obj.nested.deep": true, // multi-level dot
102
+ "arr[0].name": "Tuan", // bracket với literal index
103
+ "arr[val2].name": "abc", // bracket với biến index (val2 = 3 → arr[3])
104
+ "arr[0][val1].x": "y", // bracket với variable key (val1 = "Hang So")
105
+ "#PARENT.field": "val1", // ghi vào parent window
106
+ "#Win1.field": "val1" // ghi vào iframe Win1
107
+ }
108
+ ]
109
+ ```
110
+
111
+ FUI **tự tạo object/array trung gian** nếu chưa tồn tại — dùng khi cập nhật một field sâu mà không phải gán lại cả object. Cùng luật này áp cho đích của `OUT` và mọi fragment gán dữ liệu.
112
+
113
+ > Khi key là `#...`, FUI gọi `setWindowData` thay vì `setValue` — xem [ui-patterns.md](ui-patterns.md) §Cross-window.
114
+
115
+ ### Event và Slot trong `attr` — luôn viết đầy đủ, không viết tắt
116
+
117
+ Trong `module.json`, khi khai báo **event** hoặc **slot** cho bất kỳ component nào (kể cả `f-*` tùy biến như `f-contest-list`, không riêng `v-*`), luôn dùng dạng đầy đủ — không dùng ký hiệu viết tắt của Vue:
118
+
119
+ | Loại | Viết tắt (Vue hỗ trợ trong `.vue`) | Dùng trong `module.json` |
120
+ |---|---|---|
121
+ | Event | `@click`, `@saved`, `@bất-kỳ-event-nào` | `v-on:click`, `v-on:saved`, `v-on:bất-kỳ-event-nào` |
122
+ | Slot | `#default`, `#item.name` | `v-slot:default`, `v-slot:item.name` |
123
+
124
+ ```json
125
+ // ❌ SAI — viết tắt, sẽ lỗi không load được
126
+ { "el": "f-contest-list", "attr": { "@saved": "CALL(apiLoadContests)" } }
127
+
128
+ // ✅ ĐÚNG
129
+ { "el": "f-contest-list", "attr": { "v-on:saved": "CALL(apiLoadContests)" } }
130
+ ```
131
+
132
+ Quy tắc này áp dụng cho **mọi event/slot của mọi component**, kể cả custom event do component tự `$emit()` (như `saved` ở trên) — không chỉ riêng DOM event chuẩn như `click`.
133
+
134
+ > Không nhầm với `#PARENT.field` / `#Win1.field` — đó là cú pháp `#` riêng của FUI cho cross-window trong **`data[]`** (xem trên), khác hoàn toàn với slot shorthand `#` của Vue trong `attr`.
135
+
136
+ ### Gán giá trị liên kết (alias)
137
+
138
+ ```json
139
+ "GanLienGiaTri": "val1" // GanLienGiaTri = giá trị hiện tại của val1
140
+ ```
141
+
142
+ Không reactive — chỉ snapshot tại thời điểm `data[]` chạy. Muốn reactive → dùng `watch`.
143
+
144
+ ### Data initialization với JS expressions
145
+
146
+ ```json
147
+ "data": [
148
+ {
149
+ "today": "moment().format('YYYY-MM-DD')",
150
+ "ids": "[1,2,3,4].map(x=>x*2)",
151
+ "firstItem": "dsUser[0]",
152
+ "total": "dsItems.length"
153
+ }
154
+ ]
155
+ ```
156
+
157
+ ---
158
+
159
+ ## 2. Component Actions (`data`)
160
+
161
+ Actions are the "methods" of your module. They handle APIs, dialogs, and logic flow.
162
+ Khai báo action thành object có tên trong `data`, gọi lại bằng `CALL`.
163
+
164
+ ### QUY TẮC NỀN — một object `{}` là MỘT lệnh; nhiều lệnh có thứ tự là MỘT MẢNG `[]`
165
+
166
+ Đây là luật gốc của FUI, đọc trước mọi thứ khác trong mục này:
167
+
168
+ > **Một lệnh / một tập lệnh = một object JSON `{}`** — vì bên trong một tập lệnh còn có thể có nhiều **tham số** và **tập lệnh con** lồng đệ quy (`IN`, `OUT`, `CALLBACK`, `THEN`/`ELSE`…).
169
+ > **Nhiều lệnh chạy tuần tự = một MẢNG các object `[{…}, {…}, {…}]`** — mảng là cách DUY NHẤT diễn đạt "trước, rồi sau".
170
+
171
+ Nhét nhiều lệnh vào chung một `{}` **không phải là "viết gọn"** — nó làm rơi lệnh:
172
+
173
+ ```json
174
+ // ❌ SAI — chỉ CALL chạy. EXE và MESS KHÔNG BAO GIỜ chạy.
175
+ "CALLBACK": {
176
+ "EXE": "vueData.previewRows = []; vueData.excelRaw = [];",
177
+ "CALL": "getList",
178
+ "MESS": "Đã cập nhật dữ liệu tháng"
179
+ }
180
+ ```
181
+
182
+ ```json
183
+ // ✅ ĐÚNG — ba lệnh, ba object, chạy đúng thứ tự đã viết.
184
+ "CALLBACK": [
185
+ { "EXE": "vueData.previewRows = []; vueData.excelRaw = [];" },
186
+ { "CALL": "getList" },
187
+ { "MESS": "Đã cập nhật dữ liệu tháng" }
188
+ ]
189
+ ```
190
+
191
+ **Vì sao rơi** — `vueAction()` (`fastproject.js:407`, `fastproject-3.0.js:448`, giống hệt nhau ở V2 và V3) dò các key lệnh theo **thứ tự cố định**, và **mỗi nhánh `return` ngay**:
192
+
193
+ ```
194
+ IN (tham số, không return) → API ⏎ → CALL ⏎ → EXE ⏎ → FUN ⏎ → IF ⏎ → MESS ⏎ → MESSBOX ⏎ → (fallthrough) mapData
195
+ ```
196
+
197
+ Nên trong một object, **đúng một lệnh chạy** — lệnh có ưu tiên cao nhất, **không phải lệnh viết trước**. Ở ví dụ sai trên, `CALL` thắng `EXE` và `MESS` dù được viết ở giữa. Đảo thứ tự các dòng cũng không đổi được gì.
198
+
199
+ Ngược lại, `runAction()` nhận mảng thì `_.forEach` từng phần tử qua `vueAction` ⇒ **mảng = tuần tự, giữ đúng thứ tự viết**.
200
+
201
+ **Đây là lỗi hỏng-âm-thầm**: không JS error, không `[Vue warn]`, API vẫn chạy, chỉ là hai lệnh kia biến mất. Triệu chứng thường gặp: "sau khi lưu, danh sách có reload nhưng không thấy toast", hoặc "toast hiện mà form không reset".
202
+
203
+ > Mọi chỗ nhận action đều theo luật này: `CALLBACK`, `ERRORCALLBACK`, `THEN`/`ELSE`, `CANCEL`, `:action`, `:update-api.new/edit/delete`, `watch`, `onclose`, và named action trong `data[]`. **Nghi ngờ thì bọc mảng** — mảng một phần tử `[{…}]` luôn hợp lệ.
204
+
205
+ #### Key nào là LỆNH, key nào là THAM SỐ
206
+
207
+ Chỉ nhóm "lệnh" mới bị luật một-object-một-lệnh. Tham số thì đi kèm lệnh trong **cùng** object là đúng.
208
+
209
+ | Nhóm | Key | Ghi chú |
210
+ |---|---|---|
211
+ | **Lệnh** (mỗi object chỉ chạy 1) | `API` · `CALL` · `EXE` · `FUN` · `IF` · `MESS` · `MESSBOX` | theo đúng thứ tự ưu tiên ở trên |
212
+ | **Tham số** (đi chung, không tính là lệnh) | `IN` · `OUT` · `HEADER` · `METHOD` · `CALLBACK` · `ERROROUT` · `ERRORCALLBACK` · `THEN` · `ELSE` · `CONFIRM` · `CANCEL` | `CONFIRM`/`CANCEL` được `runAction` xử lý một tầng trên rồi mới gọi `vueAction` |
213
+
214
+ **Ngoại lệ duy nhất — `MESS` đi kèm `API`:** `callAPI` tự bắn toast khi thành công (`fastproject-3.0.js:755`, sau `OUT`, trước `CALLBACK`), nên ở đây `MESS` là **tham số của `API`**, không phải lệnh thứ hai. `{ "API": …, "IN": …, "MESS": …, "CALLBACK": … }` là đúng và rất thông dụng.
215
+ Lưu ý: chỉ `MESS`, **không** áp cho `MESSBOX`; và chỉ với `API`, **không** với `FUN` (`runFunction` không đọc `MESS`).
216
+
217
+ #### Đừng trộn key gán dữ liệu vào cùng object với lệnh
218
+
219
+ Fragment gán dữ liệu (`{ "dlgOpen": false, "form.Name": "" }`) chạy ở **nhánh cuối cùng** của `vueAction` — nhánh này chỉ tới lượt khi **không** key lệnh nào khớp. Có lệnh trong cùng object thì mọi key gán bị bỏ im lặng:
220
+
221
+ ```json
222
+ // ❌ SAI — CALL chạy, ba biến kia KHÔNG được gán
223
+ { "PageNum": 1, "Description": "", "IsPaper": false, "CALL": "reload" }
224
+
225
+ // ✅ ĐÚNG
226
+ [
227
+ { "PageNum": 1, "Description": "", "IsPaper": false },
228
+ { "CALL": "reload" }
229
+ ]
230
+ ```
231
+
232
+ ### Bảng key của Action Protocol
233
+
234
+ | Key | Mô tả |
235
+ |---|---|
236
+ | `API` | Endpoint cần gọi |
237
+ | `IN` | Tham số đầu vào (tham chiếu `vueData.` hoặc `item.`) |
238
+ | `OUT` | Lưu response vào biến (vd `"myList"`) — dạng object để tách nhiều SELECT |
239
+ | `CALLBACK` | Action chạy sau khi thành công |
240
+ | `ERROROUT` / `ERRORCALLBACK` | Bắt lỗi thủ công thay cho dialog lỗi mặc định |
241
+ | `CONFIRM` / `CANCEL` | Hỏi xác nhận trước khi chạy / action khi user bấm Hủy |
242
+ | `MESS` / `MESSBOX` | Toast / modal thông báo |
243
+ | `CALL` | Gọi một named action khác |
244
+ | `IF` / `THEN` / `ELSE` | Rẽ nhánh điều kiện |
245
+ | `FUN` | Gọi hàm FUI có sẵn (`openWindow`, `showMessage`, ...) |
246
+ | `ARRAY` + `COL` | Trích một cột từ mảng object thành mảng phẳng |
247
+ | `EXE` | JS thô — dùng hạn chế |
248
+
249
+ ```json
250
+ "fetchUsers": {
251
+ "API": "/api/users",
252
+ "IN": { "GroupID": "vueData.selectedGroup" },
253
+ "OUT": "userList",
254
+ "CALLBACK": { "MESS": "Loaded!" }
255
+ }
256
+ ```
257
+
258
+ Chi tiết từng key ở các mục bên dưới.
259
+
260
+ ### Standard API Call
261
+ ```json
262
+ "apiLoadData": {
263
+ "API": "/api/controller/action",
264
+ "IN": {
265
+ "Page": 1,
266
+ "Search": "vueData.searchText",
267
+ "ID": "item.ID"
268
+ },
269
+ "OUT": "items",
270
+ "CALLBACK": [
271
+ { "MESS": "Loaded!" },
272
+ { "CALL": "anotherAction" }
273
+ ]
274
+ }
275
+ ```
276
+
277
+ > Hai lệnh ⇒ **mảng hai object**. Gộp `MESS` và `CALL` vào một `{}` thì chỉ `CALL` chạy — xem §QUY TẮC NỀN ở trên.
278
+
279
+ > **Không gán nested object trực tiếp trong action:** `{ "form": { "field": "item.field" } }` sẽ không hoạt động — dùng dot notation cho từng field: `{ "form.field": "item.field" }`. Nếu đang dùng `ctrl-update`, không cần quản lý form thủ công.
280
+
281
+ ### API URL Interpolation
282
+
283
+ `{{ }}` trong URL API được interpolate từ vueData:
284
+
285
+ ```json
286
+ "getDetail": {
287
+ "API": "/ts/Student/{{StudentID}}",
288
+ "OUT": "studentDetail"
289
+ },
290
+ "getReport": {
291
+ "API": "/ts/Report/{{sYear}}/{{sMonth}}",
292
+ "OUT": "reportData"
293
+ }
294
+ ```
295
+
296
+ ### API Error Handling — ERROROUT / ERRORCALLBACK
297
+
298
+ Mặc định, API lỗi sẽ hiện dialog lỗi toàn màn hình. Dùng `ERROROUT` + `ERRORCALLBACK` để bắt lỗi và xử lý thủ công:
299
+
300
+ ```json
301
+ "saveData": {
302
+ "API": "/ts/Student_Insert",
303
+ "IN": { "Name": "sName" },
304
+ "ERROROUT": {
305
+ "errStatus": "status",
306
+ "errMessage": "responseJSON"
307
+ },
308
+ "ERRORCALLBACK": {
309
+ "MESS": "Lưu thất bại: {{errMessage.Message}}"
310
+ }
311
+ }
312
+ ```
313
+
314
+ - `ERROROUT`: map từ XHR error object — các key thông dụng: `status` (HTTP status code), `responseJSON` (parsed error body)
315
+ - `ERRORCALLBACK`: action chạy sau khi map ERROROUT (không hiện dialog mặc định)
316
+ - Khi có `ERROROUT`, lỗi sẽ KHÔNG tự hiện — bạn phải xử lý hoàn toàn
317
+
318
+ ### API Method và Header override
319
+
320
+ ```json
321
+ "getPublicData": {
322
+ "API": "/ts/PublicData",
323
+ "METHOD": "GET",
324
+ "HEADER": { "x-api-key": "`my-api-key" },
325
+ "OUT": "publicData"
326
+ }
327
+ ```
328
+
329
+ - Mặc định: `METHOD = "POST"`, `HEADER = { authorization: token }`
330
+ - Override khi cần GET hoặc custom header cho external API
331
+
332
+ ### Literal String Values (Important)
333
+
334
+ Apply this rule only for values passed inside action `IN` mapping.
335
+ If a string has no spaces, FUI can treat it like an expression/variable in `IN`.
336
+ When you need a plain literal string, force it with:
337
+ - Backtick prefix: ``"`myValue"`` (or closed ``"`myValue`"``)
338
+ - Single quotes inside JSON string: `"'myValue'"`
339
+
340
+ ```json
341
+ {
342
+ "FUN": "openWindow",
343
+ "IN": {
344
+ "id": "`winUser",
345
+ "url": "'/fp/module?mid=123'",
346
+ "title": "`Tiêu đề cố định"
347
+ }
348
+ }
349
+ ```
350
+
351
+ Đặc biệt cần ép literal cho các key như `id`, `url`, hoặc bất kỳ key nào trong `IN` phải giữ nguyên dạng text.
352
+
353
+ For component props in `attr` without `:`, value is already a plain string prop and usually does not need wrapping — vd `"url": "/fp/module?mid=123"` trong `attr` đã là string literal, không cần backtick.
354
+
355
+ ### Confirmation + CANCEL
356
+
357
+ `CONFIRM` hiện dialog xác nhận trước khi thực thi. `CANCEL` (optional) chạy khi user bấm Hủy:
358
+
359
+ ```json
360
+ "deleteItem": {
361
+ "CONFIRM": "Bạn có chắc muốn xóa {{dongChon.Name}}?",
362
+ "API": "/api/delete",
363
+ "IN": { "id": "dongChon.ID" },
364
+ "CALLBACK": { "CALL": "reloadData" },
365
+ "CANCEL": { "MESS": "Đã hủy thao tác" }
366
+ }
367
+ ```
368
+
369
+ > `CONFIRM` string hỗ trợ `{{ }}` interpolation từ vueData.
370
+
371
+ ### Conditional Logic
372
+ ```json
373
+ "checkStatus": {
374
+ "IF": "status === 1",
375
+ "THEN": { "MESS": "Active" },
376
+ "ELSE": {
377
+ "IF": "status === 2",
378
+ "THEN": { "MESS": "Pending" },
379
+ "ELSE": { "MESS": "Inactive" }
380
+ }
381
+ }
382
+ ```
383
+
384
+ > Condition: JS expression, truy cập vueData trực tiếp (không cần `vueData.` prefix).
385
+ > THEN/ELSE có thể là string JS: `"THEN": "vueData.x = 1"`.
386
+
387
+ ### MESS vs MESSBOX
388
+
389
+ | | MESS | MESSBOX |
390
+ |---|---|---|
391
+ | Hiển thị | Toast (góc trên, tự đóng) | Modal dialog (user phải đóng) |
392
+ | Dùng khi | Thông báo thành công/thất bại | Cần user đọc và xác nhận đã thấy |
393
+
394
+ ```json
395
+ { "MESS": "Lưu thành công!" }
396
+ { "MESSBOX": "Phiên của bạn đã hết hạn. Vui lòng đăng nhập lại." }
397
+ ```
398
+
399
+ Cả hai hỗ trợ `{{ }}` interpolation.
400
+
401
+ ### FUN: showMessage — Dialog thông báo có onclose
402
+
403
+ ```json
404
+ {
405
+ "FUN": "showMessage",
406
+ "IN": {
407
+ "title": "`Xác nhận thành công",
408
+ "message": "`Dữ liệu đã được lưu. Tiếp tục?",
409
+ "onclose": {
410
+ "CALL": "reloadData"
411
+ }
412
+ }
413
+ }
414
+ ```
415
+
416
+ - `title`: tiêu đề dialog
417
+ - `message`: nội dung chi tiết (optional)
418
+ - `onclose`: action chạy sau khi user đóng dialog (thay thế `MESS` khi cần làm gì sau khi đọc xong)
419
+
420
+ ### CALL — Gọi action đã khai báo
421
+
422
+ Action được lưu thành biến trong `data[]` → gọi lại bất cứ đâu bằng `CALL`:
423
+
424
+ ```json
425
+ "data": [
426
+ { "apiGetLogs": { "API": "/nema/Wifi_Log_today", "OUT": "dataTableLogs" } }
427
+ ]
428
+ ```
429
+
430
+ ```json
431
+ { "CALL": "apiGetLogs" }
432
+ ```
433
+
434
+ `CALL` cũng dùng trong CALLBACK, IF/THEN/ELSE, hoặc watch để gọi action đã định nghĩa.
435
+
436
+ ---
437
+
438
+ ### ARRAY — Trích xuất cột từ mảng cấu trúc
439
+
440
+ Tạo mảng phẳng gồm tất cả giá trị của một cột từ array có cấu trúc:
441
+
442
+ ```json
443
+ {
444
+ "danhSachID": {
445
+ "ARRAY": "dsHoaDon",
446
+ "COL": "DanhMucThuID"
447
+ }
448
+ }
449
+ // danhSachID = [32423, 4, 23, 4, 234, 2]
450
+ ```
451
+
452
+ ---
453
+
454
+ ### OUT — Gán kết quả API vào nhiều biến
455
+
456
+ Thay vì gán toàn bộ response vào một biến, dùng `OUT` dạng object để map từng phần:
457
+
458
+ ```json
459
+ "getReport": {
460
+ "API": "/ts/TS_Report",
461
+ "OUT": {
462
+ "tongSoLuong": "data[0][0].Total",
463
+ "danhSachChiTiet": "data[1]",
464
+ "thongKe": "data[2]"
465
+ }
466
+ }
467
+ ```
468
+
469
+ Dùng khi SP trả về nhiều SELECT (`data[0]`, `data[1]`...) — gán mỗi phần vào biến riêng thay vì dùng CALLBACK + EXE.
470
+
471
+ ---
472
+
473
+ ### MAP — Mapping dữ liệu trong bộ nhớ (không qua API)
474
+
475
+ Khi chỉ cần re-map dữ liệu giữa các biến mà không gọi API, dùng block chỉ có `IN` và `OUT`:
476
+
477
+ ```json
478
+ {
479
+ "IN": {
480
+ "HoTen": "formData.FullName",
481
+ "MaSV": "formData.StudentID"
482
+ },
483
+ "OUT": "mappedData"
484
+ }
485
+ ```
486
+
487
+ ```json
488
+ {
489
+ "IN": { "HoTen": "formData.FullName" },
490
+ "OUT": { "displayName": "HoTen" }
491
+ }
492
+ ```
493
+
494
+ ---
495
+
496
+ ### EXE — Raw JavaScript
497
+
498
+ ```json
499
+ { "EXE": "vueData.items = vueData.items.filter(x => x.active); " }
500
+ ```
501
+
502
+ > Dùng khi cần logic phức tạp không thể diễn tả bằng action JSON. Có thể access `vueData` trực tiếp. Kết thúc bằng `;`.
503
+
504
+ ---
505
+
506
+ ## 3. Watchers (`watch`)
507
+
508
+ Trigger actions automatically when data changes.
509
+
510
+ For production-grade cascading and filter watcher design, see [watcher-patterns.md](watcher-patterns.md).
511
+
512
+ ```json
513
+ "watch": {
514
+ "searchText": { "CALL": "apiLoadData" },
515
+ "selectedGroup": { "CALL": "apiLoadUsers" }
516
+ }
517
+ ```
518
+
519
+ ### v_old — Giá trị trước khi thay đổi
520
+
521
+ Trong CALLBACK hoặc action của watch, `v_old` chứa giá trị CŨ (trước khi thay đổi):
522
+
523
+ ```json
524
+ "watch": {
525
+ "taoID": {
526
+ "backupID": "v_old.taoID",
527
+ "BAC": 12345
528
+ }
529
+ }
530
+ ```
531
+
532
+ ### deep-watch — Watch nested object/array
533
+
534
+ Mặc định watch là shallow (chỉ detect thay đổi reference). Dùng `deep-watch` để detect thay đổi bên trong:
535
+
536
+ ```json
537
+ "watch": {
538
+ "deep-watch": {
539
+ "formData": { "CALL": "autoSave" },
540
+ "selectedItems": { "CALL": "recalcTotal" }
541
+ },
542
+ "sModuleID": { "CALL": "reload" }
543
+ }
544
+ ```
545
+
546
+ > `deep-watch` được xử lý trước các watch thông thường. Tốn hiệu năng hơn — chỉ dùng khi cần thiết.
547
+
548
+ ---
549
+
550
+ ## 4. Table Cell Action Context
551
+
552
+ Khi action chạy từ cell trong `f-table` (t-button, t-check, t-select, t-menu, v-on:click), FUI inject thêm:
553
+
554
+ | Variable | Giá trị |
555
+ |---|---|
556
+ | `item` | Object của row hiện tại |
557
+ | `index` | Index của row trong array |
558
+ | `$row` | Có thể set thủ công: `{ "$row": "item" }` |
559
+
560
+ ```json
561
+ {
562
+ "el": "t-button",
563
+ "text": "Tên",
564
+ "value": "ten",
565
+ "attr": {
566
+ ":label": "`${item.ten}-${vueData.val1}`",
567
+ ":action": [
568
+ { "$row": "item" },
569
+ {
570
+ "API": "/api/getDetail",
571
+ "OUT": "DETAIL",
572
+ "CALLBACK": {
573
+ "danhSach[index].kohienthi": true
574
+ }
575
+ },
576
+ { "MESS": "Row {{danhSach[index].ten}}" }
577
+ ]
578
+ }
579
+ }
580
+ ```
581
+
582
+ > `danhSach[index].kohienthi` — update trực tiếp item trong array bằng `index` inject từ context. Đây là pattern chuẩn để update 1 item cụ thể trong table.
583
+
584
+ ---
585
+
586
+ ## 5. URL Parameters → vueData
587
+
588
+ URL query params tự động được inject vào `vueData` khi module load:
589
+
590
+ ```
591
+ URL: https://fp.example.vn/ts/newhocsinh?id=12345&mod=abcdef
592
+ → vueData.p_router = "/ts/newhocsinh"
593
+ → vueData.p_routers = ["ts", "newhocsinh"]
594
+ → vueData.id = "12345"
595
+ → vueData.mod = "abcdef"
596
+ ```
597
+
598
+ | Biến | Kiểu | Mô tả |
599
+ |---|---|---|
600
+ | `p_router` | string | Path sau domain, không có query string |
601
+ | `p_routers` | array | Mảng các segment của path |
602
+ | (query params) | string | Mỗi param trong query string → biến cùng tên |
603
+
604
+ Dùng trong `data[]` để đọc URL params:
605
+ ```json
606
+ "data": [
607
+ {
608
+ "IF": "typeof menu != 'undefined' && menu=='0'",
609
+ "THEN": { "v_Set.menu": false }
610
+ }
611
+ ]
612
+ ```
613
+
614
+ ---
615
+
616
+ ## 6. Controls Layout (`controls`)
617
+
618
+ > **Vuetify version (V2 framework):** FUI V2 dùng **Vuetify 1.5** — grid system dùng `v-container / v-layout / v-flex`. FUI V3 (fastproject-3.0.js) dùng **Vuetify 3** với grid `v-container / v-row / v-col`. Phần lớn module.json trong skill này viết theo cú pháp V2 (đại đa số project đang chạy); project mới dùng V3 thì tra bảng ngưỡng `w` V2/V3 bên dưới (mục `w dạng pixel`). Tham chiếu V2: https://v15.vuetifyjs.com/en/framework/grid/ — **đang tạo project mới và phải chọn V2 hay V3?** Mặc định là V3, xem [project-provisioning.md](project-provisioning.md) §Bước 1.
619
+
620
+ **MANDATORY**: All UI elements MUST be nested within a Grid System structure: **Container** > **Row** > **Col** > **Element**. Do NOT place elements directly at the root.
621
+
622
+ ### Default prop cho Container và Row
623
+
624
+ | Phần | Default | Ghi chú |
625
+ |---|---|---|
626
+ | Container `prop` | `"fluid grid-list-md"` | Dùng mặc định này trước — chỉ thêm class khác khi có lý do cụ thể |
627
+ | Row `prop` | `"row wrap"` hoặc `""` | Dùng mặc định này trước; hàng có nhiều loại control/chiều cao khác nhau dùng `"row wrap align-center"` |
628
+
629
+ > Spacing và alignment ưu tiên đặt trong `col` hoặc `el attr` hơn là trong container/row prop.
630
+
631
+ ### Canh giữa control theo chiều dọc trong cùng một hàng
632
+
633
+ Trước khi chốt một row, phải kiểm tra các control trong row có cùng trục giữa theo chiều dọc hay
634
+ không — đặc biệt khi trộn input/select, button, icon, checkbox, label hoặc control có chiều cao khác
635
+ nhau.
636
+
637
+ - Mặc định cho hàng hỗn hợp: thêm `align-center` vào `prop` của row, ví dụ
638
+ `"prop": "row wrap align-center"`.
639
+ - Nếu chỉ một control lệch trục và không nên thay alignment của cả hàng, đặt
640
+ `"col": { "class": "align-self-center" }` cho cột đó.
641
+ - Không dùng `mt-*`, `mb-*` hoặc inline `style` để đẩy từng control lên/xuống nhằm giả lập canh giữa.
642
+ - Sau khi dựng layout, tự rà lại row ở desktop và mobile; control xuống dòng phải vẫn có khoảng cách
643
+ tự nhiên, không bị lệch vì margin thủ công.
644
+
645
+ ```json
646
+ {
647
+ "prop": "row wrap align-center",
648
+ "cols": [
649
+ { "el": "v-text-field", "w": 6, "attr": { "label": "Từ khóa", "v-model": "vueData.keyword" } },
650
+ { "el": "f-button", "w": "", "col": { "class": "shrink" },
651
+ "attr": { "label": "Tìm", "icon-text": "mdi-magnify" } }
652
+ ]
653
+ }
654
+ ```
655
+
656
+ ### Ngoại lệ — Toàn bộ trang là một custom component (`fluid pa-0`)
657
+
658
+ Khi cả trang chỉ render **một component tự xây dựng duy nhất** (component tự lo toàn bộ layout/padding bên trong), đặt container `prop` thành `"fluid pa-0"` thay vì default `"fluid grid-list-md"` — tránh bị cộng thêm padding mặc định của grid bên ngoài component.
659
+
660
+ ```json
661
+ [
662
+ {
663
+ "prop": "fluid pa-0",
664
+ "rows": [
665
+ {
666
+ "prop": "",
667
+ "cols": [
668
+ {
669
+ "el": "uc-main-ui",
670
+ "w": "12",
671
+ "attr": {}
672
+ }
673
+ ]
674
+ }
675
+ ]
676
+ }
677
+ ]
678
+ ```
679
+
680
+ > Chỉ áp dụng khi component con là **toàn bộ nội dung trang** (dashboard tự thiết kế, custom app nhúng qua `uc-*`). Nếu trang còn control khác ngoài component (filter, header, nhiều section) → vẫn dùng default `"fluid grid-list-md"` để giữ spacing chuẩn giữa các phần.
681
+
682
+ ### Basic Grid Structure
683
+ ```json
684
+ {
685
+ "prop": "fluid grid-list-md",
686
+ "rows": [
687
+ {
688
+ "prop": "row wrap",
689
+ "cols": [
690
+ { "w": "6", "el": "v-text-field", "attr": { "label": "Tên", "v-model": "tenBien" } },
691
+ { "w": "6", "el": "v-btn", "innerHTML": "Submit" }
692
+ ]
693
+ }
694
+ ]
695
+ }
696
+ ```
697
+
698
+ ### Cấu trúc đầy đủ của một element trong `cols`
699
+
700
+ ```json
701
+ {
702
+ "w": "6",
703
+ "el": "v-text-field",
704
+ "col": { "class": "py-0" },
705
+ "attr": { "label": "Tên", "v-model": "tenBien" },
706
+ "innerHTML": "..."
707
+ }
708
+ ```
709
+
710
+ | Field | Truyền vào | Mô tả |
711
+ |---|---|---|
712
+ | `w` | `v-flex` | Shorthand width — `1`–`12` (grid) hoặc `>= 25` (pixel, **bắt buộc kèm `col: { "class": "shrink" }`** — xem mục dưới). **BẮT BUỘC với mọi item trong `cols`** |
713
+ | `col` | `v-flex` | Các thuộc tính khác của v-flex: class, style, offset, align-self... |
714
+ | `el` | tag component | Tag HTML (`div`, `span`), component Vuetify hoặc FUI (`v-text-field`, `f-button`, `uc-*`...) |
715
+ | `attr` | element | Props/attrs/directive của chính component: `label`, `v-model`, `:disabled`, `v-on:click`. **KHÔNG dùng `@`** |
716
+ | `innerHTML` | slot default | HTML string hoặc **mảng đệ quy** các Control Object lồng bên trong |
717
+
718
+ ```json
719
+ {
720
+ "el": "v-btn",
721
+ "w": 6,
722
+ "col": { "class": "text-center" },
723
+ "attr": {
724
+ ":disabled": "true",
725
+ "color": "primary",
726
+ "v-on:click": "CALL(vueData.submit)"
727
+ },
728
+ "innerHTML": "Submit"
729
+ }
730
+ ```
731
+
732
+ ### `w` dạng pixel — BẮT BUỘC kèm `col: { "class": "shrink" }`
733
+
734
+ `w` có **hai chế độ**, runtime chọn theo ngưỡng chứ không theo ý định của người viết:
735
+
736
+ - `w` nhỏ → **số cột grid**, sinh class `sm{w}` (V2) / `v-col-{w}` (V3)
737
+ - `w` lớn → **bề rộng cố định px**, sinh inline `flex-basis: {w}px`
738
+
739
+ Nhưng cột grid mặc định là `flex-grow: 1` (`.flex` ở V2, `.v-col` ở V3), mà **`flex-grow` thắng `flex-basis`**: cột vẫn giãn ra chiếm hết chỗ trống, con số px **không có tác dụng nào**. Không lỗi JS, không `[Vue warn]` — chỉ là bề rộng khác điều mình khai.
740
+
741
+ ```json
742
+ // ❌ SAI — 300 hoàn toàn vô tác dụng, cột vẫn giãn full
743
+ { "el": "v-text-field", "w": 300, "attr": { "v-model": "tuKhoa" } }
744
+
745
+ // ✅ ĐÚNG — shrink tắt flex-grow, flex-basis 300px mới ăn
746
+ { "el": "v-text-field", "w": 300, "col": { "class": "shrink" },
747
+ "attr": { "v-model": "tuKhoa" } }
748
+ ```
749
+
750
+ Số đo thật (chromium, framework V2 + V3 thật, viewport 1200px, cột kế bên chiếm phần còn lại):
751
+
752
+ | Khai | V2 | V3 |
753
+ |---|---|---|
754
+ | `w: 300` không có gì thêm | 687px ❌ | 738px ❌ |
755
+ | `w: 300` + `col: { "shrink": true }` | 300px ✅ | 738px ❌ |
756
+ | `w: 300` + `col: { "class": "shrink" }` | 300px ✅ | 300px ✅ |
757
+
758
+ **Chỉ `col: { "class": "shrink" }` đúng ở cả hai framework.** Dạng thuộc tính trần `col: { "shrink": true }` là prop của `v-flex` (FUI V2 = Vuetify 1.5, xem §Vuetify version ở trên); Vuetify 3 không có prop đó nên nó rơi xuống thành attribute DOM trơ. Class `shrink` thì ở V3 do chính `projectdefaultstyle.css` của FUI định nghĩa (`flex-grow:0; flex-shrink:1; flex-basis:content`), nên có ở cả hai. `col: { "class": "flex-grow-0" }` cũng cho 300px ở cả hai — dùng `shrink` cho thống nhất với phần còn lại của skill.
759
+
760
+ #### Ngưỡng khác nhau giữa V2 và V3 → tránh hẳn `w` 13–24
761
+
762
+ | | Ngưỡng chuyển sang px | `w` = 20 cho ra |
763
+ |---|---|---|
764
+ | V2 (`fastproject.js`) | `w > 12` | `flex-basis: 20px` — px, hoạt động |
765
+ | V3 (`fastproject-3.0.js`) | `w > 24` | class `v-col-20` — **Vuetify 3 chỉ định nghĩa `.v-col-1` … `.v-col-12`** |
766
+
767
+ Ở V3, `w` từ 13 đến 24 sinh ra một class **không tồn tại**: cột không có bề rộng nào cả, rơi về chia đều (đo được: `w: 20` → `flex-basis: 0px`, rộng 600px). Đây là vùng chết — cùng một `w` cho hai kết quả trái ngược trên hai framework.
768
+
769
+ > **Luật:** `w` chỉ dùng `1`–`12` (chia cột) hoặc **`>= 25`** (pixel, kèm `col: { "class": "shrink" }`). Không bao giờ dùng 13–24.
770
+
771
+ `module_validate` kiểm cả hai vế này (warning, quét cả `cols` lồng trong `innerHTML`) — chạy trước khi publish thì không phải phát hiện qua ảnh render.
772
+
773
+ ### Hàng nhiều control (toolbar) — CHUẨN MỰC: mọi cột `shrink`, trừ `v-spacer`
774
+
775
+ Đây là hàng phổ biến nhất trong FUI: vài filter + nút thao tác bên trái, một nhóm nút đẩy sang phải. **Khuôn chuẩn — chép nguyên rồi thay control:**
776
+
777
+ ```json
778
+ {
779
+ "prop": "row",
780
+ "cols": [
781
+ { "el": "v-select", "w": "150", "col": "shrink", "attr": {} },
782
+ { "el": "v-select", "w": "180", "col": "shrink", "attr": {} },
783
+ { "el": "f-button", "w": "", "col": "shrink", "attr": {} },
784
+ { "el": "v-spacer", "w": "" },
785
+ { "el": "f-excel-reader", "col": "shrink", "attr": {} },
786
+ { "el": "f-button", "w": "", "col": "shrink", "attr": {} }
787
+ ]
788
+ }
789
+ ```
790
+
791
+ > **Luật:** trong một hàng chứa nhiều control điều khiển, **MỌI cột đều `"col": "shrink"`** — kể cả cột `w` rỗng. Ngoại lệ **duy nhất** là `v-spacer`: nó phải giãn được thì mới đẩy được nhóm sau sang phải, cho nó `shrink` là vô hiệu hoá chính nó.
792
+
793
+ **`w` rỗng KHÔNG có nghĩa là "tự co theo nội dung".** `fastproject*.js` bọc toàn bộ phần xử lý bề rộng trong `if (c.w)`, nên `w: ""` sinh ra **không class, cũng không `flex-basis`** — cột còn trơ là `.v-col` / `.flex` với `flex-grow: 1`, tức **chia đều phần trống** y như mọi cột khác. Nút bị kéo dãn ra, các nút cách nhau những khoảng trống bằng nhau, và nhóm cuối hàng không sát mép phải.
794
+
795
+ Số đo thật (chromium, framework V2 + V3 thật, viewport 1200px; hàng gồm 2 select `w:150`/`w:180` + nút "Tìm" | `v-spacer` | 2 nút cuối):
796
+
797
+ | | select `w:150` | nút "Tìm" | `v-spacer` | Kết quả nhìn thấy |
798
+ |---|---|---|---|---|
799
+ | Không cột nào `shrink` | 271 / 240 ❌ | 145 / 154 | 145 / 90 | filter phình, mọi thứ dàn đều |
800
+ | **Chỉ `v-spacer`, các cột khác không `shrink`** | 150 / 150 | **218 / 198** ❌ | 218 / 134 | **nút phình, hở khoảng giữa các nút, nhóm cuối không sát phải** |
801
+ | `shrink` cả `v-spacer` | 150 / 150 | 88 / 64 | **24 / 0** ❌ | cả hàng dồn về trái, mất hẳn canh phải (Σ 780/662 trên 1200px) |
802
+ | **`shrink` mọi cột trừ `v-spacer`** | 150 / 150 ✅ | 88 / 64 ✅ | **445 / 537** ✅ | filter đúng px, nút vừa chữ, nhóm cuối sát mép phải |
803
+
804
+ *(số bên trái = V3, bên phải = V2)*
805
+
806
+ Hàng thứ hai chính là lỗi hay gặp nhất: nghe **"cho hai nút này canh phải"** thì chỉ chèn `v-spacer` mà quên `shrink`. `v-spacer` lúc đó cũng chỉ là một cột `flex-grow: 1` ngang hàng với các nút — nó không "đẩy" được gì cả, chỉ nhận đúng một phần bằng nhau. Muốn hai nút **sát nhau và sát mép phải** thì hai nút đó **bắt buộc** phải hết giãn, nghĩa là phải có `shrink`.
807
+
808
+ Ghi nhớ theo hệ quả chứ không theo quy tắc rời: **cột nào không `shrink` là cột được chia phần trống.** Trong một hàng toolbar, thứ duy nhất đáng được chia phần trống là khoảng trắng — tức `v-spacer`.
809
+
810
+ Vài chi tiết đi kèm:
811
+
812
+ - `col` viết được **chuỗi ngắn** (`"col": "shrink"`) hoặc **object** (`"col": { "class": "shrink" }`) — tương đương nhau; chuỗi ngắn gọn hơn cho toolbar. Cần thêm thứ khác (`v-if`, `style`…) thì dùng object và giữ `class: "shrink"` bên trong.
813
+ - **Không bao giờ là `"col": { "shrink": true }`** — dạng thuộc tính trần chỉ ăn ở V2 (xem mục trên).
814
+ - Cột **không** có `v-spacer` mà muốn canh phải cả hàng: đặt `prop` của row là `"row justify-end"` thay vì chèn spacer.
815
+ - Cần nhiều hơn một nhóm (trái | giữa | phải): chèn **hai** `v-spacer`, mỗi cái vẫn không `shrink`.
816
+ - Hàng **form nhập liệu** thì ngược lại — ở đó các field *nên* chia cột (`w` 1–12, không `shrink`) để co giãn theo màn hình. `shrink` là chuẩn cho **hàng điều khiển**, không phải cho mọi hàng.
817
+
818
+ ### `innerHTML` Supports `{{ }}`
819
+
820
+ `innerHTML` dạng string hỗ trợ Vue template interpolation để bind text reactive:
821
+
822
+ ```json
823
+ { "el": "div", "w": "12", "innerHTML": "Xin chào {{formData.fullName}}" }
824
+ ```
825
+
826
+ ### Layout Rules — 4 quy tắc cứng
827
+
828
+ - **Wrapper**: LUÔN bắt đầu bằng Grid Wrapper (`container > rows > cols`), không đặt element ở gốc.
829
+ - **Không có key `children`**: lồng control bằng `innerHTML` dạng mảng.
830
+ - **Attributes**: `attr` cho thuộc tính của element; `col` cho thuộc tính của cột grid.
831
+ - **Events**: dùng `v-on:click` (JSON hợp lệ), không dùng `@click`.
832
+
833
+ ### Responsive Forms
834
+ ```json
835
+ "attr": {
836
+ ":width": "$vuetify.breakpoint.mdAndUp ? '500px' : '100%'"
837
+ }
838
+ ```
839
+
840
+ ---
841
+
842
+ ## 7. Common Element Patterns
843
+
844
+ - **HTML Content**: Use `innerHTML` for text or simple HTML.
845
+ - **Events**: `v-on:click`, `v-on:change`. wrap logic in `CALL()`.
846
+ - **Visibility**: Use `v-if` (pre-render) or `v-show` (CSS toggle).
847
+ - **Loops**: `v-for` is rarely used directly in `module.json`. Use `f-table` for lists or recursive partials.