@kernhq/module-quire 0.7.4 → 0.9.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 (88) hide show
  1. package/README.md +93 -55
  2. package/dist/{server → client}/formula.d.ts +6 -0
  3. package/dist/client/formula.d.ts.map +1 -0
  4. package/dist/{server → client}/formula.js +6 -0
  5. package/dist/client/formula.js.map +1 -0
  6. package/dist/contract/properties.d.ts +26 -0
  7. package/dist/contract/properties.d.ts.map +1 -1
  8. package/dist/contract/properties.js +24 -0
  9. package/dist/contract/properties.js.map +1 -1
  10. package/dist/contract/router.d.ts +334 -0
  11. package/dist/contract/router.d.ts.map +1 -1
  12. package/dist/contract/router.js +43 -1
  13. package/dist/contract/router.js.map +1 -1
  14. package/dist/server/_impl.d.ts +341 -0
  15. package/dist/server/_impl.d.ts.map +1 -1
  16. package/dist/server/_impl.js +79 -10
  17. package/dist/server/_impl.js.map +1 -1
  18. package/dist/server/document.d.ts +13 -0
  19. package/dist/server/document.d.ts.map +1 -0
  20. package/dist/server/document.js +58 -0
  21. package/dist/server/document.js.map +1 -0
  22. package/dist/server/index.d.ts.map +1 -1
  23. package/dist/server/index.js +26 -1
  24. package/dist/server/index.js.map +1 -1
  25. package/dist/server/render.d.ts +53 -0
  26. package/dist/server/render.d.ts.map +1 -0
  27. package/dist/server/render.js +358 -0
  28. package/dist/server/render.js.map +1 -0
  29. package/dist/server/services/databases.d.ts +73 -1
  30. package/dist/server/services/databases.d.ts.map +1 -1
  31. package/dist/server/services/databases.js +167 -10
  32. package/dist/server/services/databases.js.map +1 -1
  33. package/dist/server/services/pages.d.ts.map +1 -1
  34. package/dist/server/services/pages.js +11 -1
  35. package/dist/server/services/pages.js.map +1 -1
  36. package/dist/server/services/query.d.ts.map +1 -1
  37. package/dist/server/services/query.js +93 -37
  38. package/dist/server/services/query.js.map +1 -1
  39. package/dist/server/services/versions.d.ts.map +1 -1
  40. package/dist/server/services/versions.js +13 -1
  41. package/dist/server/services/versions.js.map +1 -1
  42. package/migrations/0006_rank_collation.sql +14 -0
  43. package/migrations/meta/_journal.json +7 -0
  44. package/package.json +12 -9
  45. package/src/client/components/PageEditor.svelte +7 -0
  46. package/src/client/components/PageTreeRow.svelte +1 -1
  47. package/src/client/core-api.ts +38 -0
  48. package/src/client/database/BoardView.svelte +354 -0
  49. package/src/client/database/CalendarView.svelte +346 -0
  50. package/src/client/database/DatabaseView.svelte +785 -0
  51. package/src/client/database/FilterMenu.svelte +281 -0
  52. package/src/client/database/FilterValue.svelte +176 -0
  53. package/src/client/database/GalleryView.svelte +177 -0
  54. package/src/client/database/ListView.svelte +108 -0
  55. package/src/client/database/OptionChip.svelte +38 -0
  56. package/src/client/database/PropertyDialog.svelte +460 -0
  57. package/src/client/database/PropertyMenu.svelte +162 -0
  58. package/src/client/database/RowPanel.svelte +157 -0
  59. package/src/client/database/SortMenu.svelte +199 -0
  60. package/src/client/database/TableView.svelte +388 -0
  61. package/src/client/database/ViewDialog.svelte +229 -0
  62. package/src/client/database/cells/Cell.svelte +103 -0
  63. package/src/client/database/cells/CheckboxCell.svelte +33 -0
  64. package/src/client/database/cells/ComputedCell.svelte +100 -0
  65. package/src/client/database/cells/DateCell.svelte +97 -0
  66. package/src/client/database/cells/LinkCell.svelte +143 -0
  67. package/src/client/database/cells/NumberCell.svelte +131 -0
  68. package/src/client/database/cells/PersonCell.svelte +116 -0
  69. package/src/client/database/cells/RelationCell.svelte +222 -0
  70. package/src/client/database/cells/SelectCell.svelte +133 -0
  71. package/src/client/database/cells/TextCell.svelte +85 -0
  72. package/src/client/database/colours.ts +27 -0
  73. package/src/client/database/property-types.test.ts +64 -0
  74. package/src/client/database/property-types.ts +294 -0
  75. package/src/client/database/view-config.test.ts +148 -0
  76. package/src/client/database/view-config.ts +102 -0
  77. package/src/client/formula.test.ts +139 -0
  78. package/src/client/formula.ts +430 -0
  79. package/src/client/i18n.ts +1176 -0
  80. package/src/client/index.ts +31 -0
  81. package/src/client/mock.ts +511 -1
  82. package/src/client/pages/PageView.svelte +47 -17
  83. package/src/client/pages/SpacePage.svelte +8 -2
  84. package/src/client/query.ts +17 -4
  85. package/src/contract/properties.ts +28 -0
  86. package/src/contract/router.ts +46 -0
  87. package/dist/server/formula.d.ts.map +0 -1
  88. package/dist/server/formula.js.map +0 -1
