@g1cloud/bluesea 5.0.0-beta.8 → 6.0.0-alpha.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 (243) hide show
  1. package/README.md +11 -0
  2. package/bin/install-claude-skill.mjs +74 -0
  3. package/css/bluesea.css +12 -30478
  4. package/dist/index.d.ts +10 -130
  5. package/dist/index.js +13 -0
  6. package/package.json +41 -59
  7. package/skills/bluesea-ui/SKILL.md +345 -0
  8. package/skills/bluesea-ui/references/components.md +196 -0
  9. package/skills/bluesea-ui/references/grid.md +159 -0
  10. package/skills/bluesea-ui/references/i18n.md +149 -0
  11. package/skills/bluesea-ui/references/validation.md +178 -0
  12. package/css/frame-default.css +0 -703
  13. package/dist/BSAlertModal-C8PIj5h_.js +0 -52
  14. package/dist/BSGridColumnSettingModal-D9CI-F_e.js +0 -149
  15. package/dist/BSRichTextMaximizedModal-DsSYqgSR.js +0 -62
  16. package/dist/BSYesNoModal-CB_RIKt0.js +0 -57
  17. package/dist/BSYoutubeInputModal-CwLcWtEI.js +0 -76
  18. package/dist/ImageInsertModal-lWQeIMu7.js +0 -106
  19. package/dist/ImageProperties.vue_vue_type_script_setup_true_lang-DDj0Odaa.js +0 -180
  20. package/dist/ImagePropertiesModal-C6RyT5B5.js +0 -98
  21. package/dist/LinkPropertiesModal-D5A3Gq2i.js +0 -119
  22. package/dist/TableInsertModal-B1F5T8qs.js +0 -74
  23. package/dist/TablePropertiesModal-DUiHSjcT.js +0 -353
  24. package/dist/VideoInsertModal-R0Bs5E2T.js +0 -106
  25. package/dist/VideoProperties.vue_vue_type_script_setup_true_lang-BCTNqg94.js +0 -194
  26. package/dist/VideoPropertiesModal-DQADTS-J.js +0 -98
  27. package/dist/YoutubeInsertModal-R97l6ge3.js +0 -84
  28. package/dist/YoutubeProperties.vue_vue_type_script_setup_true_lang-Ce9a5UDA.js +0 -181
  29. package/dist/YoutubePropertiesModal-BbrU7l5m.js +0 -102
  30. package/dist/bluesea.css +0 -11278
  31. package/dist/bluesea.js +0 -223
  32. package/dist/bluesea.umd.cjs +0 -72221
  33. package/dist/component/basic/BSButton.vue.d.ts +0 -50
  34. package/dist/component/basic/BSCalendar.vue.d.ts +0 -68
  35. package/dist/component/basic/BSCalendarRange.vue.d.ts +0 -57
  36. package/dist/component/basic/BSConsole.vue.d.ts +0 -36
  37. package/dist/component/basic/BSDate.vue.d.ts +0 -19
  38. package/dist/component/basic/BSFormLabel.vue.d.ts +0 -19
  39. package/dist/component/basic/BSImage.vue.d.ts +0 -44
  40. package/dist/component/basic/BSLink.vue.d.ts +0 -21
  41. package/dist/component/basic/BSLoadingIcon.vue.d.ts +0 -2
  42. package/dist/component/basic/BSPageNavigation.vue.d.ts +0 -52
  43. package/dist/component/basic/BSPopup.vue.d.ts +0 -70
  44. package/dist/component/basic/BSPopupButton.vue.d.ts +0 -41
  45. package/dist/component/basic/BSProgressBar.vue.d.ts +0 -33
  46. package/dist/component/basic/BSTooltip.vue.d.ts +0 -35
  47. package/dist/component/global/AddressSearchButton.vue.d.ts +0 -36
  48. package/dist/component/global/AddressSearchList.gen.d.ts +0 -4
  49. package/dist/component/global/BSAddressInput.vue.d.ts +0 -72
  50. package/dist/component/global/BSLocaleSelect.vue.d.ts +0 -35
  51. package/dist/component/global/BSNameInput.vue.d.ts +0 -80
  52. package/dist/component/global/BSTelInput.vue.d.ts +0 -88
  53. package/dist/component/grid/BSDateRangeFilter.vue.d.ts +0 -63
  54. package/dist/component/grid/BSDateRangeFilters.vue.d.ts +0 -56
  55. package/dist/component/grid/BSGrid.vue.d.ts +0 -177
  56. package/dist/component/grid/BSGridCell.vue.d.ts +0 -85
  57. package/dist/component/grid/BSGridCellCheckbox.vue.d.ts +0 -21
  58. package/dist/component/grid/BSGridCellDragHandle.vue.d.ts +0 -15
  59. package/dist/component/grid/BSGridCellEdit.vue.d.ts +0 -21
  60. package/dist/component/grid/BSGridCellSerialNo.vue.d.ts +0 -15
  61. package/dist/component/grid/BSGridColumnSettingModal.vue.d.ts +0 -25
  62. package/dist/component/grid/BSGridControl.vue.d.ts +0 -78
  63. package/dist/component/grid/BSGridHeaderCell.vue.d.ts +0 -45
  64. package/dist/component/grid/BSGridHeaderCellCheckbox.vue.d.ts +0 -19
  65. package/dist/component/grid/BSGridHeaderCellResizeHandle.vue.d.ts +0 -21
  66. package/dist/component/grid/BSGridHeaderCellSerialNo.vue.d.ts +0 -2
  67. package/dist/component/grid/BSGridLookup.vue.d.ts +0 -67
  68. package/dist/component/grid/BSGridRow.vue.d.ts +0 -140
  69. package/dist/component/grid/BSTextFilter.vue.d.ts +0 -42
  70. package/dist/component/grid/DateFilterModel.d.ts +0 -19
  71. package/dist/component/grid/GridExtension.d.ts +0 -17
  72. package/dist/component/grid/GridLib.d.ts +0 -21
  73. package/dist/component/grid/GridModel.d.ts +0 -399
  74. package/dist/component/grid/extension/gridExcelDownloadExtension.d.ts +0 -22
  75. package/dist/component/grid/vDndSupport.d.ts +0 -18
  76. package/dist/component/input/BSCheckbox.vue.d.ts +0 -63
  77. package/dist/component/input/BSCheckboxGroup.vue.d.ts +0 -79
  78. package/dist/component/input/BSCodeEditor.vue.d.ts +0 -73
  79. package/dist/component/input/BSColorInput.vue.d.ts +0 -68
  80. package/dist/component/input/BSDateInput.vue.d.ts +0 -90
  81. package/dist/component/input/BSDateInputPopup.vue.d.ts +0 -41
  82. package/dist/component/input/BSDateRange.vue.d.ts +0 -113
  83. package/dist/component/input/BSDateRangeInputPopup.vue.d.ts +0 -45
  84. package/dist/component/input/BSDateRangePresets.vue.d.ts +0 -40
  85. package/dist/component/input/BSHtmlEditor.vue.d.ts +0 -64
  86. package/dist/component/input/BSImageUpload.vue.d.ts +0 -118
  87. package/dist/component/input/BSMediaPreview.vue.d.ts +0 -55
  88. package/dist/component/input/BSMultiImageUpload.vue.d.ts +0 -108
  89. package/dist/component/input/BSMultiSelect.vue.d.ts +0 -91
  90. package/dist/component/input/BSNumberInput.vue.d.ts +0 -91
  91. package/dist/component/input/BSPopupSelect.vue.d.ts +0 -54
  92. package/dist/component/input/BSPositionedImageUpload.vue.d.ts +0 -97
  93. package/dist/component/input/BSPriceInput.vue.d.ts +0 -91
  94. package/dist/component/input/BSRadioButton.vue.d.ts +0 -72
  95. package/dist/component/input/BSRadioButtonGroup.vue.d.ts +0 -86
  96. package/dist/component/input/BSSelect.vue.d.ts +0 -95
  97. package/dist/component/input/BSSelectPopup.vue.d.ts +0 -87
  98. package/dist/component/input/BSTextArea.vue.d.ts +0 -88
  99. package/dist/component/input/BSTextInput.vue.d.ts +0 -100
  100. package/dist/component/input/BSTreeMultiSelect.vue.d.ts +0 -89
  101. package/dist/component/input/BSTreeSelect.vue.d.ts +0 -90
  102. package/dist/component/input/BSYesNoGroup.vue.d.ts +0 -55
  103. package/dist/component/input/BSYesNoSelect.vue.d.ts +0 -51
  104. package/dist/component/input/BSYoutubeInputModal.vue.d.ts +0 -6
  105. package/dist/component/input/DateInputLib.d.ts +0 -27
  106. package/dist/component/input/DateRangePresetModel.d.ts +0 -16
  107. package/dist/component/input/HtmlEditorModel.d.ts +0 -5
  108. package/dist/component/input/ImageUploadLib.d.ts +0 -22
  109. package/dist/component/input/ImageUploadModel.d.ts +0 -12
  110. package/dist/component/input/ValidationErrors.vue.d.ts +0 -19
  111. package/dist/component/input/vDragAndDropSupport.d.ts +0 -10
  112. package/dist/component/layout/BSCardLayout.vue.d.ts +0 -41
  113. package/dist/component/layout/BSHorizontalLayoutResizer.vue.d.ts +0 -6
  114. package/dist/component/layout/BSListControl.vue.d.ts +0 -52
  115. package/dist/component/layout/BSListLayout.vue.d.ts +0 -85
  116. package/dist/component/layout/BSListLayoutItem.vue.d.ts +0 -82
  117. package/dist/component/layout/BSTabSheet.vue.d.ts +0 -63
  118. package/dist/component/layout/BSVerticalLayoutResizer.vue.d.ts +0 -6
  119. package/dist/component/layout/SlideDownTransition.vue.d.ts +0 -9
  120. package/dist/component/layout/TabSheetModel.d.ts +0 -49
  121. package/dist/component/multilang/BSMultiLangExpandButton.vue.d.ts +0 -2
  122. package/dist/component/multilang/BSMultiLangHtmlEditor.vue.d.ts +0 -84
  123. package/dist/component/multilang/BSMultiLangImageUpload.vue.d.ts +0 -81
  124. package/dist/component/multilang/BSMultiLangRichText.vue.d.ts +0 -93
  125. package/dist/component/multilang/BSMultiLangTextArea.vue.d.ts +0 -96
  126. package/dist/component/multilang/BSMultiLangTextInput.vue.d.ts +0 -96
  127. package/dist/component/multilang/multiLangLib.d.ts +0 -6
  128. package/dist/component/richtext/BSRichText.vue.d.ts +0 -90
  129. package/dist/component/richtext/BSRichTextMaximizedModal.vue.d.ts +0 -74
  130. package/dist/component/richtext/RichTextLib.d.ts +0 -15
  131. package/dist/component/richtext/RichTextModel.d.ts +0 -9
  132. package/dist/component/richtext/RichTextToolbar.vue.d.ts +0 -36
  133. package/dist/component/richtext/image/ImageInsertModal.vue.d.ts +0 -22
  134. package/dist/component/richtext/image/ImageProperties.vue.d.ts +0 -19
  135. package/dist/component/richtext/image/ImagePropertiesModal.vue.d.ts +0 -17
  136. package/dist/component/richtext/image/RichTextImage.d.ts +0 -7
  137. package/dist/component/richtext/image/RichTextImageLib.d.ts +0 -13
  138. package/dist/component/richtext/image/ToolButtonImage.vue.d.ts +0 -19
  139. package/dist/component/richtext/link/LinkPropertiesModal.vue.d.ts +0 -17
  140. package/dist/component/richtext/link/ToolButtonLink.vue.d.ts +0 -19
  141. package/dist/component/richtext/richTextUtil.d.ts +0 -18
  142. package/dist/component/richtext/table/RichTextTable.d.ts +0 -8
  143. package/dist/component/richtext/table/RichTextTableCell.d.ts +0 -1
  144. package/dist/component/richtext/table/RichTextTableLib.d.ts +0 -23
  145. package/dist/component/richtext/table/RichTextTableView.d.ts +0 -19
  146. package/dist/component/richtext/table/TableInsertModal.vue.d.ts +0 -17
  147. package/dist/component/richtext/table/TablePropertiesModal.vue.d.ts +0 -17
  148. package/dist/component/richtext/table/ToolButtonTable.vue.d.ts +0 -19
  149. package/dist/component/richtext/tool/ToolButtonFontColor.vue.d.ts +0 -19
  150. package/dist/component/richtext/tool/ToolButtonFontSize.vue.d.ts +0 -19
  151. package/dist/component/richtext/tool/ToolButtonFontStyle.vue.d.ts +0 -19
  152. package/dist/component/richtext/tool/ToolButtonHeading.vue.d.ts +0 -19
  153. package/dist/component/richtext/tool/ToolButtonListItem.vue.d.ts +0 -19
  154. package/dist/component/richtext/tool/ToolButtonMaximize.vue.d.ts +0 -19
  155. package/dist/component/richtext/tool/ToolButtonTextAlign.vue.d.ts +0 -19
  156. package/dist/component/richtext/video/RichTextVideo.d.ts +0 -29
  157. package/dist/component/richtext/video/RichTextVideoLib.d.ts +0 -16
  158. package/dist/component/richtext/video/ToolButtonVideo.vue.d.ts +0 -19
  159. package/dist/component/richtext/video/VideoInsertModal.vue.d.ts +0 -22
  160. package/dist/component/richtext/video/VideoProperties.vue.d.ts +0 -19
  161. package/dist/component/richtext/video/VideoPropertiesModal.vue.d.ts +0 -17
  162. package/dist/component/richtext/youtube/RichTextYoutube.d.ts +0 -8
  163. package/dist/component/richtext/youtube/RichTextYoutubeLib.d.ts +0 -14
  164. package/dist/component/richtext/youtube/ToolButtonYoutube.vue.d.ts +0 -19
  165. package/dist/component/richtext/youtube/YoutubeInsertModal.vue.d.ts +0 -17
  166. package/dist/component/richtext/youtube/YoutubeProperties.vue.d.ts +0 -19
  167. package/dist/component/richtext/youtube/YoutubePropertiesModal.vue.d.ts +0 -17
  168. package/dist/component/tree/BSTree.vue.d.ts +0 -104
  169. package/dist/component/tree/BSTreeControl.vue.d.ts +0 -90
  170. package/dist/component/tree/BSTreeRow.vue.d.ts +0 -89
  171. package/dist/component/tree/TreeLib.d.ts +0 -11
  172. package/dist/component/tree/TreeModel.d.ts +0 -188
  173. package/dist/component/tree/vTreeDndSupport.d.ts +0 -10
  174. package/dist/config/config.d.ts +0 -155
  175. package/dist/contextmenu/BSContextMenu.vue.d.ts +0 -43
  176. package/dist/contextmenu/BSContextMenuContainer.vue.d.ts +0 -2
  177. package/dist/contextmenu/contextMenuPlugin.d.ts +0 -43
  178. package/dist/directive/vClickOutside.d.ts +0 -21
  179. package/dist/directive/vFocusJump.d.ts +0 -19
  180. package/dist/directive/vFocusLoop.d.ts +0 -25
  181. package/dist/directive/vFocusOnLoad.d.ts +0 -4
  182. package/dist/directive/vT.d.ts +0 -11
  183. package/dist/directive/vTooltip.d.ts +0 -9
  184. package/dist/frame/FrameModel.d.ts +0 -82
  185. package/dist/frame/ShortcutKeyHandler.d.ts +0 -37
  186. package/dist/frame/default/DefaultBody.vue.d.ts +0 -2
  187. package/dist/frame/default/DefaultFrameModel.d.ts +0 -107
  188. package/dist/frame/default/DefaultHeader.vue.d.ts +0 -23
  189. package/dist/frame/default/HeaderTab.vue.d.ts +0 -17
  190. package/dist/frame/default/HeaderTabListButton.vue.d.ts +0 -2
  191. package/dist/frame/default/HeaderTabs.vue.d.ts +0 -15
  192. package/dist/frame/default/SidebarMenu.vue.d.ts +0 -2
  193. package/dist/frame/default/SidebarMenuItem.vue.d.ts +0 -25
  194. package/dist/frame/default/SidebarMenuLink.vue.d.ts +0 -29
  195. package/dist/frame/default/SidebarMenuSearch.vue.d.ts +0 -28
  196. package/dist/index-DRs7QwNQ.js +0 -70174
  197. package/dist/modal/BSAlertModal.vue.d.ts +0 -23
  198. package/dist/modal/BSModalContainer.vue.d.ts +0 -2
  199. package/dist/modal/BSModalFrame.vue.d.ts +0 -37
  200. package/dist/modal/BSModalWrapper.vue.d.ts +0 -17
  201. package/dist/modal/BSYesNoModal.vue.d.ts +0 -25
  202. package/dist/modal/modalPlugin.d.ts +0 -73
  203. package/dist/model/CommonTypes.d.ts +0 -96
  204. package/dist/model/DefaultImpl.d.ts +0 -8
  205. package/dist/model/FieldContext.d.ts +0 -15
  206. package/dist/model/FilterModel.d.ts +0 -114
  207. package/dist/model/MoneySerializer.d.ts +0 -3
  208. package/dist/model/PaginatedList.d.ts +0 -29
  209. package/dist/model/SearchModel.d.ts +0 -22
  210. package/dist/notification/BSAlarmFrame.vue.d.ts +0 -26
  211. package/dist/notification/BSNotificationContainer.vue.d.ts +0 -2
  212. package/dist/notification/notificationPlugin.d.ts +0 -33
  213. package/dist/savepoint/SavePoint.d.ts +0 -47
  214. package/dist/text/i18n.d.ts +0 -28
  215. package/dist/util/alertUtil.d.ts +0 -1
  216. package/dist/util/componentUtil.d.ts +0 -40
  217. package/dist/util/debounceUtil.d.ts +0 -16
  218. package/dist/util/elementUtil.d.ts +0 -58
  219. package/dist/util/formatUtil.d.ts +0 -70
  220. package/dist/util/langUtil.d.ts +0 -3
  221. package/dist/util/multiLangUtil.d.ts +0 -27
  222. package/dist/util/routeUtil.d.ts +0 -3
  223. package/dist/util/typeUtil.d.ts +0 -18
  224. package/dist/util/waitUtil.d.ts +0 -19
  225. package/dist/validator/FieldValidator.d.ts +0 -45
  226. package/dist/validator/FormValidator.d.ts +0 -35
  227. package/scss/bluesea-base.scss +0 -6
  228. package/scss/bluesea.scss +0 -2
  229. package/scss/component.scss +0 -515
  230. package/scss/font.scss +0 -3
  231. package/scss/frame/default.scss +0 -293
  232. package/scss/layout.scss +0 -192
  233. package/scss/reset.scss +0 -68
  234. package/scss/rich-text-container.scss +0 -113
  235. package/scss/rich-text-content.scss +0 -137
  236. package/scss/transition.scss +0 -108
  237. package/scss/utility.scss +0 -816
  238. package/scss/variable.scss +0 -347
  239. package/text/bluesea_text_en.json +0 -956
  240. package/text/bluesea_text_fr.json +0 -956
  241. package/text/bluesea_text_ja.json +0 -956
  242. package/text/bluesea_text_ko.json +0 -956
  243. package/text/bluesea_text_zh.json +0 -956
