@greendrake/ui-data 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Eugene Greendrake
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,334 @@
1
+ # @greendrake/ui-data
2
+
3
+ Data-heavy dashboard components on top of `@greendrake/ui`: a virtualised `InfiniteScrollTable`, the `MasterDetail` split and the `CrudPanel` shell built on it, the `useRecordEditor` diff-and-patch editing convention, and an admin-CRUD kit of loader adapters, sort state, detail/action composables, formatters and `DescriptionList`. Ships as TypeScript + SFC source; the consuming Vite app compiles it.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ bun add @greendrake/ui-data vue
9
+ ```
10
+
11
+ `vue` is the peer dependency; `@greendrake/ui` and `@greendrake/scss-kit` come in as dependencies, so `@greendrake/ui`'s own peer `vue-router` must be installed as well.
12
+
13
+ No build step, no `.d.ts`: `exports` points at `src/main.ts` and the consumer's Vite + `vue-tsc` toolchain compiles the SFCs, whose generics (`InfiniteScrollTable<T>`, `CrudPanel<T>`) are written against the `vueCompilerOptions` of `@greendrake/dev-config/tsconfig/vue.json`. A consumer's tsconfig extends it:
14
+
15
+ ```json
16
+ {
17
+ "extends": "@greendrake/dev-config/tsconfig/vue.json",
18
+ "include": ["src"]
19
+ }
20
+ ```
21
+
22
+ ## Styling contract
23
+
24
+ As for `@greendrake/ui`: the app loads `@greendrake/theme` and defines the app-provided custom properties its README lists. The table and panel additionally read the optional `--cell-padding`, `--list-row-padding`, `--row-border-bottom`, `--row-select-color` and `--row-select-color-hover` from that list, each with a fallback.
25
+
26
+ Layout: `InfiniteScrollTable` measures its own container to decide how many rows to render, so its height must flow top-down. Place it in a flex column whose items have `min-height: 0`, or pass `visibleHeight`; a content-driven height makes the measurement loop until every row is rendered.
27
+
28
+ ## InfiniteScrollTable
29
+
30
+ Virtualised table: only the rows that fit the measured viewport (plus two clipped ones) are in the DOM, scrolling is pixel-continuous through a custom scrollbar, the wheel, touch panning with inertia, and the keyboard when `selectable`. Rows arrive through the loader in `bufferSize` windows around the viewport; when a single response holds the whole dataset (`data.length >= total`) the table keeps it in memory and serves text-filter refinements and sorting locally without further calls. Columns are resizable by dragging header edges.
31
+
32
+ ### Loader contract
33
+
34
+ ```ts
35
+ interface LoaderParams {
36
+ offset: number
37
+ limit: number
38
+ [key: string]: string | number // every non-empty `filters` entry
39
+ }
40
+ interface LoaderResponse<T> {
41
+ data: T[]
42
+ total: number
43
+ }
44
+ type LoaderFunction<T> = (params: LoaderParams, loading: Ref<boolean>, background: boolean) => Promise<LoaderResponse<T>>
45
+ ```
46
+
47
+ `loading` is the table's spinner flag, handed to the loader to set around its call. The spinner also shows while the viewport overlaps rows not yet buffered, so a loader that never touches `loading` still gets one for gaps.
48
+
49
+ `background` is true when nobody asked for the load — a live binding re-reading after a server push or a dropped connection (`refresh(true)`). What that means for the request is the loader's to decide; with `@greendrake/vue-api` it is `backgroundCall(background)`, a call that shows no spinner and raises no error prompt.
50
+
51
+ `T` must satisfy `Record<string, unknown>`, so declare row types as type aliases (`type User = { … }`), not interfaces: an interface carries no implicit index signature and is rejected where the table's generic is inferred.
52
+
53
+ ```ts
54
+ interface Filters {
55
+ [key: string]: string | null | undefined
56
+ sort_by?: string
57
+ sort_dir?: 'asc' | 'desc'
58
+ }
59
+ ```
60
+
61
+ A `filters` change re-fetches from offset 0 unless the whole dataset is in memory and the change is a refinement of the text filter (`textFilterKey`) or a sort change, in which case it is applied locally over `localSearchFields`.
62
+
63
+ ### Props
64
+
65
+ | Prop | Type | Default |
66
+ | --- | --- | --- |
67
+ | `loader` | `LoaderFunction<T>` | required |
68
+ | `columns` | `ColumnDefinition<T>[]` | required |
69
+ | `rowKey` | `RowKey<T>` — field name, or `(row) => string \| number` for composite keys | `'id'` |
70
+ | `rowHeight` | px | 45 |
71
+ | `visibleHeight` | px, or `null` to measure the container | `null` |
72
+ | `filters` | `Filters` | `{}` |
73
+ | `textFilterKey` | the filter key treated as the text search | `'q'` |
74
+ | `localSearchFields` | fields the in-memory text filter matches; `false` sends every text-filter change to the loader | `['title']` |
75
+ | `selectable` | pointer/keyboard row selection (wrapper gets `tabindex="0"`) | `false` |
76
+ | `rowDraggable` | rows are `draggable`, drop targets are tracked | `false` |
77
+ | `hideHeader` | | `false` |
78
+ | `onRowClick` | `(record: T) => void` | — |
79
+ | `bufferSize` | rows fetched per request | 1000 |
80
+ | `renderAll` | render every buffered row (test affordance for datasets within one buffer) | `false` |
81
+
82
+ ```ts
83
+ interface ColumnDefinition<T> {
84
+ field: string
85
+ label: string
86
+ className?: string // on th and td
87
+ cellClass?: (value: unknown, record: T) => string | undefined
88
+ sortable?: boolean
89
+ sortType?: 'string' | 'number' | 'date' // for local sorting; inferred from the data otherwise
90
+ sortIcon?: string // rendered after the label
91
+ onHeaderClick?: () => void
92
+ onCellClick?: (value: unknown, record: T) => void
93
+ formatter?: (value: unknown, record: T) => string
94
+ wrap?: boolean // multi-line cell instead of single-line ellipsis
95
+ component?: Component // custom cell renderer
96
+ componentProps?: (record: T) => Record<string, unknown>
97
+ }
98
+ ```
99
+
100
+ Selection: mouse click selects, Shift extends a range, Ctrl/Cmd toggles; touch taps select; Arrow keys move the cursor (Shift extends), Ctrl/Cmd+A selects all.
101
+
102
+ ### Emits
103
+
104
+ - `range-changed(range: VisibleRange)` — `{ start, end, total }`, the 1-based window of fully visible rows; emitted immediately and on every change.
105
+ - `rowactivate(record: T)` — double-click.
106
+ - `rowdragstart(record: T, event: DragEvent)`, `rowdrop(event: DragEvent, target: DropTarget<T> | null)` — with `rowDraggable`; `DropTarget` is `{ record, position: 'before' | 'after' }`, the hovered row carrying a `drop-before` / `drop-after` class meanwhile.
107
+
108
+ Slot `empty` replaces the `messages.nothingFound` row.
109
+
110
+ ### Exposed
111
+
112
+ Type a template ref as `ref<InfiniteScrollTableExposed<Row>>()`; a generic SFC instance has no nameable `InstanceType`.
113
+
114
+ ```ts
115
+ interface InfiniteScrollTableExposed<T> {
116
+ refresh: (background?: boolean) => Promise<void> // re-fetch around the current position; the promise settles with the load
117
+ patchRow: (keyValue: string | number, data: Partial<T>) => boolean
118
+ addRow: (data: T, position?: RowPosition) => void // 'start' (default) | 'end' | { after: key } | { before: key }
119
+ removeRow: (keyValue: string | number) => void
120
+ removeRows: (keyValues: (string | number)[]) => void
121
+ resort: () => void // re-apply local filter/sort in place (after patching a sorted field)
122
+ getRow: (keyValue: string | number) => T | undefined
123
+ willFilterLocally: (value: string | undefined) => boolean // whether a text-filter change to `value` stays in memory
124
+ selected: Set<string | number> | undefined // the selection when `selectable`
125
+ }
126
+ ```
127
+
128
+ Row surgery (`patchRow`, `addRow`, `removeRows`) is the way to reflect a single-record change; `refresh()` after one is a wasted round trip.
129
+
130
+ ### Example
131
+
132
+ ```vue
133
+ <script setup lang="ts">
134
+ import { ref, computed } from 'vue'
135
+ import { InfiniteScrollTable, useSortState, fmtDate, type LoaderFunction, type Filters, type InfiniteScrollTableExposed } from '@greendrake/ui-data'
136
+
137
+ type User = { id: string; email: string; created_at: string }
138
+
139
+ const sort = useSortState('created_at')
140
+ const columns = computed(() => [sort.column<User>('email', 'Email'), sort.column<User>('created_at', 'Created', { formatter: fmtDate })])
141
+ const filters = computed<Filters>(() => sort.sortFilters.value)
142
+
143
+ // listUsers is the app's own call, resolving to { data, total }
144
+ const loader: LoaderFunction<User> = async (params, loading) => {
145
+ loading.value = true
146
+ try {
147
+ return await listUsers(params)
148
+ } finally {
149
+ loading.value = false
150
+ }
151
+ }
152
+ const table = ref<InfiniteScrollTableExposed<User>>()
153
+ </script>
154
+ <template>
155
+ <InfiniteScrollTable ref="table" :loader="loader" :columns="columns" :filters="filters" row-key="id" selectable />
156
+ </template>
157
+ ```
158
+
159
+ ## MasterDetail
160
+
161
+ Resizable master pane on the left, tabbed detail panes on the right (a `TabBar` from `@greendrake/ui` with closable tabs). Props: `initialWidth` (default 150), `minWidth` (default 100), `fullWidthUntilTab` (default `false`; the master spans the whole width until a tab opens). Emits `tab-open`, `tab-close`, `tab-activate`, each with the `TabDef`. Slots: `master` receives `{ openTab }`; `detail` receives `{ tab, isActive }` and is rendered once per open tab, inactive panes kept alive with `v-show`. Exposes `openTab(id, label?)` (opens or activates), `closeTab(id)`, `activateTab(id)`, `updateTab(id, newId?, newLabel?)`, `tabs`, `activeTab`, `activeTabId`.
162
+
163
+ ## CrudPanel
164
+
165
+ Master/detail CRUD shell: a controls bar (search field, optional Create button, refresh, visible-range readout, optional Delete button) over an `InfiniteScrollTable` in the master pane, and detail tabs opened by double-clicking a row. A panel without a `detail` slot is table-only: rows select (for deletion) and never open a pane.
166
+
167
+ | Prop | Type | Default |
168
+ | --- | --- | --- |
169
+ | `columns` | `ColumnDefinition<T>[]` | required |
170
+ | `loader` | `LoaderFunction<T>` | required |
171
+ | `rowKey` | `RowKey<T>` | `'id'` |
172
+ | `rowHeight` | px | 28 |
173
+ | `filters` | `Filters`, merged with the search box's `q` | `{}` |
174
+ | `initialWidth`, `minWidth`, `fullWidthUntilTab` | forwarded to `MasterDetail` | 200, 120, `true` |
175
+ | `showRange` | render the `start-end/total` readout | `true` |
176
+ | `localSearchFields` | `string[] \| false \| null`; `null` keeps the table's default | `null` |
177
+ | `creatable` | Create button, opens a `new:<n>` tab | `false` |
178
+ | `newTabLabel` | label of that tab | `messages.newTab` |
179
+ | `onDelete` | `(keys: Key[]) => Promise<unknown>`; enables the Delete button for the selection | `null` |
180
+ | `deleteConfirmMessage` | when set, deletion asks first in a `Modal` | `null` |
181
+ | `keyExists` | `(key: Key) => boolean`, backs the `isDupe` slot prop | `null` |
182
+
183
+ The search field's debounce is `localFilterDebounce` over the table: swift when the change resolves in memory, longer when it triggers a fetch. A single column hides the table header.
184
+
185
+ Slots: `controls-left` (after the search field and Create), `controls-middle` (`{ range: VisibleRange }`), `controls-right` (after Delete; `{ selected: Set<Key> | undefined }`), `detail` (`{ tab: TabDef, isNew: boolean, record: T | undefined, isDupe: (original: Key, candidate: Key) => boolean }`). `record` is the live table row for the tab's key (`undefined` for a `new:` tab or a row the table has not seen); `isDupe` flags a candidate key that differs from `original` and already exists, for rename and create forms.
186
+
187
+ Exposed (`CrudPanelExposed<T>`; type the ref as `ref<CrudPanelExposed<Row>>()`): `openTab`, `closeTab`, `updateTab`, `removeRows(keys)` (also clears the selection), `addRow`, `patchRow`, `getRow`, `refresh`, `applySaved(key, record, tabId?)`, and readonly `selected`, `range`, `deleting`.
188
+
189
+ `applySaved` is the post-save hook of the record-edit convention below: it patches the row in place when the key exists, otherwise adds the row and, given the originating `new:` tab's id, renames that tab to the persisted key so the `record` slot prop resolves and the next save updates instead of creates.
190
+
191
+ ```vue
192
+ <script setup lang="ts">
193
+ import { ref, computed } from 'vue'
194
+ import { CrudPanel, listLoader, useSortState, type CrudPanelExposed } from '@greendrake/ui-data'
195
+ import HostEditor from './HostEditor.vue'
196
+
197
+ type Host = { name: string; address: string; enabled: boolean }
198
+
199
+ const sort = useSortState('name', 'asc')
200
+ const columns = computed(() => [sort.column<Host>('name', 'Name')])
201
+ // listHosts / deleteHosts are the app's own calls; listHosts resolves to { data, total }
202
+ const loader = listLoader<Host>(args => listHosts(args), sort)
203
+ const crud = ref<CrudPanelExposed<Host>>()
204
+ </script>
205
+ <template>
206
+ <CrudPanel ref="crud" :columns="columns" :loader="loader" row-key="name" creatable :on-delete="keys => deleteHosts(keys)" delete-confirm-message="Delete the selected hosts?">
207
+ <template #detail="{ tab, isNew, record, isDupe }">
208
+ <HostEditor :record="record" :is-new="isNew" :is-dupe="isDupe" @saved="(key, host) => crud?.applySaved(key, host, tab.id)" />
209
+ </template>
210
+ </CrudPanel>
211
+ </template>
212
+ ```
213
+
214
+ ## Record editing
215
+
216
+ `useRecordEditor<T>(options: RecordEditorOptions<T>): RecordEditor<T>` implements the convention `CrudPanel` and `patchRow` expect:
217
+
218
+ 1. The editor receives the full record (from a table row, a fetch, etc.).
219
+ 2. It keeps `data` (the state the backend has acknowledged) and `workData` (a reactive working copy bound to inputs).
220
+ 3. On save only the fields that differ between the two are sent.
221
+ 4. `save(patch)` resolves to the full updated record, which overwrites both objects — server-side changes (timestamps, derived fields) are acknowledged and the next diff is correct.
222
+ 5. The editor emits `('saved', key, record)`; the host wires it to `CrudPanel.applySaved`.
223
+
224
+ ```ts
225
+ interface RecordEditorOptions<T> {
226
+ initial: T // deep-cloned into data and workData
227
+ save: (patch: Partial<T>) => Promise<T>
228
+ fields?: readonly (keyof T)[] // editable/diffed keys; default: the keys of `initial`
229
+ normalise?: (record: T) => T // canonicalise initial and every response (e.g. undefined vs null)
230
+ }
231
+ interface RecordEditor<T> {
232
+ data: T
233
+ workData: T
234
+ changes: ComputedRef<Partial<T> | null>
235
+ dirty: ComputedRef<boolean>
236
+ saving: Ref<boolean>
237
+ submit: () => Promise<T | null> // null when nothing changed
238
+ reset: () => void // workData ← data
239
+ applyResponse: (updated: T) => void // acknowledge a server update outside submit()
240
+ }
241
+ ```
242
+
243
+ Equality is by canonical JSON (strings, numbers, booleans, arrays, plain objects). Validation, HTTP and multi-step flows stay with the caller: gate `submit()` behind checks, or build around `changes` / `applyResponse`.
244
+
245
+ ```vue
246
+ <script setup lang="ts">
247
+ import { useRecordEditor } from '@greendrake/ui-data'
248
+ import { Checkbox, Field, TextField, AsyncAction } from '@greendrake/ui'
249
+
250
+ type Host = { name: string; address: string; enabled: boolean }
251
+
252
+ const props = defineProps<{ record: Host }>()
253
+ const emit = defineEmits<{ saved: [key: string, record: Host] }>()
254
+
255
+ // updateHost is the app's own call, resolving to the full updated Host
256
+ const { workData, dirty, submit } = useRecordEditor<Host>({
257
+ initial: props.record,
258
+ fields: ['address', 'enabled'],
259
+ save: patch => updateHost(props.record.name, patch)
260
+ })
261
+ const save = async (): Promise<void> => {
262
+ const updated = await submit()
263
+ if (updated) emit('saved', updated.name, updated)
264
+ }
265
+ </script>
266
+ <template>
267
+ <Field label="Address">
268
+ <TextField v-model="workData.address" />
269
+ </Field>
270
+ <Checkbox v-model="workData.enabled" label="Enabled" />
271
+ <AsyncAction :promise-maker="save" :is-dirty="dirty" is-valid label-dirty="Save" label-done="Saved" />
272
+ </template>
273
+ ```
274
+
275
+ ## Admin-CRUD kit
276
+
277
+ ### listLoader
278
+
279
+ `listLoader<T>(fetch: (args: PageArgs, background: boolean) => Promise<LoaderResponse<T>>, sort?: SortState)` adapts a paged list call to the table's `LoaderFunction<T>`:
280
+
281
+ ```ts
282
+ type PageArgs = {
283
+ q?: string
284
+ offset?: number
285
+ limit?: number
286
+ sort_by?: string
287
+ sort_dir?: string
288
+ }
289
+ ```
290
+
291
+ Without `sort` the call receives the bare `offset`/`limit` window; with it, `q` (the text filter) and the current `sort_by`/`sort_dir` as well — for list methods with server-side search and sort. Extra filter fields ride the `fetch` closure, and so does the `background` flag, handed on verbatim: `listLoader((args, background) => listThreads({ ...args, type }, background))`. The returned function ignores the `loading` ref; the table's gap detection drives the spinner.
292
+
293
+ ### useSortState
294
+
295
+ `useSortState(initialBy: string, initialDir: SortDir = 'desc', defaultDir: SortDir | ((field: string) => SortDir) = 'asc'): SortState`
296
+
297
+ ```ts
298
+ interface SortState {
299
+ sortBy: Ref<string>
300
+ sortDir: Ref<SortDir> // 'asc' | 'desc'
301
+ toggleSort: (field: string) => void // same field flips; a new field takes over with defaultDir
302
+ sortIcon: (field: string) => string | undefined // '▲' / '▼' for the active field
303
+ sortFilters: ComputedRef<{ sort_by: string; sort_dir: SortDir }> // spread into Filters
304
+ column: <T>(field: string, label: string, options?: ColumnOptions<T>) => ColumnDefinition<T>
305
+ }
306
+ ```
307
+
308
+ `column` builds a `ColumnDefinition` whose header click toggles the sort and whose icon reflects it. `ColumnOptions<T>` is `ColumnDefinition<T>` minus `field`/`label`/`sortable`/`sortIcon`/`onHeaderClick`, plus `sortKey?: string | false` — the field sent to the backend when it differs from `field`, or `false` for an unsortable column. Build the columns array inside a `computed` so icons track state.
309
+
310
+ ### localFilterDebounce
311
+
312
+ `localFilterDebounce(table: Ref<LocalFilterable | undefined>): (value: string | undefined) => number` — a `SearchField` `debounce` resolver: 250ms when the table reports it can serve the change from memory (`willFilterLocally`), 600ms otherwise (and before the table mounts). `LocalFilterable` is the `{ willFilterLocally }` slice of `InfiniteScrollTableExposed`.
313
+
314
+ ### useDetailLoader
315
+
316
+ `useDetailLoader<T, Id>(id: WatchSource<Id>, fetcher: (id: Id) => Promise<T>): DetailLoader<T>` — the detail-pane fetch skeleton: loads immediately, reloads whenever `id` changes, discards responses that arrive after the id moved on. Returns `{ data: Ref<T | null>, loading, error: Ref<string | null>, reload(), mutate(fn: (cur: T) => void) }`. A rejection's message lands in `error` for in-pane display, so pass a fetcher that does not report errors globally itself. `mutate` folds a known local change into the loaded value without a re-fetch.
317
+
318
+ ### useAsyncAction
319
+
320
+ `useAsyncAction<Args extends unknown[]>(fn: (...args: Args) => Promise<string | void>): AsyncAction<Args>` — in-flight/error/notice bookkeeping for an action button. Returns `{ acting, error, notice, run(...args) }`: `run` ignores re-entry while acting, clears `error`/`notice`, captures a rejection's message in `error`, and stores a string result as the success `notice`. Buttons sharing one in-flight state parameterise `fn` and pass the variant to `run`.
321
+
322
+ ### errorMessage
323
+
324
+ `errorMessage(e: unknown): string` — `e.message` for an `Error`, `String(e)` otherwise.
325
+
326
+ ### Formatters
327
+
328
+ - `fmtDate(v: unknown): string` — `'YYYY-MM-DD HH:MM:SSZ'` in UTC from an ISO/`'YYYY-MM-DD HH:MM:SS'` string (a zoneless string is taken as UTC), a number or a `Date`; `''` for `null`/`undefined`/`''`; throws on other types.
329
+ - `fmtBool(v: unknown): string` — `'✓'` or `''`, for table cells.
330
+ - `fmtYesNo(v: unknown): string` — `'yes'` / `'no'`, for detail prose.
331
+
332
+ ### DescriptionList
333
+
334
+ Label/value grid for detail panes. Prop `items: (DescriptionItem | false | null | undefined)[]` — falsy entries are skipped, so conditional rows inline as `cond && { … }`. `DescriptionItem` is `{ label: string; value: string | number | null | undefined; code?: boolean }`; `code` renders the value in `<code>`. Values arrive display-ready.
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@greendrake/ui-data",
3
+ "version": "0.1.0",
4
+ "description": "Data-heavy dashboard components: virtualized InfiniteScrollTable, CrudPanel/MasterDetail, record editing and the admin-CRUD kit",
5
+ "license": "MIT",
6
+ "author": "Eugene Greendrake <eugene@greendrake.nz>",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/greendrake/lib.git",
10
+ "directory": "ui-data"
11
+ },
12
+ "homepage": "https://github.com/greendrake/lib/tree/main/ui-data#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/greendrake/lib/issues"
15
+ },
16
+ "type": "module",
17
+ "main": "src/main.ts",
18
+ "types": "src/main.ts",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./src/main.ts",
22
+ "default": "./src/main.ts"
23
+ }
24
+ },
25
+ "files": [
26
+ "src"
27
+ ],
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "scripts": {
32
+ "check": "vue-tsc --noEmit"
33
+ },
34
+ "prettier": "@greendrake/dev-config/prettier",
35
+ "dependencies": {
36
+ "@greendrake/scss-kit": "0.1.0",
37
+ "@greendrake/ui": "0.1.0"
38
+ },
39
+ "peerDependencies": {
40
+ "vue": "^3.5.34"
41
+ },
42
+ "devDependencies": {
43
+ "@greendrake/dev-config": "0.1.0",
44
+ "typescript": "6.0.3",
45
+ "vue": "^3.5.34",
46
+ "vue-tsc": "^2.2.0"
47
+ }
48
+ }