@kernhq/module-quire 0.8.0 → 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.
- package/README.md +93 -55
- package/dist/{server → client}/formula.d.ts +6 -0
- package/dist/client/formula.d.ts.map +1 -0
- package/dist/{server → client}/formula.js +6 -0
- package/dist/client/formula.js.map +1 -0
- package/dist/contract/properties.d.ts +26 -0
- package/dist/contract/properties.d.ts.map +1 -1
- package/dist/contract/properties.js +24 -0
- package/dist/contract/properties.js.map +1 -1
- package/dist/contract/router.d.ts +334 -0
- package/dist/contract/router.d.ts.map +1 -1
- package/dist/contract/router.js +43 -1
- package/dist/contract/router.js.map +1 -1
- package/dist/server/_impl.d.ts +341 -0
- package/dist/server/_impl.d.ts.map +1 -1
- package/dist/server/_impl.js +79 -10
- package/dist/server/_impl.js.map +1 -1
- package/dist/server/services/databases.d.ts +73 -1
- package/dist/server/services/databases.d.ts.map +1 -1
- package/dist/server/services/databases.js +167 -10
- package/dist/server/services/databases.js.map +1 -1
- package/dist/server/services/pages.d.ts.map +1 -1
- package/dist/server/services/pages.js +11 -1
- package/dist/server/services/pages.js.map +1 -1
- package/dist/server/services/query.d.ts.map +1 -1
- package/dist/server/services/query.js +93 -37
- package/dist/server/services/query.js.map +1 -1
- package/migrations/0006_rank_collation.sql +14 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +3 -2
- package/src/client/components/PageTreeRow.svelte +1 -1
- package/src/client/core-api.ts +38 -0
- package/src/client/database/BoardView.svelte +354 -0
- package/src/client/database/CalendarView.svelte +346 -0
- package/src/client/database/DatabaseView.svelte +785 -0
- package/src/client/database/FilterMenu.svelte +281 -0
- package/src/client/database/FilterValue.svelte +176 -0
- package/src/client/database/GalleryView.svelte +177 -0
- package/src/client/database/ListView.svelte +108 -0
- package/src/client/database/OptionChip.svelte +38 -0
- package/src/client/database/PropertyDialog.svelte +460 -0
- package/src/client/database/PropertyMenu.svelte +162 -0
- package/src/client/database/RowPanel.svelte +157 -0
- package/src/client/database/SortMenu.svelte +199 -0
- package/src/client/database/TableView.svelte +388 -0
- package/src/client/database/ViewDialog.svelte +229 -0
- package/src/client/database/cells/Cell.svelte +103 -0
- package/src/client/database/cells/CheckboxCell.svelte +33 -0
- package/src/client/database/cells/ComputedCell.svelte +100 -0
- package/src/client/database/cells/DateCell.svelte +97 -0
- package/src/client/database/cells/LinkCell.svelte +143 -0
- package/src/client/database/cells/NumberCell.svelte +131 -0
- package/src/client/database/cells/PersonCell.svelte +116 -0
- package/src/client/database/cells/RelationCell.svelte +222 -0
- package/src/client/database/cells/SelectCell.svelte +133 -0
- package/src/client/database/cells/TextCell.svelte +85 -0
- package/src/client/database/colours.ts +27 -0
- package/src/client/database/property-types.test.ts +64 -0
- package/src/client/database/property-types.ts +294 -0
- package/src/client/database/view-config.test.ts +148 -0
- package/src/client/database/view-config.ts +102 -0
- package/src/client/formula.test.ts +139 -0
- package/src/client/formula.ts +430 -0
- package/src/client/i18n.ts +1176 -0
- package/src/client/index.ts +31 -0
- package/src/client/mock.ts +511 -1
- package/src/client/pages/PageView.svelte +47 -17
- package/src/client/pages/SpacePage.svelte +8 -2
- package/src/client/query.ts +17 -4
- package/src/contract/properties.ts +28 -0
- package/src/contract/router.ts +46 -0
- package/dist/server/formula.d.ts.map +0 -1
- package/dist/server/formula.js.map +0 -1
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { DropdownMenu, Icon, type MenuItem } from '@kernhq/ui'
|
|
3
|
+
import type { Property } from '../../contract/index.js'
|
|
4
|
+
import { t } from '../i18n.js'
|
|
5
|
+
import { descriptorFor } from './property-types.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A column header, and everything that can be done to it.
|
|
9
|
+
*
|
|
10
|
+
* This menu is also where the **keyboard route** for the two pointer gestures lives: dragging the
|
|
11
|
+
* grabber resizes and there is no drag on the header row at all, so "move earlier", "move later",
|
|
12
|
+
* "widen" and "narrow" are not conveniences — they are the only way to do those things without a
|
|
13
|
+
* mouse, which makes them part of the specification rather than an extra.
|
|
14
|
+
*/
|
|
15
|
+
interface Props {
|
|
16
|
+
property: Property
|
|
17
|
+
canEdit: boolean
|
|
18
|
+
first: boolean
|
|
19
|
+
last: boolean
|
|
20
|
+
sortDirection: 'asc' | 'desc' | null
|
|
21
|
+
onEdit: () => void
|
|
22
|
+
onHide: () => void
|
|
23
|
+
onMove: (direction: -1 | 1) => void
|
|
24
|
+
onResize: (delta: number) => void
|
|
25
|
+
onSort: (direction: 'asc' | 'desc' | null) => void
|
|
26
|
+
onFilter: () => void
|
|
27
|
+
onDelete: () => void
|
|
28
|
+
}
|
|
29
|
+
const {
|
|
30
|
+
property,
|
|
31
|
+
canEdit,
|
|
32
|
+
first,
|
|
33
|
+
last,
|
|
34
|
+
sortDirection,
|
|
35
|
+
onEdit,
|
|
36
|
+
onHide,
|
|
37
|
+
onMove,
|
|
38
|
+
onResize,
|
|
39
|
+
onSort,
|
|
40
|
+
onFilter,
|
|
41
|
+
onDelete,
|
|
42
|
+
}: Props = $props()
|
|
43
|
+
|
|
44
|
+
const descriptor = $derived(descriptorFor(property.type))
|
|
45
|
+
|
|
46
|
+
const items = $derived<MenuItem[]>([
|
|
47
|
+
{ type: 'label', label: property.name },
|
|
48
|
+
{
|
|
49
|
+
id: 'sort-asc',
|
|
50
|
+
label: t('db_sort_asc'),
|
|
51
|
+
icon: 'arrow-up',
|
|
52
|
+
disabled: sortDirection === 'asc',
|
|
53
|
+
onSelect: () => onSort('asc'),
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: 'sort-desc',
|
|
57
|
+
label: t('db_sort_desc'),
|
|
58
|
+
icon: 'chevron-down',
|
|
59
|
+
disabled: sortDirection === 'desc',
|
|
60
|
+
onSelect: () => onSort('desc'),
|
|
61
|
+
},
|
|
62
|
+
...(sortDirection
|
|
63
|
+
? [{ id: 'sort-clear', label: t('db_sort_remove'), icon: 'x', onSelect: () => onSort(null) }]
|
|
64
|
+
: []),
|
|
65
|
+
{ type: 'separator' },
|
|
66
|
+
{ id: 'filter', label: t('db_filter_by'), icon: 'filter', onSelect: onFilter },
|
|
67
|
+
{ type: 'separator' },
|
|
68
|
+
{
|
|
69
|
+
id: 'move-earlier',
|
|
70
|
+
label: t('db_move_earlier'),
|
|
71
|
+
icon: 'chevron-left',
|
|
72
|
+
disabled: first || !canEdit,
|
|
73
|
+
onSelect: () => onMove(-1),
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: 'move-later',
|
|
77
|
+
label: t('db_move_later'),
|
|
78
|
+
icon: 'chevron-right',
|
|
79
|
+
disabled: last || !canEdit,
|
|
80
|
+
onSelect: () => onMove(1),
|
|
81
|
+
},
|
|
82
|
+
{ id: 'widen', label: t('db_widen'), icon: 'maximize-2', onSelect: () => onResize(40) },
|
|
83
|
+
{ id: 'narrow', label: t('db_narrow'), icon: 'minimize-2', onSelect: () => onResize(-40) },
|
|
84
|
+
{ type: 'separator' },
|
|
85
|
+
{
|
|
86
|
+
id: 'edit',
|
|
87
|
+
label: t('db_edit_property'),
|
|
88
|
+
icon: 'pencil',
|
|
89
|
+
disabled: !canEdit,
|
|
90
|
+
onSelect: onEdit,
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: 'hide',
|
|
94
|
+
label: t('db_hide_property'),
|
|
95
|
+
icon: 'eye-off',
|
|
96
|
+
disabled: !canEdit,
|
|
97
|
+
onSelect: onHide,
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: 'delete',
|
|
101
|
+
label: t('db_delete_property'),
|
|
102
|
+
icon: 'trash-2',
|
|
103
|
+
danger: true,
|
|
104
|
+
disabled: !canEdit,
|
|
105
|
+
onSelect: onDelete,
|
|
106
|
+
},
|
|
107
|
+
])
|
|
108
|
+
</script>
|
|
109
|
+
|
|
110
|
+
<DropdownMenu {items} align="start">
|
|
111
|
+
{#snippet trigger(props: Record<string, unknown>)}
|
|
112
|
+
<button {...props} type="button" class="head" title={property.name}>
|
|
113
|
+
<Icon name={descriptor.icon} size={12} strokeWidth={1.7} />
|
|
114
|
+
<span class="nm">{property.name}</span>
|
|
115
|
+
{#if sortDirection}
|
|
116
|
+
<Icon name={sortDirection === 'asc' ? 'arrow-up' : 'chevron-down'} size={11} strokeWidth={2} />
|
|
117
|
+
{/if}
|
|
118
|
+
</button>
|
|
119
|
+
{/snippet}
|
|
120
|
+
</DropdownMenu>
|
|
121
|
+
|
|
122
|
+
<style>
|
|
123
|
+
/*
|
|
124
|
+
* The header cell is the trigger, so the whole 34px strip is the hit target rather than a 12px
|
|
125
|
+
* chevron beside a label.
|
|
126
|
+
*/
|
|
127
|
+
.head {
|
|
128
|
+
display: inline-flex;
|
|
129
|
+
align-items: center;
|
|
130
|
+
gap: 6px;
|
|
131
|
+
min-width: 0;
|
|
132
|
+
width: 100%;
|
|
133
|
+
height: 26px;
|
|
134
|
+
padding: 0 6px;
|
|
135
|
+
margin-inline-start: -6px;
|
|
136
|
+
border: 0;
|
|
137
|
+
border-radius: var(--kern-r-sm);
|
|
138
|
+
background: none;
|
|
139
|
+
color: var(--kern-ink-280);
|
|
140
|
+
font: inherit;
|
|
141
|
+
font-size: 11px;
|
|
142
|
+
font-weight: 600;
|
|
143
|
+
letter-spacing: 0.06em;
|
|
144
|
+
text-transform: uppercase;
|
|
145
|
+
text-align: start;
|
|
146
|
+
}
|
|
147
|
+
/* Uppercase tracking breaks connected Arabic and Persian letters. */
|
|
148
|
+
:global([dir='rtl']) .head {
|
|
149
|
+
letter-spacing: 0;
|
|
150
|
+
text-transform: none;
|
|
151
|
+
}
|
|
152
|
+
.head:hover {
|
|
153
|
+
background: var(--kern-surface-hover);
|
|
154
|
+
color: var(--kern-ink-900);
|
|
155
|
+
}
|
|
156
|
+
.nm {
|
|
157
|
+
min-width: 0;
|
|
158
|
+
overflow: hidden;
|
|
159
|
+
text-overflow: ellipsis;
|
|
160
|
+
white-space: nowrap;
|
|
161
|
+
}
|
|
162
|
+
</style>
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Button, RightPanel, relativeTime } from '@kernhq/ui'
|
|
3
|
+
import type { Database, Property, Row } 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 { descriptorFor } from './property-types.js'
|
|
8
|
+
import { orderedProperties } from './view-config.js'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* One row, with every column — hidden ones included.
|
|
12
|
+
*
|
|
13
|
+
* This is where a row is inspected, and it has to be: `Page` carries no field saying "this page is
|
|
14
|
+
* a row of that database", deliberately, so opening the row's own page shows its prose and could
|
|
15
|
+
* not show its properties without inventing a second meaning for `pages.database_id`. The panel
|
|
16
|
+
* links out to the page instead.
|
|
17
|
+
*/
|
|
18
|
+
interface Props {
|
|
19
|
+
database: Database
|
|
20
|
+
row: Row
|
|
21
|
+
people: Person[]
|
|
22
|
+
workspaceId: string
|
|
23
|
+
canEdit: boolean
|
|
24
|
+
onClose: () => void
|
|
25
|
+
onChange: (property: Property, value: unknown) => void
|
|
26
|
+
onTitleChange: (title: string) => void
|
|
27
|
+
onOpenPage: () => void
|
|
28
|
+
}
|
|
29
|
+
const { database, row, people, workspaceId, canEdit, onClose, onChange, onTitleChange, onOpenPage }: Props =
|
|
30
|
+
$props()
|
|
31
|
+
|
|
32
|
+
const properties = $derived(orderedProperties(database))
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<RightPanel title={t('db_row_panel')} width={420} onClose={onClose}>
|
|
36
|
+
<div class="body">
|
|
37
|
+
{#if canEdit}
|
|
38
|
+
<input
|
|
39
|
+
class="title"
|
|
40
|
+
value={row.title}
|
|
41
|
+
aria-label={t('db_title_column')}
|
|
42
|
+
placeholder={t('untitled')}
|
|
43
|
+
onblur={(e) => onTitleChange(e.currentTarget.value)}
|
|
44
|
+
onkeydown={(e) => {
|
|
45
|
+
if (e.key === 'Enter') e.currentTarget.blur()
|
|
46
|
+
}}
|
|
47
|
+
/>
|
|
48
|
+
{:else}
|
|
49
|
+
<h2 class="title as-heading">{row.title.trim() || t('untitled')}</h2>
|
|
50
|
+
{/if}
|
|
51
|
+
|
|
52
|
+
<p class="stamp">{t('edited_ago', { when: relativeTime(row.updatedAt) })}</p>
|
|
53
|
+
|
|
54
|
+
{#if properties.length === 0}
|
|
55
|
+
<p class="hint">{t('db_no_columns')}</p>
|
|
56
|
+
{:else}
|
|
57
|
+
<dl class="fields">
|
|
58
|
+
{#each properties as property (property.id)}
|
|
59
|
+
<div class="field">
|
|
60
|
+
<dt title={property.hidden ? t('db_hidden_here') : undefined}>
|
|
61
|
+
{property.name}
|
|
62
|
+
{#if property.hidden}<span class="tag">{t('db_hidden')}</span>{/if}
|
|
63
|
+
</dt>
|
|
64
|
+
<dd>
|
|
65
|
+
<Cell
|
|
66
|
+
{property}
|
|
67
|
+
{row}
|
|
68
|
+
{people}
|
|
69
|
+
{workspaceId}
|
|
70
|
+
canEdit={canEdit && !descriptorFor(property.type).readOnly}
|
|
71
|
+
onchange={(value) => onChange(property, value)}
|
|
72
|
+
/>
|
|
73
|
+
</dd>
|
|
74
|
+
</div>
|
|
75
|
+
{/each}
|
|
76
|
+
</dl>
|
|
77
|
+
{/if}
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
{#snippet footer()}
|
|
81
|
+
<Button variant="secondary" onclick={onOpenPage}>{t('db_open_as_page')}</Button>
|
|
82
|
+
{/snippet}
|
|
83
|
+
</RightPanel>
|
|
84
|
+
|
|
85
|
+
<style>
|
|
86
|
+
.body {
|
|
87
|
+
padding: 18px;
|
|
88
|
+
}
|
|
89
|
+
.title {
|
|
90
|
+
width: 100%;
|
|
91
|
+
padding: 2px 6px;
|
|
92
|
+
margin-inline-start: -6px;
|
|
93
|
+
border: 1px solid transparent;
|
|
94
|
+
border-radius: var(--kern-r-sm);
|
|
95
|
+
background: none;
|
|
96
|
+
color: var(--kern-ink-900);
|
|
97
|
+
font: inherit;
|
|
98
|
+
font-size: 18px;
|
|
99
|
+
font-weight: 600;
|
|
100
|
+
letter-spacing: -0.02em;
|
|
101
|
+
}
|
|
102
|
+
.title:hover {
|
|
103
|
+
background: var(--kern-surface-active);
|
|
104
|
+
}
|
|
105
|
+
.title:focus {
|
|
106
|
+
border-color: var(--kern-border);
|
|
107
|
+
background: var(--kern-surface-raised);
|
|
108
|
+
outline: none;
|
|
109
|
+
}
|
|
110
|
+
.title.as-heading {
|
|
111
|
+
margin: 0;
|
|
112
|
+
}
|
|
113
|
+
.stamp {
|
|
114
|
+
margin: 6px 0 16px;
|
|
115
|
+
font-size: 12px;
|
|
116
|
+
color: var(--kern-ink-450);
|
|
117
|
+
}
|
|
118
|
+
.fields {
|
|
119
|
+
margin: 0;
|
|
120
|
+
padding: 0;
|
|
121
|
+
display: flex;
|
|
122
|
+
flex-direction: column;
|
|
123
|
+
gap: 10px;
|
|
124
|
+
}
|
|
125
|
+
.field {
|
|
126
|
+
display: grid;
|
|
127
|
+
grid-template-columns: 130px minmax(0, 1fr);
|
|
128
|
+
align-items: center;
|
|
129
|
+
gap: 10px;
|
|
130
|
+
}
|
|
131
|
+
.field dt {
|
|
132
|
+
display: flex;
|
|
133
|
+
align-items: center;
|
|
134
|
+
gap: 5px;
|
|
135
|
+
font-size: 12.5px;
|
|
136
|
+
color: var(--kern-ink-450);
|
|
137
|
+
overflow: hidden;
|
|
138
|
+
}
|
|
139
|
+
.field dd {
|
|
140
|
+
margin: 0;
|
|
141
|
+
min-width: 0;
|
|
142
|
+
font-size: 13px;
|
|
143
|
+
color: var(--kern-ink-700);
|
|
144
|
+
}
|
|
145
|
+
.tag {
|
|
146
|
+
padding: 0 5px;
|
|
147
|
+
border-radius: var(--kern-r-sm);
|
|
148
|
+
background: var(--kern-surface-chip);
|
|
149
|
+
font-size: 10.5px;
|
|
150
|
+
color: var(--kern-ink-550);
|
|
151
|
+
}
|
|
152
|
+
.hint {
|
|
153
|
+
margin: 0;
|
|
154
|
+
font-size: 12.5px;
|
|
155
|
+
color: var(--kern-ink-450);
|
|
156
|
+
}
|
|
157
|
+
</style>
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Button, Icon, Popover, ToolbarButton } from '@kernhq/ui'
|
|
3
|
+
import type { Database, Sort, ViewConfig } from '../../contract/index.js'
|
|
4
|
+
import { t } from '../i18n.js'
|
|
5
|
+
import { orderedProperties } from './view-config.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The view's sorting, in order.
|
|
9
|
+
*
|
|
10
|
+
* Order matters and is the whole reason this is a list rather than a single choice, so moving a
|
|
11
|
+
* rule up and down is a control rather than something you get by deleting and re-adding.
|
|
12
|
+
*/
|
|
13
|
+
interface Props {
|
|
14
|
+
database: Database
|
|
15
|
+
config: ViewConfig
|
|
16
|
+
canEdit: boolean
|
|
17
|
+
open: boolean
|
|
18
|
+
onOpenChange: (open: boolean) => void
|
|
19
|
+
onchange: (patch: Partial<ViewConfig>) => void
|
|
20
|
+
}
|
|
21
|
+
const { database, config, canEdit, open, onOpenChange, onchange }: Props = $props()
|
|
22
|
+
|
|
23
|
+
const properties = $derived(orderedProperties(database))
|
|
24
|
+
const sorts = $derived(config.sorts)
|
|
25
|
+
|
|
26
|
+
const setSorts = (next: Sort[]) => onchange({ sorts: next })
|
|
27
|
+
const patchAt = (index: number, patch: Partial<Sort>) =>
|
|
28
|
+
setSorts(sorts.map((s, i) => (i === index ? { ...s, ...patch } : s)))
|
|
29
|
+
|
|
30
|
+
function addSort() {
|
|
31
|
+
const used = new Set(sorts.map((s) => s.propertyKey))
|
|
32
|
+
const next = properties.find((p) => !used.has(p.key)) ?? properties[0]
|
|
33
|
+
if (!next) return
|
|
34
|
+
setSorts([...sorts, { propertyKey: next.key, direction: 'asc' }])
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function move(index: number, delta: -1 | 1) {
|
|
38
|
+
const to = index + delta
|
|
39
|
+
if (to < 0 || to >= sorts.length) return
|
|
40
|
+
const next = [...sorts]
|
|
41
|
+
const [moved] = next.splice(index, 1)
|
|
42
|
+
if (moved) next.splice(to, 0, moved)
|
|
43
|
+
setSorts(next)
|
|
44
|
+
}
|
|
45
|
+
</script>
|
|
46
|
+
|
|
47
|
+
<Popover {open} {onOpenChange} align="start" width="380px">
|
|
48
|
+
{#snippet trigger(props: Record<string, unknown>)}
|
|
49
|
+
<ToolbarButton
|
|
50
|
+
{...props}
|
|
51
|
+
icon="chevrons-up-down"
|
|
52
|
+
active={sorts.length > 0}
|
|
53
|
+
onClear={sorts.length > 0 && canEdit ? () => setSorts([]) : undefined}
|
|
54
|
+
>
|
|
55
|
+
{sorts.length > 0 ? t('db_sorts', { n: sorts.length }) : t('db_sort')}
|
|
56
|
+
</ToolbarButton>
|
|
57
|
+
{/snippet}
|
|
58
|
+
|
|
59
|
+
<div class="panel">
|
|
60
|
+
{#if sorts.length === 0}
|
|
61
|
+
<p class="empty">{t('db_sort_none_desc')}</p>
|
|
62
|
+
{:else}
|
|
63
|
+
<ul class="rows">
|
|
64
|
+
{#each sorts as sort, index (index)}
|
|
65
|
+
<li class="line">
|
|
66
|
+
<select
|
|
67
|
+
class="control grow"
|
|
68
|
+
value={sort.propertyKey}
|
|
69
|
+
disabled={!canEdit}
|
|
70
|
+
aria-label={t('db_sort_property')}
|
|
71
|
+
onchange={(e) => patchAt(index, { propertyKey: e.currentTarget.value })}
|
|
72
|
+
>
|
|
73
|
+
{#each properties as option (option.key)}
|
|
74
|
+
<option value={option.key}>{option.name}</option>
|
|
75
|
+
{/each}
|
|
76
|
+
</select>
|
|
77
|
+
<select
|
|
78
|
+
class="control"
|
|
79
|
+
value={sort.direction}
|
|
80
|
+
disabled={!canEdit}
|
|
81
|
+
aria-label={t('db_sort_direction')}
|
|
82
|
+
onchange={(e) => patchAt(index, { direction: e.currentTarget.value as 'asc' | 'desc' })}
|
|
83
|
+
>
|
|
84
|
+
<option value="asc">{t('db_asc')}</option>
|
|
85
|
+
<option value="desc">{t('db_desc')}</option>
|
|
86
|
+
</select>
|
|
87
|
+
<button
|
|
88
|
+
type="button"
|
|
89
|
+
class="icon"
|
|
90
|
+
disabled={!canEdit || index === 0}
|
|
91
|
+
aria-label={t('db_move_up')}
|
|
92
|
+
onclick={() => move(index, -1)}
|
|
93
|
+
>
|
|
94
|
+
<Icon name="chevron-up" size={13} strokeWidth={1.9} />
|
|
95
|
+
</button>
|
|
96
|
+
<button
|
|
97
|
+
type="button"
|
|
98
|
+
class="icon"
|
|
99
|
+
disabled={!canEdit || index === sorts.length - 1}
|
|
100
|
+
aria-label={t('db_move_down')}
|
|
101
|
+
onclick={() => move(index, 1)}
|
|
102
|
+
>
|
|
103
|
+
<Icon name="chevron-down" size={13} strokeWidth={1.9} />
|
|
104
|
+
</button>
|
|
105
|
+
<button
|
|
106
|
+
type="button"
|
|
107
|
+
class="icon"
|
|
108
|
+
disabled={!canEdit}
|
|
109
|
+
aria-label={t('db_sort_remove')}
|
|
110
|
+
onclick={() => setSorts(sorts.filter((_, i) => i !== index))}
|
|
111
|
+
>
|
|
112
|
+
<Icon name="x" size={13} strokeWidth={1.9} />
|
|
113
|
+
</button>
|
|
114
|
+
</li>
|
|
115
|
+
{/each}
|
|
116
|
+
</ul>
|
|
117
|
+
{/if}
|
|
118
|
+
|
|
119
|
+
<div class="foot">
|
|
120
|
+
<Button size="sm" variant="secondary" disabled={!canEdit || properties.length === 0} onclick={addSort}>
|
|
121
|
+
{t('db_sort_add')}
|
|
122
|
+
</Button>
|
|
123
|
+
{#if sorts.length > 0}
|
|
124
|
+
<Button size="sm" variant="secondary" disabled={!canEdit} onclick={() => setSorts([])}>
|
|
125
|
+
{t('db_sort_clear')}
|
|
126
|
+
</Button>
|
|
127
|
+
{/if}
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
</Popover>
|
|
131
|
+
|
|
132
|
+
<style>
|
|
133
|
+
.panel {
|
|
134
|
+
padding: 12px;
|
|
135
|
+
display: flex;
|
|
136
|
+
flex-direction: column;
|
|
137
|
+
gap: 10px;
|
|
138
|
+
}
|
|
139
|
+
.empty {
|
|
140
|
+
margin: 0;
|
|
141
|
+
font-size: 12.5px;
|
|
142
|
+
color: var(--kern-ink-450);
|
|
143
|
+
}
|
|
144
|
+
.rows {
|
|
145
|
+
list-style: none;
|
|
146
|
+
margin: 0;
|
|
147
|
+
padding: 0;
|
|
148
|
+
display: flex;
|
|
149
|
+
flex-direction: column;
|
|
150
|
+
gap: 6px;
|
|
151
|
+
}
|
|
152
|
+
.line {
|
|
153
|
+
display: flex;
|
|
154
|
+
align-items: center;
|
|
155
|
+
gap: 4px;
|
|
156
|
+
}
|
|
157
|
+
.control {
|
|
158
|
+
min-width: 0;
|
|
159
|
+
height: 30px;
|
|
160
|
+
padding: 0 8px;
|
|
161
|
+
border: 1px solid var(--kern-border);
|
|
162
|
+
border-radius: var(--kern-r-md);
|
|
163
|
+
background: var(--kern-surface-raised);
|
|
164
|
+
color: var(--kern-ink-700);
|
|
165
|
+
font: inherit;
|
|
166
|
+
font-size: 12.5px;
|
|
167
|
+
}
|
|
168
|
+
.control:focus {
|
|
169
|
+
outline: none;
|
|
170
|
+
border-color: var(--kern-accent);
|
|
171
|
+
box-shadow: 0 0 0 3px var(--kern-ring);
|
|
172
|
+
}
|
|
173
|
+
.grow {
|
|
174
|
+
flex: 1;
|
|
175
|
+
}
|
|
176
|
+
.icon {
|
|
177
|
+
flex: none;
|
|
178
|
+
display: inline-grid;
|
|
179
|
+
place-items: center;
|
|
180
|
+
width: 28px;
|
|
181
|
+
height: 28px;
|
|
182
|
+
border: 0;
|
|
183
|
+
border-radius: var(--kern-r-sm);
|
|
184
|
+
background: none;
|
|
185
|
+
color: var(--kern-ink-450);
|
|
186
|
+
}
|
|
187
|
+
.icon:hover:not(:disabled) {
|
|
188
|
+
background: var(--kern-surface-hover);
|
|
189
|
+
color: var(--kern-ink-900);
|
|
190
|
+
}
|
|
191
|
+
.icon:disabled {
|
|
192
|
+
color: var(--kern-ink-300);
|
|
193
|
+
cursor: default;
|
|
194
|
+
}
|
|
195
|
+
.foot {
|
|
196
|
+
display: flex;
|
|
197
|
+
gap: 8px;
|
|
198
|
+
}
|
|
199
|
+
</style>
|