@g1cloud/bluesea 5.0.0-beta.27 → 5.0.0-beta.28

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 (34) hide show
  1. package/README.md +21 -0
  2. package/bin/install-claude-skill.mjs +74 -0
  3. package/dist/{BSAlertModal-DT2Wai4R.js → BSAlertModal-BpbJuAe1.js} +1 -1
  4. package/dist/{BSGridColumnSettingModal-CUMe_yWj.js → BSGridColumnSettingModal-8MqhRWkU.js} +1 -1
  5. package/dist/{BSRichTextMaximizedModal-CaaSAgmI.js → BSRichTextMaximizedModal-C86Skc5v.js} +1 -1
  6. package/dist/{BSYesNoModal-C91E2MSF.js → BSYesNoModal-CHbktVAj.js} +1 -1
  7. package/dist/{BSYoutubeInputModal-DSI-NoGb.js → BSYoutubeInputModal-JKnr4hGE.js} +1 -1
  8. package/dist/{ImageInsertModal-7u7YeHsI.js → ImageInsertModal-DQwkQJ8b.js} +2 -2
  9. package/dist/{ImageProperties.vue_vue_type_script_setup_true_lang-CSHlFWfd.js → ImageProperties.vue_vue_type_script_setup_true_lang-BsMcsXdh.js} +1 -1
  10. package/dist/{ImagePropertiesModal-HRPdVJRK.js → ImagePropertiesModal-X7blKqTy.js} +2 -2
  11. package/dist/{LinkPropertiesModal-x73isyqI.js → LinkPropertiesModal-DGiiTivW.js} +1 -1
  12. package/dist/{TableInsertModal-BfWLdDCa.js → TableInsertModal-CupFfnOG.js} +1 -1
  13. package/dist/{TablePropertiesModal-D1wSXQ3C.js → TablePropertiesModal-CfK9i7Q5.js} +1 -1
  14. package/dist/{VideoInsertModal-DQ-wO6_P.js → VideoInsertModal-BwRRgibx.js} +2 -2
  15. package/dist/{VideoProperties.vue_vue_type_script_setup_true_lang-D73f1tdh.js → VideoProperties.vue_vue_type_script_setup_true_lang-zEMpmzTZ.js} +1 -1
  16. package/dist/{VideoPropertiesModal-BYADjPfV.js → VideoPropertiesModal-Dn6AzhPy.js} +2 -2
  17. package/dist/{YoutubeInsertModal-DNq4v5Ll.js → YoutubeInsertModal-DCn5bhN5.js} +2 -2
  18. package/dist/{YoutubeProperties.vue_vue_type_script_setup_true_lang-CWIVZP3H.js → YoutubeProperties.vue_vue_type_script_setup_true_lang-B-YVlp4Y.js} +1 -1
  19. package/dist/{YoutubePropertiesModal-D2-7I2sg.js → YoutubePropertiesModal-Dg-n8cTv.js} +2 -2
  20. package/dist/bluesea.js +1 -1
  21. package/dist/bluesea.umd.cjs +9 -2
  22. package/dist/{index-BIvcVEog.js → index-e3O4IL4V.js} +25 -18
  23. package/dist/text/i18n.d.ts +2 -1
  24. package/package.json +6 -1
  25. package/skills/bluesea-ui/SKILL.md +312 -0
  26. package/skills/bluesea-ui/references/components.md +189 -0
  27. package/skills/bluesea-ui/references/grid.md +159 -0
  28. package/skills/bluesea-ui/references/i18n.md +126 -0
  29. package/skills/bluesea-ui/references/validation.md +176 -0
  30. package/text/bluesea_text_en.json +248 -964
  31. package/text/bluesea_text_fr.json +248 -964
  32. package/text/bluesea_text_ja.json +248 -964
  33. package/text/bluesea_text_ko.json +248 -964
  34. package/text/bluesea_text_zh.json +248 -964