@@ -0,0 +1,108 @@
1
+ <script lang="ts">
2
+ import { DropdownMenu, Icon, IconButton, ListRow, type MenuItem } from '@kernhq/ui'
3
+ import type { Database, Row, View } from '../../contract/index.js'
4
+ import type { Person } from '../core-api.js'
5
+ import { t } from '../i18n.js'
6
+ import Cell from './cells/Cell.svelte'
7
+ import { visiblePropertiesOf } from './view-config.js'
8
+
9
+ /**
10
+ * One row per row: the title, then two or three columns as trailing values.
11
+ *
12
+ * `ListRow` is given neither `href` nor `onclick` — either turns it into a link or a button, and
13
+ * the actions live inside it. Opening is the explicit action at the end, as in the table.
14
+ */
15
+ interface Props {
16
+ database: Database
17
+ view: View | null
18
+ rows: Row[]
19
+ people: Person[]
20
+ workspaceId: string
21
+ canEdit: boolean
22
+ onOpenRow: (row: Row) => void
23
+ onOpenPage: (row: Row) => void
24
+ onDeleteRow: (row: Row) => void
25
+ }
26
+ const { database, view, rows, people, workspaceId, canEdit, onOpenRow, onOpenPage, onDeleteRow }: Props =
27
+ $props()
28
+
29
+ const columns = $derived(visiblePropertiesOf(database, view).slice(0, 3))
30
+
31
+ const menu = (row: Row): MenuItem[] => [
32
+ { id: 'open', label: t('db_open_row'), icon: 'maximize-2', onSelect: () => onOpenRow(row) },
33
+ { id: 'page', label: t('db_open_as_page'), icon: 'file-text', onSelect: () => onOpenPage(row) },
34
+ { type: 'separator' },
35
+ {
36
+ id: 'delete',
37
+ label: t('db_delete_row'),
38
+ icon: 'trash-2',
39
+ danger: true,
40
+ disabled: !canEdit,
41
+ onSelect: () => onDeleteRow(row),
42
+ },
43
+ ]
44
+ </script>
45
+
46
+ <div class="list">
47
+ {#each rows as row (row.id)}
48
+ <ListRow>
49
+ {#snippet leading()}
50
+ <Icon name="file-text" size={14} strokeWidth={1.6} />
51
+ {/snippet}
52
+ {#snippet title()}
53
+ {row.title.trim() || t('untitled')}
54
+ {/snippet}
55
+ {#snippet meta()}
56
+ {#each columns as property (property.id)}
57
+ <span class="meta-cell" title={property.name}>
58
+ <Cell {property} {row} {people} {workspaceId} canEdit={false} onchange={() => {}} />
59
+ </span>
60
+ {/each}
61
+ {/snippet}
62
+ {#snippet trailing()}
63
+ <span class="actions">
64
+ <IconButton
65
+ icon="maximize-2"
66
+ label={t('db_open_row_named', { title: row.title.trim() || t('untitled') })}
67
+ size={26}
68
+ variant="ghost"
69
+ onclick={() => onOpenRow(row)}
70
+ />
71
+ <DropdownMenu items={menu(row)}>
72
+ {#snippet trigger(props: Record<string, unknown>)}
73
+ <IconButton
74
+ {...props}
75
+ icon="ellipsis"
76
+ label={t('db_row_actions', { title: row.title.trim() || t('untitled') })}
77
+ size={26}
78
+ variant="ghost"
79
+ />
80
+ {/snippet}
81
+ </DropdownMenu>
82
+ </span>
83
+ {/snippet}
84
+ </ListRow>
85
+ {/each}
86
+ </div>
87
+
88
+ <style>
89
+ .list {
90
+ padding-block-end: 40px;
91
+ }
92
+ .meta-cell {
93
+ display: inline-flex;
94
+ align-items: center;
95
+ max-width: 200px;
96
+ min-width: 0;
97
+ overflow: hidden;
98
+ }
99
+ .actions {
100
+ display: inline-flex;
101
+ gap: 2px;
102
+ opacity: 0;
103
+ }
104
+ :global(.krow:hover) .actions,
105
+ :global(.krow:focus-within) .actions {
106
+ opacity: 1;
107
+ }
108
+ </style>
@@ -0,0 +1,38 @@
1
+ <script lang="ts">
2
+ import type { SelectOption } from '../../contract/index.js'
3
+ import { toneFor } from './colours.js'
4
+
5
+ /** One select, status or multi-select value, painted from the closed tone map in `colours.ts`. */
6
+ interface Props {
7
+ option?: SelectOption | null
8
+ /** what to draw when the option itself is gone — the raw id is better than an empty cell */
9
+ label?: string
10
+ colour?: string
11
+ compact?: boolean
12
+ }
13
+ const { option = null, label, colour, compact = false }: Props = $props()
14
+
15
+ const tone = $derived(toneFor(colour ?? option?.colour))
16
+ const text = $derived(label ?? option?.label ?? '')
17
+ </script>
18
+
19
+ <span class="chip" class:compact style:background={tone.bg} style:color={tone.fg}>{text}</span>
20
+
21
+ <style>
22
+ .chip {
23
+ display: inline-flex;
24
+ align-items: center;
25
+ max-width: 100%;
26
+ padding: 3px 9px;
27
+ border-radius: var(--kern-r-md);
28
+ font-size: 12px;
29
+ line-height: 1.35;
30
+ white-space: nowrap;
31
+ overflow: hidden;
32
+ text-overflow: ellipsis;
33
+ }
34
+ .chip.compact {
35
+ padding: 1px 7px;
36
+ font-size: 11.5px;
37
+ }
38
+ </style>
@@ -0,0 +1,460 @@
1
+ <script lang="ts">
2
+ import { Button, Dialog, Field, Icon, Input, Select, Switch } from '@kernhq/ui'
3
+ import { createQuery } from '@tanstack/svelte-query'
4
+ import type {
5
+ DatabaseRef,
6
+ Property,
7
+ PropertyConfig,
8
+ PropertyType,
9
+ RollupFunction,
10
+ SelectOption,
11
+ } from '../../contract/index.js'
12
+ import { getQuireApi } from '../api-instance.js'
13
+ import { FormulaError, parseFormula } from '../formula.js'
14
+ import { t } from '../i18n.js'
15
+ import { quireKeys } from '../query.js'
16
+ import { OPTION_COLOURS, toneFor } from './colours.js'
17
+ import { CREATABLE_TYPES, descriptorFor } from './property-types.js'
18
+ import { STATUS_GROUPS } from './view-config.js'
19
+
20
+ /**
21
+ * Add a column, or change one.
22
+ *
23
+ * Only the configuration the chosen type actually uses is shown — a select's options are not a
24
+ * question you can answer about a number, and a form full of inert fields is how a person learns to
25
+ * stop reading it.
26
+ *
27
+ * The formula is validated **here**, as it is typed, by the same parser the server evaluates with.
28
+ * That is why `formula.ts` moved into `src/client`: a round trip per keystroke is not a validator,
29
+ * and a column saved with a broken expression is a table of error chips.
30
+ */
31
+ interface Props {
32
+ open: boolean
33
+ workspaceId: string
34
+ spaceId: string
35
+ databaseId: string
36
+ /** null when adding */
37
+ property: Property | null
38
+ /** this database's relation columns, which a rollup walks */
39
+ relations: Property[]
40
+ busy: boolean
41
+ onClose: () => void
42
+ onSubmit: (input: { name: string; type: PropertyType; config: PropertyConfig }) => void
43
+ }
44
+ const { open, workspaceId, spaceId, databaseId, property, relations, busy, onClose, onSubmit }: Props =
45
+ $props()
46
+
47
+ const api = getQuireApi()
48
+
49
+ let name = $state('')
50
+ let type = $state<PropertyType>('text')
51
+ let config = $state<PropertyConfig>({})
52
+ let submitting = $state(false)
53
+
54
+ /** Reset every time the dialog opens, so a cancelled edit never leaks into the next one. */
55
+ $effect(() => {
56
+ if (!open) return
57
+ name = property?.name ?? ''
58
+ type = property?.type ?? 'text'
59
+ config = { ...(property?.config ?? {}) }
60
+ submitting = false
61
+ })
62
+
63
+ const descriptor = $derived(descriptorFor(type))
64
+
65
+ /** The databases a relation may point at, and the one a rollup reads through. */
66
+ const databasesQuery = createQuery(() => ({
67
+ queryKey: ['quire', 'database', workspaceId, 'space', spaceId],
68
+ enabled: open && Boolean(workspaceId && spaceId) && (type === 'relation' || type === 'rollup'),
69
+ queryFn: () => api.databases.list({ workspaceId, spaceId }),
70
+ }))
71
+ const targets = $derived<DatabaseRef[]>(databasesQuery.data ?? [])
72
+
73
+ /** A rollup gathers a column from the database on the *other* side of the relation it walks. */
74
+ const viaProperty = $derived(relations.find((r) => r.id === config.rollupRelationPropertyId) ?? null)
75
+ const viaDatabaseId = $derived(viaProperty?.config.relationDatabaseId ?? null)
76
+ const viaDatabase = createQuery(() => ({
77
+ queryKey: quireKeys.database(workspaceId, viaDatabaseId ?? ''),
78
+ enabled: open && type === 'rollup' && Boolean(viaDatabaseId),
79
+ queryFn: () => api.databases.get({ workspaceId, databaseId: viaDatabaseId! }),
80
+ }))
81
+
82
+ const formulaError = $derived.by(() => {
83
+ if (type !== 'formula') return null
84
+ const expression = (config.expression ?? '').trim()
85
+ if (!expression) return null
86
+ try {
87
+ parseFormula(expression)
88
+ return null
89
+ } catch (err) {
90
+ return err instanceof FormulaError || err instanceof Error ? err.message : t('common.error')
91
+ }
92
+ })
93
+
94
+ const options = $derived<SelectOption[]>(config.options ?? [])
95
+ const needsOptions = $derived(type === 'select' || type === 'multi_select' || type === 'status')
96
+
97
+ const setConfig = (patch: Partial<PropertyConfig>) => {
98
+ config = { ...config, ...patch }
99
+ }
100
+
101
+ /** A stable id, because the option id is what every row already stores. */
102
+ const nextOptionId = () => `opt_${Math.random().toString(36).slice(2, 9)}`
103
+
104
+ const addOption = () =>
105
+ setConfig({
106
+ options: [
107
+ ...options,
108
+ {
109
+ id: nextOptionId(),
110
+ label: t('db_option_new'),
111
+ colour: OPTION_COLOURS[options.length % OPTION_COLOURS.length] ?? 'grey',
112
+ ...(type === 'status' ? { group: 'todo' as const } : {}),
113
+ },
114
+ ],
115
+ })
116
+
117
+ const patchOption = (id: string, patch: Partial<SelectOption>) =>
118
+ setConfig({ options: options.map((o) => (o.id === id ? { ...o, ...patch } : o)) })
119
+
120
+ const removeOption = (id: string) => setConfig({ options: options.filter((o) => o.id !== id) })
121
+
122
+ const ROLLUP_FUNCTIONS: RollupFunction[] = [
123
+ 'count',
124
+ 'count_values',
125
+ 'count_unique',
126
+ 'sum',
127
+ 'average',
128
+ 'min',
129
+ 'max',
130
+ 'range',
131
+ 'show_original',
132
+ 'checked',
133
+ 'unchecked',
134
+ 'percent_checked',
135
+ ]
136
+
137
+ const valid = $derived(
138
+ name.trim().length > 0 &&
139
+ formulaError === null &&
140
+ (!needsOptions || options.every((o) => o.label.trim().length > 0)),
141
+ )
142
+
143
+ /**
144
+ * `disabled={busy}` reaches the button on the next render, and two quick clicks are one render
145
+ * apart — so the flag is set in the same tick as the click and read before anything is sent.
146
+ */
147
+ function submit() {
148
+ if (submitting || !valid) return
149
+ submitting = true
150
+ onSubmit({ name: name.trim(), type, config: $state.snapshot(config) })
151
+ }
152
+
153
+ $effect(() => {
154
+ if (!busy) submitting = false
155
+ })
156
+ </script>
157
+
158
+ <Dialog
159
+ {open}
160
+ title={property ? t('db_edit_property') : t('db_add_property')}
161
+ size="md"
162
+ onOpenChange={(o) => !o && onClose()}
163
+ >
164
+ <div class="form">
165
+ <Field label={t('db_property_name')}>
166
+ {#snippet children(id: string)}
167
+ <Input {id} bind:value={name} placeholder={t('db_property_name')} />
168
+ {/snippet}
169
+ </Field>
170
+
171
+ <Field label={t('db_property_type')} hint={property ? t('db_property_retype_hint') : undefined}>
172
+ {#snippet children(id: string)}
173
+ <Select
174
+ {id}
175
+ value={type}
176
+ options={CREATABLE_TYPES.map((option) => ({
177
+ value: option,
178
+ label: t(`db_type_${option}`),
179
+ icon: descriptorFor(option).icon,
180
+ }))}
181
+ onValueChange={(next) => (type = next as PropertyType)}
182
+ />
183
+ {/snippet}
184
+ </Field>
185
+
186
+ {#if needsOptions}
187
+ <fieldset class="group">
188
+ <legend>{t('db_options')}</legend>
189
+ {#if options.length === 0}
190
+ <p class="hint">{t('db_options_none')}</p>
191
+ {/if}
192
+ <ul class="options">
193
+ {#each options as option (option.id)}
194
+ <li>
195
+ <span class="swatch" style:background={toneFor(option.colour).bg}></span>
196
+ <input
197
+ class="opt-label"
198
+ value={option.label}
199
+ aria-label={t('db_option_label')}
200
+ oninput={(e) => patchOption(option.id, { label: e.currentTarget.value })}
201
+ />
202
+ <select
203
+ class="opt-select"
204
+ value={option.colour}
205
+ aria-label={t('db_option_colour')}
206
+ onchange={(e) => patchOption(option.id, { colour: e.currentTarget.value })}
207
+ >
208
+ {#each OPTION_COLOURS as colour (colour)}
209
+ <option value={colour}>{t(`db_colour_${colour}`)}</option>
210
+ {/each}
211
+ </select>
212
+ {#if type === 'status'}
213
+ <select
214
+ class="opt-select"
215
+ value={option.group ?? 'todo'}
216
+ aria-label={t('db_option_group')}
217
+ onchange={(e) =>
218
+ patchOption(option.id, {
219
+ group: e.currentTarget.value as 'todo' | 'doing' | 'done',
220
+ })}
221
+ >
222
+ {#each STATUS_GROUPS as band (band)}
223
+ <option value={band}>{t(`db_status_${band}`)}</option>
224
+ {/each}
225
+ </select>
226
+ {/if}
227
+ <button
228
+ type="button"
229
+ class="opt-remove"
230
+ aria-label={t('db_option_remove', { label: option.label })}
231
+ onclick={() => removeOption(option.id)}
232
+ >
233
+ <Icon name="x" size={13} strokeWidth={1.9} />
234
+ </button>
235
+ </li>
236
+ {/each}
237
+ </ul>
238
+ <Button size="sm" variant="secondary" onclick={addOption}>{t('db_option_add')}</Button>
239
+ </fieldset>
240
+ {/if}
241
+
242
+ {#if type === 'number'}
243
+ <Field label={t('db_number_format')}>
244
+ {#snippet children(id: string)}
245
+ <Select
246
+ {id}
247
+ value={config.format ?? 'plain'}
248
+ options={['plain', 'percent', 'currency'].map((f) => ({ value: f, label: t(`db_format_${f}`) }))}
249
+ onValueChange={(next) => setConfig({ format: next })}
250
+ />
251
+ {/snippet}
252
+ </Field>
253
+ <Field label={t('db_precision')}>
254
+ {#snippet children(id: string)}
255
+ <Input
256
+ {id}
257
+ type="number"
258
+ min="0"
259
+ max="8"
260
+ value={config.precision ?? ''}
261
+ oninput={(e) => {
262
+ const raw = (e.currentTarget as HTMLInputElement).value
263
+ setConfig({ precision: raw === '' ? undefined : Number(raw) })
264
+ }}
265
+ />
266
+ {/snippet}
267
+ </Field>
268
+ {/if}
269
+
270
+ {#if type === 'date'}
271
+ <Switch
272
+ checked={config.includeTime === true}
273
+ label={t('db_include_time')}
274
+ onCheckedChange={(on) => setConfig({ includeTime: on })}
275
+ />
276
+ {/if}
277
+
278
+ {#if type === 'person'}
279
+ <Switch
280
+ checked={config.multiple !== false}
281
+ label={t('db_person_multiple')}
282
+ onCheckedChange={(on) => setConfig({ multiple: on })}
283
+ />
284
+ {/if}
285
+
286
+ {#if type === 'relation'}
287
+ <Field label={t('db_relation_database')} hint={t('db_relation_database_hint')}>
288
+ {#snippet children(id: string)}
289
+ <Select
290
+ {id}
291
+ value={config.relationDatabaseId ?? ''}
292
+ placeholder={t('db_relation_choose')}
293
+ options={targets
294
+ .filter((d) => d.id !== databaseId)
295
+ .map((d) => ({ value: d.id, label: d.name || t('untitled') }))}
296
+ onValueChange={(next) => setConfig({ relationDatabaseId: next || undefined })}
297
+ />
298
+ {/snippet}
299
+ </Field>
300
+ {/if}
301
+
302
+ {#if type === 'rollup'}
303
+ <Field label={t('db_rollup_relation')}>
304
+ {#snippet children(id: string)}
305
+ <Select
306
+ {id}
307
+ value={config.rollupRelationPropertyId ?? ''}
308
+ placeholder={t('db_rollup_choose')}
309
+ options={relations.map((r) => ({ value: r.id, label: r.name }))}
310
+ onValueChange={(next) => setConfig({ rollupRelationPropertyId: next || undefined })}
311
+ />
312
+ {/snippet}
313
+ </Field>
314
+ {#if relations.length === 0}
315
+ <p class="hint">{t('db_rollup_needs_relation')}</p>
316
+ {/if}
317
+ <Field label={t('db_rollup_target')}>
318
+ {#snippet children(id: string)}
319
+ <Select
320
+ {id}
321
+ value={config.rollupTargetPropertyId ?? ''}
322
+ placeholder={t('db_rollup_choose')}
323
+ disabled={!viaDatabaseId}
324
+ options={(viaDatabase.data?.properties ?? []).map((p) => ({ value: p.id, label: p.name }))}
325
+ onValueChange={(next) => setConfig({ rollupTargetPropertyId: next || undefined })}
326
+ />
327
+ {/snippet}
328
+ </Field>
329
+ <Field label={t('db_rollup_function')}>
330
+ {#snippet children(id: string)}
331
+ <Select
332
+ {id}
333
+ value={config.rollupFunction ?? 'count'}
334
+ options={ROLLUP_FUNCTIONS.map((fn) => ({ value: fn, label: t(`db_rollup_${fn}`) }))}
335
+ onValueChange={(next) => setConfig({ rollupFunction: next as RollupFunction })}
336
+ />
337
+ {/snippet}
338
+ </Field>
339
+ {/if}
340
+
341
+ {#if type === 'formula'}
342
+ <Field
343
+ label={t('db_expression')}
344
+ hint={formulaError ? undefined : t('db_expression_hint')}
345
+ error={formulaError}
346
+ >
347
+ {#snippet children(id: string)}
348
+ <Input
349
+ {id}
350
+ mono
351
+ value={config.expression ?? ''}
352
+ placeholder={'round(prop("Hours") / 8, 2)'}
353
+ oninput={(e) => setConfig({ expression: (e.currentTarget as HTMLInputElement).value })}
354
+ />
355
+ {/snippet}
356
+ </Field>
357
+ {/if}
358
+
359
+ {#if descriptor.readOnly && type !== 'formula' && type !== 'rollup'}
360
+ <p class="hint">{t('db_type_readonly_hint')}</p>
361
+ {/if}
362
+ </div>
363
+
364
+ {#snippet footer()}
365
+ <Button variant="secondary" onclick={onClose}>{t('common.cancel')}</Button>
366
+ <Button disabled={!valid || busy || submitting} onclick={submit}>
367
+ {property ? t('common.save') : t('common.add')}
368
+ </Button>
369
+ {/snippet}
370
+ </Dialog>
371
+
372
+ <style>
373
+ .form {
374
+ display: flex;
375
+ flex-direction: column;
376
+ gap: 16px;
377
+ }
378
+ .group {
379
+ border: 1px solid var(--kern-border);
380
+ border-radius: var(--kern-r-lg);
381
+ padding: 12px 14px 14px;
382
+ margin: 0;
383
+ display: flex;
384
+ flex-direction: column;
385
+ gap: 10px;
386
+ }
387
+ legend {
388
+ padding-inline: 4px;
389
+ font-size: 12px;
390
+ font-weight: 600;
391
+ color: var(--kern-ink-550);
392
+ }
393
+ .options {
394
+ list-style: none;
395
+ margin: 0;
396
+ padding: 0;
397
+ display: flex;
398
+ flex-direction: column;
399
+ gap: 6px;
400
+ }
401
+ .options li {
402
+ display: flex;
403
+ align-items: center;
404
+ gap: 7px;
405
+ }
406
+ .swatch {
407
+ flex: none;
408
+ width: 12px;
409
+ height: 12px;
410
+ border-radius: 3px;
411
+ }
412
+ .opt-label {
413
+ flex: 1;
414
+ min-width: 0;
415
+ height: 30px;
416
+ padding: 0 9px;
417
+ border: 1px solid var(--kern-border);
418
+ border-radius: var(--kern-r-md);
419
+ background: var(--kern-surface-raised);
420
+ color: var(--kern-ink-700);
421
+ font: inherit;
422
+ font-size: 13px;
423
+ }
424
+ .opt-label:focus {
425
+ outline: none;
426
+ border-color: var(--kern-accent);
427
+ box-shadow: 0 0 0 3px var(--kern-ring);
428
+ }
429
+ .opt-select {
430
+ flex: none;
431
+ height: 30px;
432
+ padding: 0 6px;
433
+ border: 1px solid var(--kern-border);
434
+ border-radius: var(--kern-r-md);
435
+ background: var(--kern-surface-raised);
436
+ color: var(--kern-ink-700);
437
+ font: inherit;
438
+ font-size: 12.5px;
439
+ }
440
+ .opt-remove {
441
+ flex: none;
442
+ display: inline-grid;
443
+ place-items: center;
444
+ width: 28px;
445
+ height: 28px;
446
+ border: 0;
447
+ border-radius: var(--kern-r-sm);
448
+ background: none;
449
+ color: var(--kern-ink-450);
450
+ }
451
+ .opt-remove:hover {
452
+ background: var(--kern-surface-hover);
453
+ color: var(--kern-ink-900);
454
+ }
455
+ .hint {
456
+ margin: 0;
457
+ font-size: 12.5px;
458
+ color: var(--kern-ink-450);
459
+ }
460
+ </style>