@@ -0,0 +1,196 @@
1
+ # Bluesea components — detailed prop reference
2
+
3
+ Use this as a prop lookup. When in doubt read the `.vue` file — the `defineProps<{...}>()` block is always the ground truth. v6 distributes components across packages: core, basic, input, grid, tree, richtext, global, frame. The canonical source is `packages/<package>/src/component/`.
4
+
5
+ ## Shared conventions
6
+
7
+ Most inputs share this surface — only deltas are listed per component below.
8
+
9
+ | Prop | Type | Notes |
10
+ |---|---|---|
11
+ | `v-model` | matches cell type | Two-way binding. |
12
+ | `name` | `string` | Required for validation + SavePoint tracking. Auto-generated when omitted, but explicit names are easier to debug. |
13
+ | `disabled` | `boolean` | Also disables validation unless `forceValidateWhenDisabled` is set. |
14
+ | `required` | `boolean` | Enables built-in "required" rule. |
15
+ | `viewMode` | `boolean` | Renders as read-only text (no `<input>`). |
16
+ | `placeholder` | `MultiLangText` | |
17
+ | `width` | CSS size string | Default `'200px'` on text/number inputs. |
18
+ | `tabindex` | `number` | |
19
+ | `hideErrorMessage` | `boolean` | Suppress the inline error `<span>`. |
20
+ | `showErrorMessageOnDisabled` | `boolean` | Show errors even when disabled. |
21
+ | `ignoreSavePoint` | `boolean` | Opt out of SavePoint "modified" tracking. |
22
+ | `extraValidationRules` | `FieldValidationRule<T>[]` | Custom rules appended after built-ins. |
23
+ | `validationMessage*` | `MultiLangText` | Overrides for each built-in rule's error message. |
24
+
25
+ ## Basic
26
+
27
+ ### BSButton (`basic/BSButton.vue`)
28
+ Renders as `<button>`, `<a>`, or `<router-link>` depending on props.
29
+
30
+ - `caption: string | MultiLangText`
31
+ - `buttonColor: 'default' | 'blue' | 'red' | 'orange' | 'green' | 'gray' | 'underline'` (default `'default'`)
32
+ - `leftIcon`, `rightIcon: string` — BSIcon name (not font-icon ligature; v6 uses SVG)
33
+ - `disabled: boolean`
34
+ - `linkUrl: string`, `linkTarget: string` — renders `<a>`
35
+ - `routePath: string` — renders `<router-link :to>`
36
+ - `tooltip: MultiLangText`
37
+
38
+ ### BSIcon (`basic/BSIcon.vue`)
39
+ Material Symbols SVG replacement for the old icon font.
40
+
41
+ - `name: string` — Material Symbol name (e.g. `'check'`, `'delete'`, `'edit'`)
42
+ - `filled: boolean` (default `false`)
43
+ - Size via Tailwind classes on the parent or custom CSS
44
+
45
+ ### BSFormLabel (`basic/BSFormLabel.vue`)
46
+ - `label: MultiLangText`
47
+ - `required: boolean`
48
+ - `tooltip: MultiLangText`
49
+
50
+ ### BSLink, BSImage, BSPopup, BSPopupButton, BSProgressBar, BSLoadingIcon, BSDate, BSCalendar, BSCalendarRange, BSPageNavigation, BSConsole
51
+ Read the individual files — they are short and each has a targeted purpose.
52
+
53
+ ## Text / number / money
54
+
55
+ ### BSTextInput (`input/BSTextInput.vue`)
56
+ `v-model` is `string`.
57
+
58
+ - `inputType: 'text' | 'password'`
59
+ - `maxlength: number` — HTML attribute
60
+ - `minLength: number`, `maxLength: number` — validation rules
61
+ - `regExp: string`
62
+ - `trimValue: boolean` (default `true`) — trims before emit
63
+ - `prefix`, `suffix: MultiLangText | MultiLangText[] | PrefixSuffix | PrefixSuffix[]` — decoration in the input border
64
+ - `autocomplete: string`
65
+
66
+ ### BSNumberInput, BSPriceInput, BSPercentInput (`input/BSNumberInput.vue`, etc.)
67
+ `v-model` is `number | undefined`.
68
+
69
+ - `minValue`, `maxValue: number`
70
+ - `decimalPlace: number`
71
+ - `thousandSeparator: boolean`
72
+ - `unit: MultiLangText` — e.g. `'원'`, `'%'`
73
+ - `currencyCode: CurrencyCode` — on `BSPriceInput`; falls back to `useBlueseaConfig().defaultCurrencyCode`
74
+
75
+ ### BSTextArea (`input/BSTextArea.vue`)
76
+ `v-model` is `string`. Adds `rows: number`, `autoResize: boolean`.
77
+
78
+ ### BSColorInput (`input/BSColorInput.vue`)
79
+ `v-model` is a hex string like `'#RRGGBB'`.
80
+
81
+ ## Date / time
82
+
83
+ ### BSDateInput, BSDateRange (`input/BSDateInput.vue`, `BSDateRange.vue`)
84
+ `v-model` is an ISO string (`dayjs`-parseable).
85
+
86
+ - `dateFormat: string` — dayjs format; falls back to `useBlueseaConfig().dateFormat`
87
+ - `timeZone: TimeZone` — falls back to `useBlueseaConfig().timeZone`
88
+ - `resolution: DateResolution` — `'DAY' | 'HOUR' | 'MINUTE' | 'MINUTE_10' | 'MINUTE_30' | 'SECOND'`
89
+ - `minValue`, `maxValue: string` — ISO bounds
90
+
91
+ Range version emits two values via `v-model:from` / `v-model:to`, or via a `DateRange` object depending on flavor; check the component.
92
+
93
+ ### BSCalendar, BSCalendarRange
94
+ Embedded calendars; useful inside custom popups. (Moved from basic to input in v6.)
95
+
96
+ ## Selection
97
+
98
+ ### BSSelect (`input/BSSelect.vue`)
99
+ Generic: `<BSSelect :items="items" :key-provider="..." :label-provider="..." />`.
100
+
101
+ - `items: T[]`
102
+ - `keyProvider: (item: T) => string | undefined`
103
+ - `labelProvider: (item: T) => MultiLangText | undefined`
104
+ - `iconProvider`, `tooltipProvider`, `enabledItemProvider`
105
+ - `allowEmpty: boolean`, `emptyLabel: MultiLangText`
106
+ - `filterable: boolean` — show search box in popup
107
+
108
+ ### BSMultiSelect, BSTreeSelect, BSTreeMultiSelect, BSPopupSelect
109
+ Same general shape; multi-select uses `Set<T>` or `T[]`. `BSTreeSelect` / `BSTreeMultiSelect` moved to tree package in v6.
110
+
111
+ ### BSCheckbox, BSCheckboxGroup, BSRadioButton, BSRadioButtonGroup, BSYesNoSelect, BSYesNoGroup
112
+ Check the `defineProps` block; `items` + `keyProvider` + `labelProvider` pattern is consistent.
113
+
114
+ ## Upload
115
+
116
+ ### BSImageUpload (`input/BSImageUpload.vue`), BSFileUpload, BSMultiImageUpload, BSPositionedImageUpload
117
+ `v-model` is `StoredFile | StoredFile[]`.
118
+
119
+ Key props:
120
+ - `maxFileSize: number` (falls back to `useBlueseaConfig().maxFileSize`)
121
+ - `accept: string` — mime filter
122
+ - `preloadOverlay: boolean` — preload preview overlay image
123
+ - Upload handlers come from your backend; Bluesea itself doesn't own the upload URL. You store the resulting URL on the `StoredFile.fileUrl` when the upload succeeds.
124
+
125
+ ## Multi-language inputs
126
+
127
+ `BSMultiLangTextInput`, `BSMultiLangTextArea`, `BSMultiLangRichText`, `BSMultiLangHtmlEditor`, `BSMultiLangImageUpload` wrap their single-language counterparts and bind to a `MultiLangString` (or `MultiLangStoredFile`) keyed by `dataLocales` from config. They render one input per locale with a language selector.
128
+
129
+ ## Rich text / code
130
+
131
+ ### BSRichText (`richtext/BSRichText.vue`)
132
+ TipTap-based WYSIWYG.
133
+
134
+ - `v-model` — HTML string
135
+ - `toolButtons: ToolButton[]` — subset of `'Heading' | 'FontSize' | 'FontColor' | 'FontStyle' | 'TextAlign' | 'ListItem' | 'Link' | 'Table' | 'Image' | 'Video' | 'Youtube'`
136
+ - `imageInsertModal`, `videoInsertModal: Component` — override default modals
137
+
138
+ ### BSHtmlEditor (`richtext/BSHtmlEditor.vue`)
139
+ Split view (source + preview) for raw HTML.
140
+
141
+ ### BSCodeEditor (`input/BSCodeEditor.vue`)
142
+ CodeMirror 6 — props `language`, `theme`, `readonly`.
143
+
144
+ ## Layout
145
+
146
+ ### BSCardLayout (`basic/BSCardLayout.vue`)
147
+ Container with header/footer slots.
148
+
149
+ ### BSListLayout + BSListControl (`basic/BSListLayout.vue`)
150
+ Master-detail layout with resizable panes.
151
+
152
+ ### BSTabSheet (`basic/BSTabSheet.vue`)
153
+ Manages tabs, provides a SavePoint to each tab. Config toggles `blockLeavingModifiedTab` / `confirmLeavingModifiedTab` via `createBluesea` options.
154
+
155
+ ### BSHorizontalLayoutResizer, BSVerticalLayoutResizer
156
+ Drag-to-resize between two DOM siblings.
157
+
158
+ ## Tree
159
+
160
+ ### BSTree (`tree/BSTree.vue`)
161
+ - `items: T[]`, `childrenProvider: (item: T) => T[] | undefined`
162
+ - `keyProvider`, `labelProvider`, `iconProvider`
163
+ - `draggable: boolean`
164
+ - `selection: Set<T>` via `v-model:selected`
165
+ - Scoped slot `#item="{ item }"` to customise rendering
166
+
167
+ ### BSTreeControl
168
+ Toolbar for BSTree (add / remove / expand-all).
169
+
170
+ ## Global entity inputs (in global package)
171
+
172
+ ### BSNameInput, BSTelInput, BSAddressInput
173
+ Bind to `Name`, `Tel`, `Address` models. Layout comes from `createBluesea({ extended: { componentConfig: { nameInput: {...}, ... } } })`, so configure once globally.
174
+
175
+ ### BSLocaleSelect
176
+ Dropdown that switches `useBlueseaConfig().currentLocale`. Handy for a language toggle in the header.
177
+
178
+ ## Popup
179
+
180
+ ### BSPopup, BSPopupButton, BSSelectPopup, BSDateInputPopup, BSDateRangeInputPopup
181
+ The popup system anchors a component to a target element. `BSPopup` is the low-level primitive — pass `target`, `open`, `position`. Most selection components already wrap `BSPopup` internally; reach for it yourself only for custom pickers.
182
+
183
+ ## Grid
184
+
185
+ See `references/grid.md`. Top-level exports: `BSGrid`, `BSGridLookup`, `BSGridControl`, plus types `Column`, `EditingRows`, `PageGridHandler`, `createPageGridHandler`.
186
+
187
+ ## Notification / modal / context menu containers
188
+
189
+ - `BSModalContainer` — mount once; the modal system renders into it.
190
+ - `BSNotificationContainer` — mount once; drives notifications and alarms.
191
+ - `BSContextMenuContainer` — mount once; the context-menu system renders into it.
192
+
193
+ ## Frame
194
+
195
+ - `DefaultHeader`, `DefaultBody` — BackOffice app shell.
196
+ - `BSAlarmFrame` — standalone alarm container if you don't use DefaultFrame.
@@ -0,0 +1,159 @@
1
+ # BSGrid deep-dive
2
+
3
+ BSGrid is the highest-surface-area component in Bluesea. This reference covers the parts you won't guess from prop names: the `createPageGridHandler` factory, lookup/filter wiring, inline editing, column preferences, extensions, and Excel export. For quick starts use the `BSGridGuide.vue` demo and the snippet in the main SKILL.md.
4
+
5
+ ## When to use which wiring
6
+
7
+ | Scenario | Use |
8
+ |---|---|
9
+ | Client-side, fixed data, no paging | `<BSGrid :columns :data />` directly. |
10
+ | Server-side paging + sorting | `createPageGridHandler(option)` and bind its `grid`, `gridEventListener`, `control`, `controlEventListener`, `lookup`, `lookupEventListener` to `BSGrid`, `BSGridControl`, `BSGridLookup`. |
11
+ | Inline add/remove/edit | `createPageGridHandler({ editable: true, ... })` or pass `:editing-rows` + `#<prop>.edit` slots manually. |
12
+
13
+ ## `createPageGridHandler` option
14
+
15
+ From `packages/grid/src/`:
16
+
17
+ ```ts
18
+ createPageGridHandler<T>({
19
+ gridId: string, // used by gridPreferenceStore; omit to opt out of persistence
20
+ editable: boolean,
21
+ getRowKey: (row: T) => string,
22
+ newRowCreator: () => T | undefined, // when editable
23
+ addRowToLast: boolean, // default false — new rows go to top
24
+ removeRowHandler: (rows: Set<T>) => boolean, // return true if you handle deletion yourself
25
+ isRowEditable: (row, editingRows) => boolean,
26
+ isRowSelectable: (row) => boolean,
27
+ getGridData: (param: SearchParam) => PaginatedList<T> | Promise<PaginatedList<T>>,
28
+ limit: number, // default 100
29
+ limitItems: number[], // default [100, 300, 500]
30
+ defaultFilter: Filter[],
31
+ defaultSorts: Sort[],
32
+ })
33
+ ```
34
+
35
+ `SearchParam` carries `offset`, `limit`, `sorts`, `defaultFilter`, `lookupFilter`, `gridFilter`. `PaginatedList<T>` is `{ offset, totalCount, data: T[] }`. Return these from your API.
36
+
37
+ Call `handler.loadGridData()` once after setup to populate. Afterwards, Bluesea refreshes on sort/limit/offset/filter changes automatically via `gridEventListener` / `controlEventListener` / `lookupEventListener`.
38
+
39
+ ## Column definition
40
+
41
+ ```ts
42
+ type Column<T> = {
43
+ propertyId: string // key into row data
44
+ templateId?: string // slot name override (defaults to propertyId)
45
+ caption: MultiLangText
46
+ cellType?: 'TEXT' | 'NUMBER' | 'DATE' | 'PERCENTAGE' | 'BOOL' | 'MULTI_LANG_STRING' | 'NAME' | 'TEL' | 'ADDRESS' | 'MONEY'
47
+ dateFormat?: string | 'DAY' | 'MINUTE' | 'SECOND'
48
+ width?: number
49
+ sortable?: boolean
50
+ sortPropertyId?: string // if sort key ≠ display key
51
+ cellStyleClass?: string
52
+ cellStyleCss?: string
53
+ headerCellStyleClass?: string
54
+ headerCellStyleCss?: string
55
+ tooltipProvider?: (row: T) => MultiLangText | undefined
56
+ }
57
+ ```
58
+
59
+ `cellType` gives you free formatting. `MULTI_LANG_STRING` picks the current data locale; `MONEY` uses the configured `moneySerializer`. Override any cell by defining a `#<propertyId>` slot.
60
+
61
+ ## Slots
62
+
63
+ | Slot | Purpose |
64
+ |---|---|
65
+ | `#<propertyId>="{ row }"` | Display cell |
66
+ | `#<propertyId>.edit="{ row }"` | Editor cell (when row is in `editingRows`) |
67
+ | `#<propertyId>.filter="{ ... }"` | Header filter cell |
68
+ | `#emptyMessage` | Shown when `data.length === 0` |
69
+
70
+ Slot names come from `propertyId` (or `templateId` when provided). You can mix — only override the columns you need.
71
+
72
+ ## Inline editing lifecycle
73
+
74
+ ```
75
+ user clicks edit icon
76
+ → gridEventListener.changeEditingRow(row, true)
77
+ → handler adds row to editingRows
78
+ → #propertyId.edit slot renders for that row, with a BSTextInput/BSNumberInput etc.
79
+ → SavePoint on the row tracks modified state
80
+ user clicks save/cancel
81
+ → you validate, call editingRows.removeRow(row) + savePoint.set() or rollback()
82
+ ```
83
+
84
+ `EditingRows<T>` supports `addRow`, `removeRow`, `getModifiedRows`, `getRows`. You usually pass an instance into `BSGrid :editing-rows="..."`.
85
+
86
+ ## Filter (BSGridLookup)
87
+
88
+ `BSGridLookup` renders above the grid as the "search bar". Its config shape:
89
+
90
+ ```ts
91
+ type GridLookupConfig = {
92
+ textFilter?: {
93
+ filterItems: Array<{
94
+ propertyId: string
95
+ caption: MultiLangText
96
+ prefix?: boolean // wrap keyword with leading % (default true)
97
+ suffix?: boolean // trailing % (default true)
98
+ filterCreator?: TextFilterCreator // custom filter builder
99
+ filterType?: 'STRING' | 'NUMBER'
100
+ }>
101
+ }
102
+ dateFilter?: {
103
+ filterItems: Array<{
104
+ propertyId: string
105
+ caption: MultiLangText
106
+ timeZone?: TimeZone
107
+ dateFormat?: string
108
+ popupDateFormat?: string
109
+ filterWidth?: string
110
+ }>
111
+ }
112
+ }
113
+ ```
114
+
115
+ For an "embedded Name" column that should search across `name.name1..4`, use the built-in `nameFilterCreator(maxIndex)`:
116
+
117
+ ```ts
118
+ import { nameFilterCreator } from '@g1cloud/bluesea'
119
+ filterItems: [{ propertyId: 'memberName', caption: '회원명', filterCreator: nameFilterCreator() }]
120
+ ```
121
+
122
+ ## GridFilter (header-level per-column filter)
123
+
124
+ Separate from Lookup — this is the popup that appears from column headers. Enable with `<BSTextFilter>` / `<BSDateRangeFilter>` / `<BSDateRangeFilters>` in the `#<propertyId>.filter` slot. They emit filters that feed back into `searchParam.gridFilter`.
125
+
126
+ ## Column preferences
127
+
128
+ If you pass `gridId` and install a `gridPreferenceStore` in `createBluesea({ extended: { gridPreferenceStore: ... } })`, Bluesea persists column widths, order, hidden flags, and the last `dateFilter` per gridId. `LocalStorageGridPreferenceStore` is the canonical implementation. Implement the `GridPreferenceStore` interface to back it with something else (cloud user prefs, etc.).
129
+
130
+ ## Extensions
131
+
132
+ `GridExtension` lets external packages inject cell renderers, row actions, toolbar buttons. The canonical example is `gridExcelDownloadExtension` (from grid package) which adds an Excel-export button to `BSGridControl`. Import and pass via the `extensions` prop.
133
+
134
+ ```ts
135
+ import { BSGrid, BSGridControl, gridExcelDownloadExtension } from '@g1cloud/bluesea'
136
+
137
+ const extensions = [gridExcelDownloadExtension({
138
+ fileName: 'users.xlsx',
139
+ getRows: async () => (await api.users.searchAll(searchParam)).data,
140
+ })]
141
+ ```
142
+
143
+ ## Fixed columns
144
+
145
+ `fixedColumnCount: number` on `GridBinding` freezes the first N columns during horizontal scroll. The user can also drag a divider to change it live — bind `settingChanged` on `gridEventListener` to persist.
146
+
147
+ ## Row display/select policy
148
+
149
+ - `rowDisplayPolicy: (row) => boolean` — hide specific rows client-side
150
+ - `rowSelectPolicy: (row) => boolean` — disable checkbox for specific rows
151
+ - `rowEditPolicy: (row, editingRows) => boolean` — per-row editability (also `option.isRowEditable` on the handler)
152
+
153
+ ## Common mistakes
154
+
155
+ - Forgetting to call `await gridHandler.loadGridData()` after setup — grid stays empty.
156
+ - Passing `defaultSorts` and expecting them to show as active sort chevrons; defaults are **appended** to user sorts in the query, not shown in the header.
157
+ - Using `propertyId: 'a.b'` for nested data — valid, but cells look up with dot-notation. Be sure `b` exists.
158
+ - Forgetting that `editingRows` must be the same reactive instance across parent/child; the handler already creates one, so use `gridHandler.grid.editingRows`.
159
+ - Overriding `#<prop>` slot and forgetting `.edit` variant — result: edit cells fall back to the default text display.
@@ -0,0 +1,149 @@
1
+ # i18n and MultiLangText
2
+
3
+ Bluesea treats every user-visible text prop as potentially multi-lingual. Understanding `MultiLangText` and the `useTexts()` composable up front prevents 90% of "caption is showing `{ key: 'xxx' }`" mistakes.
4
+
5
+ ## The `MultiLangText` union
6
+
7
+ ```ts
8
+ type LocaleName = string // e.g. 'ko', 'en', 'ja', 'ko-KR'
9
+
10
+ type MultiLangString = Record<LocaleName, string> // { ko: '저장', en: 'Save' }
11
+ type MultiLangMessage = { key: string; args?: unknown[]; locale?: LocaleName }
12
+
13
+ type MultiLangText =
14
+ | string
15
+ | MultiLangString
16
+ | MultiLangMessage
17
+ ```
18
+
19
+ Bluesea uses these rules to render:
20
+
21
+ 1. If it's a plain string, use it as-is.
22
+ 2. If it's an object with `key`, look the key up in the `I18NTexts` registry for the current locale and substitute `args`.
23
+ 3. Otherwise, treat it as per-locale map and pick `currentLocale` (with fallbacks).
24
+
25
+ You detect `MultiLangMessage` by the presence of a `key` property; `isMultiLangMessage(text)` does this check.
26
+
27
+ ## Registering texts
28
+
29
+ v6 uses composables instead of module-scope singletons. Get the `useTexts()` composable and call methods on it:
30
+
31
+ ```ts
32
+ import { useTexts } from '@g1cloud/bluesea'
33
+
34
+ const { addTexts } = useTexts()
35
+
36
+ addTexts('ko', [
37
+ { key: 'btn.save', text: '저장' },
38
+ { key: 'err.required', text: '필수 입력 항목입니다.' },
39
+ { key: 'msg.saved', text: '{0} 개가 저장되었습니다.' }, // {0} {1} for args
40
+ { key: 'help.html', text: '<strong>주의</strong>', html: true },
41
+ ])
42
+
43
+ addTexts('en', [
44
+ { key: 'btn.save', text: 'Save' },
45
+ { key: 'err.required', text: 'This field is required.' },
46
+ { key: 'msg.saved', text: '{0} items saved.' },
47
+ ])
48
+ ```
49
+
50
+ ### Compact JSON format
51
+
52
+ For large catalogues, `addTexts` also accepts a compact object form so the JSON file stays small. Same call — the runtime detects the shape via `Array.isArray`:
53
+
54
+ ```ts
55
+ const { addTexts } = useTexts()
56
+
57
+ addTexts('ko', {
58
+ 'btn.save': '저장', // plain text
59
+ 'help.html': ['<strong>주의</strong>', 1], // array → html: true
60
+ })
61
+ ```
62
+
63
+ - value is a **string** → equivalent to `{ key, text, html: false }`
64
+ - value is `[text, 1]` (any truthy second element) → equivalent to `{ key, text, html: true }`
65
+
66
+ The shipped `@g1cloud/bluesea/text/bluesea_text_*.json` files use this compact format. Both formats can be mixed for the same locale; later calls overwrite earlier keys.
67
+
68
+ For real apps, store texts in `texts_ko.json` / `texts_en.json` and run the message generator. The generator produces type-safe key constants.
69
+
70
+ Locale fallback chain: `currentLocale` → parent locale (`ko-KR` → `ko`) → `defaultLocale`. If no match, the key itself is returned — that is what "unresolved" text looks like in the UI.
71
+
72
+ ## Looking up at runtime
73
+
74
+ ```ts
75
+ import { useTexts } from '@g1cloud/bluesea'
76
+
77
+ const { t, interpretMultiLangText } = useTexts()
78
+
79
+ const msg = t({ key: 'msg.saved', args: [3] }) // → '3 items saved.' (in en)
80
+
81
+ // Generic interpreter that accepts any MultiLangText
82
+ const label = interpretMultiLangText({ ko: '저장', en: 'Save' })
83
+ ```
84
+
85
+ ## Templates: interpolation instead of v-t
86
+
87
+ v5 used the `v-t` directive, which relied on DOM manipulation and doesn't work in SSR. v6 removes it — use `{{ t(...) }}` interpolation instead:
88
+
89
+ ```vue
90
+ <span>{{ t({ key: 'btn.save' }) }}</span>
91
+ <input :placeholder="t({ key: 'ph.name' })" />
92
+ <img :alt="t({ key: 'alt.logo' })" />
93
+ ```
94
+
95
+ When you have an HTML string to render, get it via `tr()` (text-render, returns HTML):
96
+
97
+ ```ts
98
+ const { tr } = useTexts()
99
+ const htmlMsg = tr({ key: 'help.html' }) // if registered with html: true
100
+ ```
101
+
102
+ And render with `v-html`:
103
+
104
+ ```vue
105
+ <div v-html="htmlMsg"></div>
106
+ ```
107
+
108
+ ## Switching locale
109
+
110
+ v6 uses composables to access config:
111
+
112
+ ```ts
113
+ import { useBlueseaConfig } from '@g1cloud/bluesea'
114
+
115
+ const { setCurrentLocale, setCurrentDataLocale } = useBlueseaConfig()
116
+ setCurrentLocale('en') // UI locale
117
+ setCurrentDataLocale('en') // BSMultiLang* pickers
118
+ ```
119
+
120
+ There is also `<BSLocaleSelect>` for a ready-made dropdown in the header.
121
+
122
+ ## Data locale vs UI locale
123
+
124
+ Bluesea separates two concerns:
125
+
126
+ - `currentLocale` / `locales` — which language the **chrome** renders in (labels, buttons, errors).
127
+ - `currentDataLocale` / `dataLocales` — which language the **data** is authored in, used by `BSMultiLang*` inputs (e.g. editing product name in `ko`, `en`, `ja`).
128
+
129
+ You can run the UI in Korean while editing English-only content, or vice versa. Most apps set both the same for end users but expose a separate data-locale toggle in admin tools.
130
+
131
+ ## `MultiLangString` in data models
132
+
133
+ When a DB field is multilingual, model it as `MultiLangString` and render it with `cellType: 'MULTI_LANG_STRING'` in grids or `<BSMultiLangTextInput>` in forms. Bluesea picks the `currentDataLocale` with the same fallback chain.
134
+
135
+ ```ts
136
+ type Product = {
137
+ id: string
138
+ name: MultiLangString // { ko: '양말', en: 'Socks', ja: '靴下' }
139
+ description: MultiLangString
140
+ }
141
+ ```
142
+
143
+ ## Common mistakes
144
+
145
+ - Hard-coding Korean strings in a library component and wondering why they don't switch locale — wrap them as `{ key: '...' }` and register for each locale.
146
+ - Using `interpretMultiLangText` inside a `computed` and expecting it not to re-run when locale changes — it does, because the composable is reactive.
147
+ - Passing a key that doesn't exist — the key itself leaks into the UI. Check your text files or run the message generator.
148
+ - Using `{{ t(...) }}` without importing `useTexts()` — you get an undefined reference. Do it at the top of `<script setup>`.
149
+ - Storing `{ key: '...' }` in the database. Keys are UI concerns; database data should be `MultiLangString` values.
@@ -0,0 +1,178 @@
1
+ # Validation in Bluesea
2
+
3
+ Bluesea's validation is deliberately unusual. Instead of wiring a validator per field, inputs *register themselves* on their DOM element. A `FormValidator` then walks the DOM tree inside a root element, finds every registered `FieldValidator`, runs them, and collects errors. This means you rarely touch `FieldValidator` directly — just give each input a `name` and scope a `FormValidator` to the form root.
4
+
5
+ ## Types at a glance
6
+
7
+ ```ts
8
+ type ValidationPhase = 'input' | 'change' | 'blur' | 'form'
9
+
10
+ type ValidationError = {
11
+ code: string
12
+ message: MultiLangText
13
+ }
14
+
15
+ type FieldValidationRule<T> = (
16
+ value: T,
17
+ phase: ValidationPhase,
18
+ fieldContext?: FieldContext<any>,
19
+ ) => Promise<ValidationError[] | undefined> | ValidationError[] | undefined
20
+
21
+ type FormValidationError = ValidationError & { name?: string }
22
+
23
+ type FormValidationRule = (phase?: ValidationPhase) => Promise<FormValidationError[] | undefined>
24
+
25
+ class ValidationFailedError {
26
+ constructor(public errors: FormValidationError[]) {}
27
+ }
28
+ ```
29
+
30
+ ## Standard form flow
31
+
32
+ ```vue
33
+ <template>
34
+ <div ref="formEl">
35
+ <BSTextInput v-model="form.name" name="name" required :max-length="50" />
36
+ <BSTextInput v-model="form.email" name="email" required reg-exp="^[^@]+@[^@]+$" />
37
+ <BSNumberInput v-model="form.age" name="age" :min-value="0" :max-value="150" />
38
+ <BSButton caption="저장" @click="save" />
39
+ </div>
40
+ </template>
41
+
42
+ <script setup lang="ts">
43
+ import { ref } from 'vue'
44
+ import { formValidator, isValidationFailedError } from '@g1cloud/bluesea'
45
+ import { useNotification } from '@g1cloud/bluesea'
46
+
47
+ const formEl = ref<HTMLElement>()
48
+ const form = ref({ name: '', email: '', age: 0 })
49
+ const { showNotification } = useNotification()
50
+
51
+ const validator = formValidator({
52
+ element: formEl,
53
+ rules: [
54
+ // form-level cross-field rule
55
+ async () => form.value.age < 18 && form.value.name.includes('kid')
56
+ ? [{ code: 'ageMismatch', message: { key: 'err.ageMismatch' } }]
57
+ : undefined,
58
+ ],
59
+ })
60
+
61
+ async function save() {
62
+ try {
63
+ await validator.validate() // throws on failure
64
+ await api.save(form.value)
65
+ showNotification({ key: 'msg.saved' }, 'info')
66
+ } catch (e) {
67
+ if (isValidationFailedError(e)) {
68
+ showNotification({ key: 'err.validationFailed' }, 'error')
69
+ return
70
+ }
71
+ throw e
72
+ }
73
+ }
74
+ </script>
75
+ ```
76
+
77
+ `validate()` throws `ValidationFailedError` with the full error list. `validationResult()` returns the errors without throwing, which is handy for custom UI (e.g. show a summary banner).
78
+
79
+ ## Built-in field rules per input
80
+
81
+ Built-in rules are configured via props on each input. Each has a companion `validationMessage*` prop to override the default message.
82
+
83
+ | Rule | Inputs | Props |
84
+ |---|---|---|
85
+ | required | All input components | `required`, `validationMessageRequired` |
86
+ | length | `BSTextInput`, `BSTextArea` | `minLength`, `maxLength`, `validationMessageMinLength`, `validationMessageMaxLength`, `validationMessageBetweenLength` |
87
+ | regexp | `BSTextInput` | `regExp`, `validationMessageRegExp` |
88
+ | numeric range | `BSNumberInput`, `BSPriceInput`, `BSPercentInput` | `minValue`, `maxValue`, `validationMessageMinValue`, `validationMessageMaxValue`, `validationMessageBetweenValue` |
89
+ | date order | `BSDateRange` | auto; override `validationMessageDateOrder` |
90
+ | date bounds | `BSDateInput`, `BSDateRange` | `minValue`, `maxValue` |
91
+ | file size / count | `BSFileUpload`, `BS*ImageUpload` | `maxFileSize`, `maxFileCount` |
92
+
93
+ Default error messages are i18n keys like `bs.error.validation.required`, `bs.error.validation.lengthMin`, so translations come "for free" if you registered Bluesea's text files.
94
+
95
+ ## Custom field rules (`extraValidationRules`)
96
+
97
+ When built-ins aren't enough, pass `:extra-validation-rules` — an array of functions. Each returns `ValidationError[]` (or empty / undefined for "ok"). Phase control lets you defer expensive checks.
98
+
99
+ ```ts
100
+ import type { FieldValidationRule } from '@g1cloud/bluesea'
101
+
102
+ const uniqueEmail: FieldValidationRule<string> = async (value, phase) => {
103
+ if (!value || phase === 'input') return // run on blur / form only
104
+ const exists = await api.users.exists(value)
105
+ return exists ? [{ code: 'duplicate', message: { key: 'err.emailTaken' } }] : undefined
106
+ }
107
+
108
+ // in template
109
+ <BSTextInput v-model="form.email" name="email" required
110
+ :extra-validation-rules="[uniqueEmail]" />
111
+ ```
112
+
113
+ Rules of thumb:
114
+ - Use `phase === 'form'` to skip expensive work on keystrokes.
115
+ - Return `undefined` for "pass"; return an empty array also passes, but `undefined` is clearer.
116
+ - Throw *inside* rules sparingly — the validator does not convert thrown errors into nice messages.
117
+
118
+ ## Form-level rules
119
+
120
+ Cross-field rules go in `formValidator({ rules: [...] })`. They receive the phase but no value; you read your reactive state directly.
121
+
122
+ ```ts
123
+ formValidator({
124
+ element: formEl,
125
+ rules: [
126
+ async () => form.value.password !== form.value.passwordConfirm
127
+ ? [{ name: 'passwordConfirm', code: 'mismatch', message: { key: 'err.pwMismatch' } }]
128
+ : undefined,
129
+ ],
130
+ })
131
+ ```
132
+
133
+ The `name` on the error aligns the failure with a specific input so `validator.getFieldValidator(name)` can jump to it.
134
+
135
+ ## Talking to individual FieldValidators
136
+
137
+ Rarely needed, but when you want to trigger validation imperatively:
138
+
139
+ ```ts
140
+ import { validateField, validateFields } from '@g1cloud/bluesea'
141
+
142
+ await validateField('email') // by name
143
+ await validateFields(['email', 'age'])
144
+ await validator.getFieldValidator('email')?.validate('blur')
145
+ ```
146
+
147
+ Clearing errors (e.g. on `cancel`):
148
+
149
+ ```ts
150
+ validator.clear()
151
+ ```
152
+
153
+ ## Async rules and the "disabled" gotcha
154
+
155
+ If a field is `disabled`, its validator short-circuits to valid. Pass `:force-validate-when-disabled="true"` only when you really need validation despite the disabled state (rare — usually disabled means "field is derived / not user-editable").
156
+
157
+ For conditionally-disabled rules, prefer returning `undefined` early rather than toggling `disabled`, because `disabled` also skips the built-in rules.
158
+
159
+ ## FieldContext
160
+
161
+ A `FieldContext<T>` is injected when a field is rendered inside a grid editor. It gives the rule access to `row`, sibling values, and the editingRows collection. Use it when one cell's validity depends on another cell in the same row.
162
+
163
+ ```ts
164
+ const priceMustExceedCost: FieldValidationRule<number> = (price, _phase, ctx) => {
165
+ const row = ctx?.row as { cost?: number } | undefined
166
+ return (row && price !== undefined && row.cost !== undefined && price < row.cost)
167
+ ? [{ code: 'priceLtCost', message: { key: 'err.priceLtCost' } }]
168
+ : undefined
169
+ }
170
+ ```
171
+
172
+ ## Common mistakes
173
+
174
+ - Forgetting `name` on an input — the validator still runs, but `error.name` is empty, so "jump to error" UX breaks.
175
+ - Wrapping the `ref` target with `v-if` that unmounts on error — the error clears because the validator is unregistered. Prefer `:disabled` or `:view-mode` to hide without unmounting.
176
+ - Calling `validator.validate()` from a plain synchronous handler — it's async. `await` it.
177
+ - Using `throw new Error(...)` in a rule. Return `[{ code, message }]` instead.
178
+ - Re-creating `formValidator` on every render; create it once in `<script setup>` at module scope.