@@ -0,0 +1,312 @@
1
+ ---
2
+ name: bluesea-ui
3
+ description: How to build g1cloud BackOffice UIs with the @g1cloud/bluesea Vue 3 component library — covers app bootstrap (configureBluesea, plugins), BS* components (BSButton, BSTextInput, BSGrid, BSSelect, BSModal, etc.), the FieldValidator/FormValidator pattern, useModal / showNotification, MultiLangText + i18n, SavePoint for inline edits, and the DefaultFrame shell. Use this skill whenever the user works in a project that imports from "@g1cloud/bluesea" or mentions any BS* component, BSGrid, BSModal, configureBluesea, useModal, MultiLangText, FieldValidator, formValidator, ValidationFailedError, SavePoint, or DefaultFrame — even when they only show a Vue snippet without naming the library. Also apply when the user asks about Bluesea, 블루씨, g1cloud BackOffice UI, or pastes code using <BSButton>, <BSTextInput>, <BSGrid>, etc.
4
+ ---
5
+
6
+ # @g1cloud/bluesea — Vue 3 UI library for g1cloud BackOffice
7
+
8
+ Bluesea is an enterprise-focused Vue 3 component library. Its design goals shape every API in this skill, so read them first — the patterns that look unusual usually follow from here:
9
+
10
+ - **Strict types.** Every component has explicit prop types. Prefer `<script setup lang="ts">` with `defineProps<{...}>()`. Don't invent loose props.
11
+ - **Unidirectional data flow.** Props down, `update:xxx` events up. Two-way binding is always `v-model:xxx`.
12
+ - **i18n-first.** Almost every text prop (caption, placeholder, tooltip, error message) accepts `MultiLangText`, not just `string`. See "MultiLangText" below.
13
+ - **Inversion of control.** Components expose scoped slots + plugin/extension hooks. Don't fork the component — fill the slot or pass an extension.
14
+ - **Validation is a first-class concern.** Inputs register themselves with a `FormValidator`. Submit handlers throw `ValidationFailedError`. See "Validation".
15
+
16
+ When you are unsure about a component's props, read the `.vue` file directly at `packages/bluesea/src/component/<group>/<Name>.vue` — the `defineProps<{...}>()` block is the source of truth. This is faster and more reliable than guessing.
17
+
18
+ ## 1. Project bootstrap
19
+
20
+ Every Bluesea app calls `configureBluesea(...)` before `createApp(...).mount(...)`, and installs the modal / notification / context-menu plugins that the library depends on. A realistic `main.ts` looks like this (trim to what you need):
21
+
22
+ ```ts
23
+ import { createApp } from 'vue'
24
+ import '@g1cloud/bluesea/css/bluesea.css'
25
+ import '@g1cloud/bluesea/css/frame-default.css' // only if you use DefaultFrame
26
+ import {
27
+ configureBluesea,
28
+ createModalPlugin,
29
+ createContextMenuPlugin,
30
+ LocalStorageGridPreferenceStore,
31
+ vT,
32
+ } from '@g1cloud/bluesea'
33
+ import App from './App.vue'
34
+
35
+ configureBluesea({
36
+ locales: ['ko', 'en', 'ja'],
37
+ defaultLocale: 'ko',
38
+ currentLocale: 'ko',
39
+ dataLocales: ['ko', 'en', 'ja'], // used by BSMultiLang* inputs
40
+ currentDataLocale: 'ko',
41
+ timeZone: 'Asia/Seoul',
42
+ dateFormat: 'YYYY-MM-DD HH:mm',
43
+ defaultCurrencyCode: 'KRW',
44
+ gridPreferenceStore: new LocalStorageGridPreferenceStore(), // persists BSGrid column widths/order
45
+ fileUrlResolver: (url) => url?.startsWith('http') ? url : `https://cdn.example.com/${url}`,
46
+ componentConfig: {
47
+ telInput: { requiredCountryNo: true, countries: [{ code: 'KR', name: 'Korea (+82)' }] },
48
+ addressInput:{ countries: [{ code: 'KR', name: 'Korea' }] },
49
+ calendar: { startYear: '-40', endYear: '+40' }, // relative to this year
50
+ },
51
+ })
52
+
53
+ createApp(App)
54
+ .use(createModalPlugin())
55
+ .use(createContextMenuPlugin())
56
+ // BSNotificationContainer does not need a plugin — just mount it once (see §4)
57
+ .directive('t', vT) // enables v-t="multiLangText"
58
+ .mount('#app')
59
+ ```
60
+
61
+ Global state lives on the reactive `blueseaConfig` object; call `blueseaConfig.setCurrentLocale('en')` to switch language at runtime and every `v-t` / caption updates.
62
+
63
+ You must also mount the container components somewhere near the app root — they are the portals the plugins render into:
64
+
65
+ ```vue
66
+ <template>
67
+ <router-view />
68
+ <BSModalContainer />
69
+ <BSNotificationContainer />
70
+ <BSContextMenuContainer />
71
+ </template>
72
+ ```
73
+
74
+ ## 2. MultiLangText (almost every text prop)
75
+
76
+ `MultiLangText` is the union that lets captions be either a plain string, a per-locale record, or an i18n key with args:
77
+
78
+ ```ts
79
+ type MultiLangText =
80
+ | string // plain — 'Save'
81
+ | Record<LocaleName, string> // per-locale — { ko: '저장', en: 'Save' }
82
+ | { key: string; args?: unknown[] } // i18n key — { key: 'btn.save' }
83
+ ```
84
+
85
+ Use i18n keys by default in real apps so the UI follows the user's locale. Register texts with `i18n.addTexts(locale, [...])` and render them with the `v-t` directive or the `t()` helper:
86
+
87
+ ```ts
88
+ import { i18n, t, interpretMultiLangText } from '@g1cloud/bluesea'
89
+
90
+ i18n.addTexts('ko', [{ key: 'btn.save', text: '저장' }])
91
+ i18n.addTexts('en', [{ key: 'btn.save', text: 'Save' }])
92
+
93
+ // inside <script setup>
94
+ const label = t({ key: 'btn.save' }) // resolves to 'Save' or '저장'
95
+ ```
96
+
97
+ ```vue
98
+ <span v-t="{ key: 'label.name' }" />
99
+ <BSButton :caption="{ key: 'btn.save' }" />
100
+ ```
101
+
102
+ When the user gives you a Korean/English string literal in demo code, pass it through as-is — components accept a plain string too. For library code that will be reused across locales, prefer keys.
103
+
104
+ ## 3. Components — what exists and when to reach for it
105
+
106
+ All components come from the single package entry: `import { BSxxx } from '@g1cloud/bluesea'`. There is **no** subpath import. Types and helpers are exported from the same entry.
107
+
108
+ | Need | Component |
109
+ |---|---|
110
+ | Button / link / router-link | `BSButton` (buttonColor: default/blue/red/orange/green/gray; linkUrl, routePath) |
111
+ | Plain text input | `BSTextInput` (validation: required, minLength, maxLength, regExp, extraValidationRules) |
112
+ | Number / price / percent | `BSNumberInput`, `BSPriceInput`, `BSPercentInput` |
113
+ | Date / date range | `BSDateInput`, `BSDateRange`, `BSCalendar`, `BSCalendarRange`, `BSDateRangePresets` |
114
+ | Select | `BSSelect`, `BSMultiSelect`, `BSTreeSelect`, `BSTreeMultiSelect`, `BSPopupSelect`, `BSYesNoSelect` |
115
+ | Checkbox / radio | `BSCheckbox`, `BSCheckboxGroup`, `BSRadioButton`, `BSRadioButtonGroup`, `BSYesNoGroup` |
116
+ | Textarea / rich text / HTML editor / code editor | `BSTextArea`, `BSRichText`, `BSHtmlEditor`, `BSCodeEditor` |
117
+ | File / image upload | `BSFileUpload`, `BSImageUpload`, `BSMultiImageUpload`, `BSPositionedImageUpload` |
118
+ | Data grid | `BSGrid` + `BSGridControl` + `BSGridLookup` (see §6) |
119
+ | Layout | `BSCardLayout`, `BSListLayout`, `BSTabSheet`, `BSHorizontalLayoutResizer`, `BSVerticalLayoutResizer` |
120
+ | Tree | `BSTree`, `BSTreeControl` |
121
+ | Multi-language inputs | `BSMultiLangTextInput`, `BSMultiLangTextArea`, `BSMultiLangRichText`, `BSMultiLangHtmlEditor`, `BSMultiLangImageUpload` |
122
+ | Global entities | `BSNameInput`, `BSTelInput`, `BSAddressInput`, `BSLocaleSelect` |
123
+ | Popup / tooltip | `BSPopup`, `BSPopupButton`, plus `v-tooltip` directive |
124
+ | Frame shell | `DefaultHeader`, `DefaultBody` (BackOffice app chrome) |
125
+
126
+ Prop naming is consistent across inputs: `v-model` for the value, `name` (required for validation), `disabled`, `required`, `width` ('200px' by default), `placeholder`, `viewMode` (read-only display), `hideErrorMessage`. Date inputs add `dateFormat`, `timeZone`. Upload inputs take `StoredFile` / `StoredFile[]`.
127
+
128
+ Typical usage:
129
+
130
+ ```vue
131
+ <BSFormLabel label="이름" required />
132
+ <BSTextInput v-model="form.name" name="name" required :max-length="50" />
133
+
134
+ <BSFormLabel label="등급" />
135
+ <BSSelect v-model="form.grade" name="grade"
136
+ :items="grades" :label-provider="g => g.label" :key-provider="g => g.id" />
137
+
138
+ <BSButton :caption="{ key: 'btn.save' }" button-color="blue" @click="handleSave" />
139
+ ```
140
+
141
+ When you need a component that this table doesn't list, check `packages/bluesea/src/index.ts` for the full export list before assuming it doesn't exist. Use the demo pages under `packages/bluesea-demo/src/page/guide/BS*Guide.vue` as copy-paste-ready examples — they are the canonical "how do I use this component" reference.
142
+
143
+ ## 4. Modals and notifications
144
+
145
+ The modal plugin is *the* way to open overlays. `useModal()` gives you `openModal`, `openAlert`, `openYesNo`; each returns a `RegisteredModalItem` whose `modalHandle.close()` programmatically closes it. Modals are plain Vue components — pass props via `bind`, listen to events via `on`.
146
+
147
+ ```ts
148
+ import { useModal, showNotification, withLoading } from '@g1cloud/bluesea'
149
+
150
+ const modal = useModal()
151
+
152
+ modal.openAlert('알림', '저장되었습니다.')
153
+
154
+ modal.openYesNo('삭제', '정말 삭제할까요?',
155
+ async () => { await api.delete(id) },
156
+ () => { /* cancelled */ })
157
+
158
+ // custom component
159
+ const handle = modal.openModal({
160
+ component: EditUserModal,
161
+ style: { width: '600px', resizable: true },
162
+ bind: { userId: 123 },
163
+ on: { submit: (user) => { console.log(user); handle.modalHandle.close() } },
164
+ })
165
+ ```
166
+
167
+ Inside the modal component, call `useModalHandle()` to get the same handle (`handle.close()`, `setDefaultStyle()`, etc.).
168
+
169
+ Toast-style messages go through `showNotification(message, style, durationMs)` where `style` is `'info' | 'error'`. Longer-lived "alarm" overlays use `showAlarm(component, duration)`. For a global spinner during async work:
170
+
171
+ ```ts
172
+ await withLoading(async () => {
173
+ await api.save(form)
174
+ showNotification({ key: 'msg.saved' })
175
+ })
176
+ ```
177
+
178
+ ## 5. Validation (the most distinctive Bluesea pattern)
179
+
180
+ Every input that takes a `name` prop auto-registers a `FieldValidator` on its DOM element. A `FormValidator` walks the DOM of a given root element, collects every registered `FieldValidator`, and runs validation. This means you do **not** wire up validators manually per field — you just give each field a `name` and scope a `FormValidator` to your form root.
181
+
182
+ ```vue
183
+ <template>
184
+ <div ref="formEl">
185
+ <BSTextInput v-model="form.email" name="email" required
186
+ reg-exp="^[^@]+@[^@]+$"
187
+ :validation-message-reg-exp="{ key: 'err.email' }" />
188
+ <BSNumberInput v-model="form.age" name="age" :min-value="0" :max-value="150" />
189
+ <BSButton caption="저장" @click="save" />
190
+ </div>
191
+ </template>
192
+
193
+ <script setup lang="ts">
194
+ import { ref } from 'vue'
195
+ import { formValidator, ValidationFailedError, isValidationFailedError, showNotification } from '@g1cloud/bluesea'
196
+
197
+ const formEl = ref<HTMLElement>()
198
+ const form = ref({ email: '', age: 0 })
199
+ const validator = formValidator({
200
+ element: formEl,
201
+ rules: [
202
+ async () => form.value.age < 18
203
+ ? [{ code: 'tooYoung', message: { key: 'err.adultOnly' } }]
204
+ : undefined,
205
+ ],
206
+ })
207
+
208
+ async function save() {
209
+ try {
210
+ await validator.validate() // throws ValidationFailedError on failure
211
+ await api.save(form.value)
212
+ showNotification({ key: 'msg.saved' })
213
+ } catch (e) {
214
+ if (isValidationFailedError(e)) {
215
+ showNotification({ key: 'err.validationFailed' }, 'error')
216
+ return
217
+ }
218
+ throw e
219
+ }
220
+ }
221
+ </script>
222
+ ```
223
+
224
+ Built-in rules on inputs: `required`, `minLength` / `maxLength`, `regExp`, `minValue` / `maxValue`, `validation-message-*` overrides for each rule. For custom rules, pass `:extra-validation-rules` — an array of `(value, phase, fieldContext) => ValidationError[] | undefined`. `phase` is `'input' | 'change' | 'blur' | 'form'`, so you can defer expensive async checks to `'blur'` or `'form'` only.
225
+
226
+ `ValidationError` is `{ code: string; message: MultiLangText }`. `ValidationFailedError.errors` is an array that includes the field `name`, so you can highlight specific fields if needed.
227
+
228
+ ## 6. BSGrid
229
+
230
+ BSGrid is the workhorse for data tables. Prefer the `createPageGridHandler` factory when you need paging + server-side sorting/filtering — it wires `grid`, `gridEventListener`, `control`, `lookup`, `lookupEventListener` together for you. For simple client-side tables, use `BSGrid` directly with `:columns` + `:data`.
231
+
232
+ ```vue
233
+ <BSGridLookup v-bind="gridHandler.lookup" v-on="gridHandler.lookupEventListener" :config="lookupConfig" />
234
+ <BSGridControl v-bind="gridHandler.control" v-on="gridHandler.controlEventListener" />
235
+ <BSGrid v-bind="gridHandler.grid" v-on="gridHandler.gridEventListener">
236
+ <template #name="{ row }"><strong>{{ row.name }}</strong></template>
237
+ <template #status="{ row }"><span class="badge">{{ row.status }}</span></template>
238
+ <template #price.edit="{ row }"><BSNumberInput v-model="row.price" name="price" /></template>
239
+ </BSGrid>
240
+
241
+ <script setup lang="ts">
242
+ import { createPageGridHandler, type Column, type PaginatedList } from '@g1cloud/bluesea'
243
+
244
+ type User = { id: number; name: string; email: string; status: string; price?: number }
245
+
246
+ const columns: Column<User>[] = [
247
+ { propertyId: 'name', caption: { key: 'col.name' }, width: 160, sortable: true },
248
+ { propertyId: 'email', caption: { key: 'col.email' }, width: 220 },
249
+ { propertyId: 'price', caption: '가격', width: 120, cellType: 'NUMBER' },
250
+ { propertyId: 'status', caption: '상태', width: 100 },
251
+ ]
252
+
253
+ const gridHandler = createPageGridHandler<User>({
254
+ gridId: 'user-list', // used by gridPreferenceStore to remember column widths
255
+ getRowKey: (u) => String(u.id),
256
+ editable: true,
257
+ getGridData: async (param): Promise<PaginatedList<User>> => api.users.search(param),
258
+ defaultSorts: [{ property: 'name', direction: 'ASC' }],
259
+ limit: 100,
260
+ limitItems: [100, 300, 500],
261
+ })
262
+ gridHandler.grid.columnSettings = columns.map(c => ({ propertyId: c.propertyId, hidden: false }))
263
+ await gridHandler.loadGridData()
264
+ </script>
265
+ ```
266
+
267
+ Cell customisation uses named slots: `#<propertyId>` for display, `#<propertyId>.edit` for the editor when `editable: true` and the row is in `editingRows`. Use `cellType` to get built-in formatting (`'TEXT' | 'NUMBER' | 'DATE' | 'PERCENTAGE' | 'BOOL' | 'MULTI_LANG_STRING' | 'NAME' | 'TEL' | 'ADDRESS' | 'MONEY'`). For date columns also set `dateFormat: 'DAY' | 'MINUTE' | 'SECOND'` or a dayjs format string.
268
+
269
+ For deeper grid topics (inline add/remove, filters, Excel export, fixed columns, extensions) read the `BSGrid` section in `references/grid.md` and the guide page `packages/bluesea-demo/src/page/guide/BSGridGuide.vue`.
270
+
271
+ ## 7. SavePoint (inline-edit modified state)
272
+
273
+ SavePoint lets a form remember its "saved" state and surface a `modified` flag per field. It is automatically injected by `BSTabSheet` and available via `useSavePoint()` — most inputs (`BSTextInput`, etc.) register themselves so the "modified" dot appears without any extra code. You typically only interact with it to `set()` after a successful save or `rollback()` on cancel:
274
+
275
+ ```ts
276
+ import { useSavePoint } from '@g1cloud/bluesea'
277
+ const savePoint = useSavePoint()
278
+
279
+ async function save() { await api.save(form.value); savePoint?.set() }
280
+ function cancel() { savePoint?.rollback() }
281
+ ```
282
+
283
+ Pass `:ignore-save-point="true"` on an input to opt it out of modified-tracking.
284
+
285
+ ## 8. Directives
286
+
287
+ - `v-t="multiLangText"` — set element textContent from a MultiLangText.
288
+ - `v-t.placeholder="multiLangText"` — set the `placeholder` attribute (or any attribute via the modifier name).
289
+ - `v-tooltip="{ content: multiLangText }"` — show on hover.
290
+ - `v-click-outside="handler"` — fire when the user clicks outside the element.
291
+ - `v-focus-on-load`, `v-focus-jump`, `v-focus-loop` — focus management helpers for forms.
292
+
293
+ Register `vT` globally (`.directive('t', vT)`). The others can be imported and used ad-hoc.
294
+
295
+ ## 9. DefaultFrame (BackOffice app shell)
296
+
297
+ If the app is a BackOffice application, use `DefaultHeader` + `DefaultBody` for sidebar + tab management. Build a `FrameModel` with your menu tree and tab controller. This is an opinionated shell — skip it for standalone screens. Detailed wiring lives in `references/frame.md` and the `DefaultFrameGuide` demo page.
298
+
299
+ ## When to write code vs read the source
300
+
301
+ - For a component's exact prop set, **read the `.vue` file** under `packages/bluesea/src/component/`. The `defineProps<{...}>()` block is authoritative; what a docs page says may lag behind.
302
+ - For a real-world wiring example, **open the matching `BS*Guide.vue`** in `packages/bluesea-demo/src/page/guide/`. They cover the common scenarios end-to-end.
303
+ - For types (`MultiLangText`, `StoredFile`, `Money`, `Filter`, `Sort`, `PaginatedList`, `Column`, etc.), the model files in `packages/bluesea/src/model/` and `packages/bluesea/src/component/grid/GridModel.ts` are short and worth reading directly.
304
+
305
+ ## Reference files
306
+
307
+ - `references/components.md` — fuller prop reference for the most-used components, grouped by category.
308
+ - `references/grid.md` — BSGrid deep-dive: PageGridHandler, filters, inline editing, extensions, Excel export.
309
+ - `references/validation.md` — custom field rules, form-level rules, async validation, and error mapping.
310
+ - `references/i18n.md` — i18n setup, MultiLangText patterns, and locale switching.
311
+
312
+ Load a reference file only when the user's question clearly touches that area; keep this file as the default map.
@@ -0,0 +1,189 @@
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. Paths below are relative to `packages/bluesea/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` — font-icon ligature
33
+ - `disabled: boolean`
34
+ - `linkUrl: string`, `linkTarget: string` — renders `<a>`
35
+ - `routePath: string` — renders `<router-link :to>`
36
+ - `tooltip: MultiLangText`
37
+
38
+ ### BSFormLabel (`basic/BSFormLabel.vue`)
39
+ - `label: MultiLangText`
40
+ - `required: boolean`
41
+ - `tooltip: MultiLangText`
42
+
43
+ ### BSLink, BSImage, BSPopup, BSPopupButton, BSProgressBar, BSLoadingIcon, BSDate, BSCalendar, BSCalendarRange, BSPageNavigation, BSConsole
44
+ Read the individual files — they are short and each has a targeted purpose.
45
+
46
+ ## Text / number / money
47
+
48
+ ### BSTextInput (`input/BSTextInput.vue`)
49
+ `v-model` is `string`.
50
+
51
+ - `inputType: 'text' | 'password'`
52
+ - `maxlength: number` — HTML attribute
53
+ - `minLength: number`, `maxLength: number` — validation rules
54
+ - `regExp: string`
55
+ - `trimValue: boolean` (default `true`) — trims before emit
56
+ - `prefix`, `suffix: MultiLangText | MultiLangText[] | PrefixSuffix | PrefixSuffix[]` — decoration in the input border
57
+ - `autocomplete: string`
58
+
59
+ ### BSNumberInput, BSPriceInput, BSPercentInput (`input/BSNumberInput.vue`, etc.)
60
+ `v-model` is `number | undefined`.
61
+
62
+ - `minValue`, `maxValue: number`
63
+ - `decimalPlace: number`
64
+ - `thousandSeparator: boolean`
65
+ - `unit: MultiLangText` — e.g. `'원'`, `'%'`
66
+ - `currencyCode: CurrencyCode` — on `BSPriceInput`; falls back to `blueseaConfig.defaultCurrencyCode`
67
+
68
+ ### BSTextArea (`input/BSTextArea.vue`)
69
+ `v-model` is `string`. Adds `rows: number`, `autoResize: boolean`.
70
+
71
+ ### BSColorInput (`input/BSColorInput.vue`)
72
+ `v-model` is a hex string like `'#RRGGBB'`.
73
+
74
+ ## Date / time
75
+
76
+ ### BSDateInput, BSDateRange (`input/BSDateInput.vue`, `BSDateRange.vue`)
77
+ `v-model` is an ISO string (`dayjs`-parseable).
78
+
79
+ - `dateFormat: string` — dayjs format; falls back to `blueseaConfig.dateFormat`
80
+ - `timeZone: TimeZone` — falls back to `blueseaConfig.timeZone`
81
+ - `resolution: DateResolution` — `'DAY' | 'HOUR' | 'MINUTE' | 'MINUTE_10' | 'MINUTE_30' | 'SECOND'`
82
+ - `minValue`, `maxValue: string` — ISO bounds
83
+
84
+ Range version emits two values via `v-model:from` / `v-model:to`, or via a `DateRange` object depending on flavor; check the component.
85
+
86
+ ### BSCalendar, BSCalendarRange
87
+ Embedded calendars; useful inside custom popups.
88
+
89
+ ## Selection
90
+
91
+ ### BSSelect (`input/BSSelect.vue`)
92
+ Generic: `<BSSelect :items="items" :key-provider="..." :label-provider="..." />`.
93
+
94
+ - `items: T[]`
95
+ - `keyProvider: (item: T) => string | undefined`
96
+ - `labelProvider: (item: T) => MultiLangText | undefined`
97
+ - `iconProvider`, `tooltipProvider`, `enabledItemProvider`
98
+ - `allowEmpty: boolean`, `emptyLabel: MultiLangText`
99
+ - `filterable: boolean` — show search box in popup
100
+
101
+ ### BSMultiSelect, BSTreeSelect, BSTreeMultiSelect, BSPopupSelect
102
+ Same general shape; multi-select uses `Set<T>` or `T[]`.
103
+
104
+ ### BSCheckbox, BSCheckboxGroup, BSRadioButton, BSRadioButtonGroup, BSYesNoSelect, BSYesNoGroup
105
+ Check the `defineProps` block; `items` + `keyProvider` + `labelProvider` pattern is consistent.
106
+
107
+ ## Upload
108
+
109
+ ### BSImageUpload (`input/BSImageUpload.vue`), BSFileUpload, BSMultiImageUpload, BSPositionedImageUpload
110
+ `v-model` is `StoredFile | StoredFile[]`.
111
+
112
+ Key props:
113
+ - `maxFileSize: number` (falls back to `blueseaConfig.maxFileSize`)
114
+ - `accept: string` — mime filter
115
+ - `preloadOverlay: boolean` — preload preview overlay image (recently renamed from `preloadPreview`)
116
+ - 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.
117
+
118
+ ## Multi-language inputs
119
+
120
+ `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.
121
+
122
+ ## Rich text / code
123
+
124
+ ### BSRichText (`richtext/BSRichText.vue`)
125
+ TipTap-based WYSIWYG.
126
+
127
+ - `v-model` — HTML string
128
+ - `toolButtons: ToolButton[]` — subset of `'Heading' | 'FontSize' | 'FontColor' | 'FontStyle' | 'TextAlign' | 'ListItem' | 'Link' | 'Table' | 'Image' | 'Video' | 'Youtube'`
129
+ - `imageInsertModal`, `videoInsertModal: Component` — override default modals
130
+
131
+ ### BSHtmlEditor (`input/BSHtmlEditor.vue`)
132
+ Split view (source + preview) for raw HTML.
133
+
134
+ ### BSCodeEditor (`input/BSCodeEditor.vue`)
135
+ CodeMirror 6 — props `language`, `theme`, `readonly`.
136
+
137
+ ## Layout
138
+
139
+ ### BSCardLayout (`layout/BSCardLayout.vue`)
140
+ Container with header/footer slots.
141
+
142
+ ### BSListLayout + BSListControl (`layout/BSListLayout.vue`)
143
+ Master-detail layout with resizable panes.
144
+
145
+ ### BSTabSheet (`layout/BSTabSheet.vue`)
146
+ Manages tabs, provides a SavePoint to each tab. Config toggles `blockLeavingModifiedTab` / `confirmLeavingModifiedTab` via global `componentConfig.tabSheet`.
147
+
148
+ ### BSHorizontalLayoutResizer, BSVerticalLayoutResizer
149
+ Drag-to-resize between two DOM siblings.
150
+
151
+ ## Tree
152
+
153
+ ### BSTree (`tree/BSTree.vue`)
154
+ - `items: T[]`, `childrenProvider: (item: T) => T[] | undefined`
155
+ - `keyProvider`, `labelProvider`, `iconProvider`
156
+ - `draggable: boolean`
157
+ - `selection: Set<T>` via `v-model:selected`
158
+ - Scoped slot `#item="{ item }"` to customise rendering
159
+
160
+ ### BSTreeControl
161
+ Toolbar for BSTree (add / remove / expand-all).
162
+
163
+ ## Global entity inputs
164
+
165
+ ### BSNameInput, BSTelInput, BSAddressInput
166
+ Bind to `Name`, `Tel`, `Address` models from `@/model/CommonTypes`. Layout (number of input boxes, widths, line breaks) comes from `componentConfig.nameInput / telInput / addressInput`, so configure them once globally.
167
+
168
+ ### BSLocaleSelect
169
+ Dropdown that switches `blueseaConfig.currentLocale`. Handy for a language toggle in the header.
170
+
171
+ ## Popup
172
+
173
+ ### BSPopup, BSPopupButton, BSSelectPopup, BSDateInputPopup, BSDateRangeInputPopup
174
+ 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.
175
+
176
+ ## Grid
177
+
178
+ See `references/grid.md`. Top-level exports: `BSGrid`, `BSGridLookup`, `BSGridControl`, plus types `Column`, `EditingRows`, `PageGridHandler`, `createPageGridHandler`.
179
+
180
+ ## Notification / modal / context menu containers
181
+
182
+ - `BSModalContainer` — mount once; the modal plugin renders into it.
183
+ - `BSNotificationContainer` — mount once; drives `showNotification`, `showAlarm`, `showLoading`.
184
+ - `BSContextMenuContainer` — mount once; the context-menu plugin renders into it.
185
+
186
+ ## Frame
187
+
188
+ - `DefaultHeader`, `DefaultBody` — BackOffice app shell.
189
+ - `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 `PageGridHandler` 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/bluesea/src/component/grid/GridModel.ts`:
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 `configureBluesea`, 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 `@/component/grid/extension/gridExcelDownloadExtension`) 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.