@happyvertical/smrt-content 0.43.3 → 0.43.4
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/AGENTS.md +86 -0
- package/dist/index.js +1 -1
- package/dist/manifest.json +1 -1
- package/dist/smrt-knowledge.json +5 -5
- package/dist/svelte/components/ContentList.svelte +669 -281
- package/dist/svelte/components/ContentList.svelte.d.ts +10 -1
- package/dist/svelte/components/ContentList.svelte.d.ts.map +1 -1
- package/dist/svelte/content-list-controller.d.ts +209 -0
- package/dist/svelte/content-list-controller.d.ts.map +1 -0
- package/dist/svelte/content-list-controller.js +650 -0
- package/dist/svelte/i18n.contribution.d.ts +31 -0
- package/dist/svelte/i18n.contribution.d.ts.map +1 -1
- package/dist/svelte/i18n.contribution.js +31 -0
- package/dist/svelte/index.d.ts +1 -0
- package/dist/svelte/index.d.ts.map +1 -1
- package/dist/svelte/index.js +3 -0
- package/package.json +14 -14
|
@@ -1,11 +1,37 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { DataTable, type DataTableColumn } from '@happyvertical/smrt-ui/data';
|
|
2
3
|
import { ConfirmDialog } from '@happyvertical/smrt-ui/feedback';
|
|
3
|
-
import { Input, Select } from '@happyvertical/smrt-ui/forms';
|
|
4
|
+
import { Checkbox, Input, Select } from '@happyvertical/smrt-ui/forms';
|
|
4
5
|
import { useI18n } from '@happyvertical/smrt-ui/i18n';
|
|
5
|
-
import { Button } from '@happyvertical/smrt-ui/ui';
|
|
6
|
+
import { Button, Pagination } from '@happyvertical/smrt-ui/ui';
|
|
6
7
|
import type { Snippet } from 'svelte';
|
|
7
8
|
import { untrack } from 'svelte';
|
|
8
9
|
import type { ContentData } from '../../mock-smrt-client.js';
|
|
10
|
+
import {
|
|
11
|
+
applyContentListFilter,
|
|
12
|
+
buildContentListColumns,
|
|
13
|
+
buildContentListSurfaceDescriptor,
|
|
14
|
+
CONTENT_LIST_ACTIONS_COLUMN_ID,
|
|
15
|
+
CONTENT_LIST_ROW_KEY,
|
|
16
|
+
CONTENT_LIST_SELECTION_COLUMN_ID,
|
|
17
|
+
CONTENT_LIST_STATUS_FILTER_ID,
|
|
18
|
+
CONTENT_LIST_TYPE_FILTER_ID,
|
|
19
|
+
type ContentListDataSurface,
|
|
20
|
+
type ContentListRow,
|
|
21
|
+
type ContentListViewMode,
|
|
22
|
+
contentListRowActions,
|
|
23
|
+
contentStateVariant,
|
|
24
|
+
contentStatusVariant,
|
|
25
|
+
createContentListController,
|
|
26
|
+
isContentListFilterExactly,
|
|
27
|
+
normalizeContentType,
|
|
28
|
+
paginateContentListRows,
|
|
29
|
+
readContentListFilter,
|
|
30
|
+
resolveContentHref,
|
|
31
|
+
selectableContentListRowIds,
|
|
32
|
+
selectContentListRows,
|
|
33
|
+
toContentListRows,
|
|
34
|
+
} from '../content-list-controller.js';
|
|
9
35
|
import { M } from '../i18n.contribution.js';
|
|
10
36
|
import ImageThumbnail from './ImageThumbnail.svelte';
|
|
11
37
|
|
|
@@ -15,12 +41,20 @@ interface Props {
|
|
|
15
41
|
apiBaseUrl?: string;
|
|
16
42
|
contents: ContentData[];
|
|
17
43
|
type?: string;
|
|
18
|
-
defaultViewMode?:
|
|
44
|
+
defaultViewMode?: ContentListViewMode;
|
|
19
45
|
onEdit: (content: ContentData) => void;
|
|
20
46
|
onDelete: (content: ContentData) => void;
|
|
21
47
|
onAdd: () => void;
|
|
22
48
|
controls?: Snippet;
|
|
23
49
|
getViewHref?: (content: ContentData) => string | null;
|
|
50
|
+
/** Announced uniformly by every presentation; #2455 extends it. */
|
|
51
|
+
loading?: boolean;
|
|
52
|
+
/** Load failure announced instead of the list. */
|
|
53
|
+
error?: string | null;
|
|
54
|
+
/** Retry affordance rendered with an error. */
|
|
55
|
+
onRetry?: () => void;
|
|
56
|
+
/** Opt-in agent addressability. Non-table presentations land with #2456. */
|
|
57
|
+
dataSurface?: ContentListDataSurface;
|
|
24
58
|
}
|
|
25
59
|
|
|
26
60
|
let {
|
|
@@ -33,154 +67,407 @@ let {
|
|
|
33
67
|
onAdd,
|
|
34
68
|
controls,
|
|
35
69
|
getViewHref = undefined,
|
|
70
|
+
loading = false,
|
|
71
|
+
error = null,
|
|
72
|
+
onRetry = undefined,
|
|
73
|
+
dataSurface = undefined,
|
|
36
74
|
}: Props = $props();
|
|
37
75
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
76
|
+
// One controller owns search, filters, sorting, paging, and selection for
|
|
77
|
+
// every presentation. The view mode lives beside it, so switching presentation
|
|
78
|
+
// never touches query or selection state.
|
|
79
|
+
// The seed is intentionally the initial `type`; the effect below keeps the
|
|
80
|
+
// locked filter in sync afterwards.
|
|
81
|
+
const controller = createContentListController({
|
|
82
|
+
type: untrack(() => type),
|
|
83
|
+
});
|
|
84
|
+
let snapshot = $state(controller.snapshot());
|
|
85
|
+
let viewMode: ContentListViewMode = $state(untrack(() => defaultViewMode));
|
|
86
|
+
let pendingDelete = $state<ContentListRow | null>(null);
|
|
87
|
+
|
|
88
|
+
$effect(() =>
|
|
89
|
+
controller.subscribe((transition) => {
|
|
90
|
+
snapshot = transition.next;
|
|
91
|
+
}),
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const tableState = $derived(snapshot.state);
|
|
95
|
+
|
|
96
|
+
/** The normalized type the `type` prop locks the list to, if any. */
|
|
97
|
+
const lockedType = $derived(type?.trim() ? normalizeContentType(type) : null);
|
|
98
|
+
|
|
99
|
+
// A `type` prop locks the type filter, exactly as the legacy select did. The
|
|
100
|
+
// lock is enforced against the live state, not only against the prop, because a
|
|
101
|
+
// data-surface `set-filters` or `reset` command can otherwise replace or clear
|
|
102
|
+
// it. The equality guard keeps the effect from dispatching in a loop.
|
|
103
|
+
$effect(() => {
|
|
104
|
+
const locked = lockedType;
|
|
105
|
+
if (locked === null) {
|
|
106
|
+
// Unlocked: the toolbar select owns the filter. Only reading `type` here
|
|
107
|
+
// keeps the legacy behaviour of clearing it when the prop is removed.
|
|
108
|
+
untrack(() =>
|
|
109
|
+
applyContentListFilter(controller, CONTENT_LIST_TYPE_FILTER_ID, null),
|
|
110
|
+
);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (
|
|
114
|
+
isContentListFilterExactly(tableState, CONTENT_LIST_TYPE_FILTER_ID, locked)
|
|
115
|
+
)
|
|
116
|
+
return;
|
|
117
|
+
untrack(() =>
|
|
118
|
+
applyContentListFilter(controller, CONTENT_LIST_TYPE_FILTER_ID, locked),
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const columnLabels = $derived({
|
|
123
|
+
type: t(M['content.content_list.column_type']),
|
|
124
|
+
title: t(M['content.content_list.column_title']),
|
|
125
|
+
author: t(M['content.content_list.column_author']),
|
|
126
|
+
status: t(M['content.content_list.column_status']),
|
|
127
|
+
state: t(M['content.content_list.column_state']),
|
|
128
|
+
publish: t(M['content.content_list.column_publish']),
|
|
129
|
+
updated: t(M['content.content_list.column_updated']),
|
|
130
|
+
site: t(M['content.content_list.column_site']),
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const queryColumns = $derived(buildContentListColumns(columnLabels));
|
|
134
|
+
const rows = $derived(toContentListRows(contents));
|
|
135
|
+
const queryRows = $derived(
|
|
136
|
+
selectContentListRows(rows, tableState, queryColumns),
|
|
43
137
|
);
|
|
138
|
+
const pageRows = $derived(paginateContentListRows(queryRows, tableState));
|
|
44
139
|
|
|
140
|
+
// The adapter owns filtering, sorting, and paging, so the controller's page has
|
|
141
|
+
// to be clamped against the adapter's result count rather than DataTable's.
|
|
45
142
|
$effect(() => {
|
|
46
|
-
|
|
143
|
+
const totalRows = queryRows.length;
|
|
144
|
+
untrack(() => controller.clampPage(totalRows));
|
|
47
145
|
});
|
|
48
146
|
|
|
49
|
-
|
|
50
|
-
|
|
147
|
+
// The card presentations have to render their own page controls: DataTable —
|
|
148
|
+
// and with it the page navigation — is only mounted in compact mode, so a page
|
|
149
|
+
// size set from a saved view or a surface command would otherwise strand the
|
|
150
|
+
// operator on page one.
|
|
151
|
+
const totalPages = $derived(
|
|
152
|
+
tableState.pageSize
|
|
153
|
+
? Math.max(1, Math.ceil(queryRows.length / tableState.pageSize))
|
|
154
|
+
: 1,
|
|
155
|
+
);
|
|
156
|
+
const showPagination = $derived(Boolean(tableState.pageSize) && totalPages > 1);
|
|
157
|
+
/** Rows are already rendered, so a load is a refresh rather than a first fill. */
|
|
158
|
+
const refreshing = $derived(loading && pageRows.length > 0);
|
|
159
|
+
|
|
160
|
+
const selectedRowKeys = $derived(
|
|
161
|
+
new Set(tableState.selectedRowIds.map((rowId) => String(rowId))),
|
|
162
|
+
);
|
|
163
|
+
// Only durable rows may be addressed by a selection.
|
|
164
|
+
const identifiedRowKeys = $derived(
|
|
165
|
+
new Set(selectableContentListRowIds(rows).map((rowId) => String(rowId))),
|
|
166
|
+
);
|
|
167
|
+
const selectablePageRowIds = $derived(selectableContentListRowIds(pageRows));
|
|
168
|
+
const allPageSelected = $derived(
|
|
169
|
+
selectablePageRowIds.length > 0 &&
|
|
170
|
+
selectablePageRowIds.every((rowId) => selectedRowKeys.has(String(rowId))),
|
|
171
|
+
);
|
|
172
|
+
const somePageSelected = $derived(
|
|
173
|
+
!allPageSelected &&
|
|
174
|
+
selectablePageRowIds.some((rowId) => selectedRowKeys.has(String(rowId))),
|
|
175
|
+
);
|
|
176
|
+
const selectedCount = $derived(tableState.selectedRowIds.length);
|
|
177
|
+
|
|
178
|
+
// DataTable's own selection column and data-surface commands can both introduce
|
|
179
|
+
// ids for rows that carry no durable identity. Normalizing here covers every
|
|
180
|
+
// path at once; re-dispatching only on a real difference keeps it settling.
|
|
181
|
+
$effect(() => {
|
|
182
|
+
const selected = tableState.selectedRowIds;
|
|
183
|
+
const durable = selected.filter((rowId) =>
|
|
184
|
+
identifiedRowKeys.has(String(rowId)),
|
|
185
|
+
);
|
|
186
|
+
if (durable.length === selected.length) return;
|
|
187
|
+
untrack(() =>
|
|
188
|
+
controller.dispatch({ type: 'setSelectedRows', rowIds: durable }),
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const selectedType = $derived(
|
|
193
|
+
readContentListFilter(tableState, CONTENT_LIST_TYPE_FILTER_ID) ?? '',
|
|
194
|
+
);
|
|
195
|
+
const selectedStatus = $derived(
|
|
196
|
+
readContentListFilter(tableState, CONTENT_LIST_STATUS_FILTER_ID) ?? '',
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
const surfaceOptions = $derived(
|
|
200
|
+
dataSurface
|
|
201
|
+
? {
|
|
202
|
+
registry: dataSurface.registry,
|
|
203
|
+
descriptor:
|
|
204
|
+
dataSurface.descriptor ??
|
|
205
|
+
buildContentListSurfaceDescriptor({ columnLabels }),
|
|
206
|
+
}
|
|
207
|
+
: undefined,
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
function isSelected(row: ContentListRow): boolean {
|
|
211
|
+
return selectedRowKeys.has(String(row.id));
|
|
51
212
|
}
|
|
52
213
|
|
|
53
|
-
function
|
|
54
|
-
|
|
214
|
+
function toggleRow(row: ContentListRow) {
|
|
215
|
+
if (!row.identified) return;
|
|
216
|
+
controller.dispatch({ type: 'toggleRowSelection', rowId: row.id });
|
|
55
217
|
}
|
|
56
218
|
|
|
57
|
-
function
|
|
58
|
-
|
|
219
|
+
function togglePageSelection() {
|
|
220
|
+
const remaining = tableState.selectedRowIds.filter(
|
|
221
|
+
(rowId) =>
|
|
222
|
+
!selectablePageRowIds.some(
|
|
223
|
+
(pageRowId) => String(pageRowId) === String(rowId),
|
|
224
|
+
),
|
|
225
|
+
);
|
|
226
|
+
controller.dispatch({
|
|
227
|
+
type: 'setSelectedRows',
|
|
228
|
+
rowIds: allPageSelected
|
|
229
|
+
? remaining
|
|
230
|
+
: [...remaining, ...selectablePageRowIds],
|
|
231
|
+
});
|
|
59
232
|
}
|
|
60
233
|
|
|
61
|
-
function
|
|
62
|
-
|
|
234
|
+
function clearSelection() {
|
|
235
|
+
controller.dispatch({ type: 'setSelectedRows', rowIds: [] });
|
|
63
236
|
}
|
|
64
237
|
|
|
65
|
-
function
|
|
66
|
-
|
|
238
|
+
function handlePageChange(page: number) {
|
|
239
|
+
controller.dispatch({ type: 'setPage', page });
|
|
67
240
|
}
|
|
68
241
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const description = getDisplayDescription(content);
|
|
73
|
-
const author = getDisplayAuthor(content);
|
|
74
|
-
|
|
75
|
-
const matchesSearch =
|
|
76
|
-
searchTerm === '' ||
|
|
77
|
-
title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
78
|
-
description.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
79
|
-
author.toLowerCase().includes(searchTerm.toLowerCase());
|
|
80
|
-
|
|
81
|
-
const isLockedType = !!type;
|
|
82
|
-
const matchesType = isLockedType
|
|
83
|
-
? content.type === type
|
|
84
|
-
: selectedType === 'All Types' ||
|
|
85
|
-
(selectedType === 'Articles' && content.type === 'article') ||
|
|
86
|
-
(selectedType === 'Documents' && content.type === 'document') ||
|
|
87
|
-
(selectedType === 'Mirrors' && content.type === 'mirror');
|
|
88
|
-
|
|
89
|
-
const matchesStatus =
|
|
90
|
-
selectedStatus === 'All Statuses' ||
|
|
91
|
-
content.status.toLowerCase() === selectedStatus.toLowerCase();
|
|
92
|
-
|
|
93
|
-
return matchesSearch && matchesType && matchesStatus;
|
|
94
|
-
}),
|
|
95
|
-
);
|
|
242
|
+
function handleSearch(value: string) {
|
|
243
|
+
controller.dispatch({ type: 'setSearch', search: value });
|
|
244
|
+
}
|
|
96
245
|
|
|
97
|
-
function
|
|
98
|
-
|
|
99
|
-
case 'article':
|
|
100
|
-
return 'Article';
|
|
101
|
-
case 'mirror':
|
|
102
|
-
return 'Mirror';
|
|
103
|
-
case 'document':
|
|
104
|
-
return 'Document';
|
|
105
|
-
default:
|
|
106
|
-
return 'Content';
|
|
107
|
-
}
|
|
246
|
+
function handleFilter(columnId: string, value: string) {
|
|
247
|
+
applyContentListFilter(controller, columnId, value || null);
|
|
108
248
|
}
|
|
109
249
|
|
|
110
|
-
function
|
|
111
|
-
|
|
112
|
-
case 'published':
|
|
113
|
-
return 'published';
|
|
114
|
-
case 'draft':
|
|
115
|
-
return 'draft';
|
|
116
|
-
case 'archived':
|
|
117
|
-
return 'archived';
|
|
118
|
-
default:
|
|
119
|
-
return 'unknown';
|
|
120
|
-
}
|
|
250
|
+
function rowActions(row: ContentListRow) {
|
|
251
|
+
return contentListRowActions(row, { getViewHref });
|
|
121
252
|
}
|
|
122
253
|
|
|
123
|
-
function
|
|
124
|
-
|
|
125
|
-
case 'highlighted':
|
|
126
|
-
return 'highlighted';
|
|
127
|
-
case 'active':
|
|
128
|
-
return 'active';
|
|
129
|
-
case 'deprecated':
|
|
130
|
-
return 'deprecated';
|
|
131
|
-
default:
|
|
132
|
-
return 'unknown';
|
|
133
|
-
}
|
|
254
|
+
function viewHref(row: ContentListRow): string | null {
|
|
255
|
+
return resolveContentHref(row.content, getViewHref);
|
|
134
256
|
}
|
|
135
257
|
|
|
136
|
-
|
|
258
|
+
function selectRowLabel(row: ContentListRow): string {
|
|
259
|
+
return isSelected(row)
|
|
260
|
+
? t(M['content.content_list.deselect_row'], { title: row.title })
|
|
261
|
+
: t(M['content.content_list.select_row'], { title: row.title });
|
|
262
|
+
}
|
|
137
263
|
|
|
138
|
-
function handleDeleteContent(
|
|
139
|
-
pendingDelete =
|
|
264
|
+
function handleDeleteContent(row: ContentListRow) {
|
|
265
|
+
pendingDelete = row;
|
|
140
266
|
}
|
|
141
267
|
|
|
142
268
|
function confirmDelete() {
|
|
143
269
|
const target = pendingDelete;
|
|
144
270
|
pendingDelete = null;
|
|
145
271
|
if (target) {
|
|
146
|
-
onDelete(target);
|
|
272
|
+
onDelete(target.content);
|
|
147
273
|
}
|
|
148
274
|
}
|
|
149
275
|
|
|
150
276
|
function cancelDelete() {
|
|
151
277
|
pendingDelete = null;
|
|
152
278
|
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Compact mode renders the shared columns with content-specific cells.
|
|
282
|
+
*
|
|
283
|
+
* Selection is a content-owned column rather than DataTable's built-in one:
|
|
284
|
+
* DataTable has no per-row selection predicate, so its header select-all would
|
|
285
|
+
* address the synthetic id of an unidentified row, which the normalization
|
|
286
|
+
* effect then strips — leaving the header permanently indeterminate. Owning the
|
|
287
|
+
* column keeps compact select-all identical to the card presentations.
|
|
288
|
+
*/
|
|
289
|
+
const tableColumns: DataTableColumn<ContentListRow>[] = $derived([
|
|
290
|
+
{
|
|
291
|
+
id: CONTENT_LIST_SELECTION_COLUMN_ID,
|
|
292
|
+
label: t(M['content.content_list.select_all']),
|
|
293
|
+
role: 'action',
|
|
294
|
+
align: 'center',
|
|
295
|
+
width: '3rem',
|
|
296
|
+
sortable: false,
|
|
297
|
+
searchable: false,
|
|
298
|
+
filterable: false,
|
|
299
|
+
header: selectHeader,
|
|
300
|
+
cell: selectCell,
|
|
301
|
+
},
|
|
302
|
+
...queryColumns.map((column) => {
|
|
303
|
+
if (column.id === 'type') return { ...column, cell: typeCell };
|
|
304
|
+
if (column.id === 'title') return { ...column, cell: titleCell };
|
|
305
|
+
if (column.id === 'status') return { ...column, cell: statusCell };
|
|
306
|
+
if (column.id === 'state') return { ...column, cell: stateCell };
|
|
307
|
+
if (column.id === 'publish') return { ...column, cell: publishCell };
|
|
308
|
+
if (column.id === 'updated') return { ...column, cell: updatedCell };
|
|
309
|
+
return column;
|
|
310
|
+
}),
|
|
311
|
+
{
|
|
312
|
+
id: CONTENT_LIST_ACTIONS_COLUMN_ID,
|
|
313
|
+
label: t(M['content.content_list.actions_column']),
|
|
314
|
+
role: 'action',
|
|
315
|
+
align: 'right',
|
|
316
|
+
sortable: false,
|
|
317
|
+
searchable: false,
|
|
318
|
+
filterable: false,
|
|
319
|
+
cell: actionsCell,
|
|
320
|
+
},
|
|
321
|
+
]);
|
|
153
322
|
</script>
|
|
154
323
|
|
|
324
|
+
{#snippet tableEmptyState()}
|
|
325
|
+
<p class="table-empty-state">{t(M['content.content_list.empty'])}</p>
|
|
326
|
+
{/snippet}
|
|
327
|
+
|
|
328
|
+
{#snippet selectHeader()}
|
|
329
|
+
<Checkbox
|
|
330
|
+
checked={allPageSelected}
|
|
331
|
+
indeterminate={somePageSelected}
|
|
332
|
+
aria-label={t(M['content.content_list.select_all'])}
|
|
333
|
+
onchange={togglePageSelection}
|
|
334
|
+
/>
|
|
335
|
+
{/snippet}
|
|
336
|
+
|
|
337
|
+
{#snippet selectCell({ row }: { row: ContentListRow })}
|
|
338
|
+
<Checkbox
|
|
339
|
+
checked={isSelected(row)}
|
|
340
|
+
disabled={!row.identified}
|
|
341
|
+
aria-label={selectRowLabel(row)}
|
|
342
|
+
title={row.identified
|
|
343
|
+
? undefined
|
|
344
|
+
: t(M['content.content_list.row_not_selectable'])}
|
|
345
|
+
onchange={() => toggleRow(row)}
|
|
346
|
+
/>
|
|
347
|
+
{/snippet}
|
|
348
|
+
|
|
349
|
+
{#snippet typeCell({ row }: { row: ContentListRow })}
|
|
350
|
+
<span class={`type-pill type-pill--${row.type}`}>{row.typeLabel}</span>
|
|
351
|
+
{/snippet}
|
|
352
|
+
|
|
353
|
+
{#snippet titleCell({ row }: { row: ContentListRow })}
|
|
354
|
+
{#if viewHref(row)}
|
|
355
|
+
<a class="title-link" href={viewHref(row)}>{row.title}</a>
|
|
356
|
+
{:else}
|
|
357
|
+
<strong>{row.title}</strong>
|
|
358
|
+
{/if}
|
|
359
|
+
{/snippet}
|
|
360
|
+
|
|
361
|
+
{#snippet statusCell({ row }: { row: ContentListRow })}
|
|
362
|
+
<span class="badge status-{contentStatusVariant(row.status)}">{row.statusLabel}</span>
|
|
363
|
+
{/snippet}
|
|
364
|
+
|
|
365
|
+
{#snippet stateCell({ row }: { row: ContentListRow })}
|
|
366
|
+
<span class="badge state-{contentStateVariant(row.state)}">{row.stateLabel}</span>
|
|
367
|
+
{/snippet}
|
|
368
|
+
|
|
369
|
+
{#snippet publishCell({ row }: { row: ContentListRow })}
|
|
370
|
+
{row.publishLabel || '-'}
|
|
371
|
+
{/snippet}
|
|
372
|
+
|
|
373
|
+
{#snippet updatedCell({ row }: { row: ContentListRow })}
|
|
374
|
+
{row.updatedLabel || '-'}
|
|
375
|
+
{/snippet}
|
|
376
|
+
|
|
377
|
+
{#snippet actionsCell({ row }: { row: ContentListRow })}
|
|
378
|
+
{@const actions = rowActions(row)}
|
|
379
|
+
<div class="actions-cell">
|
|
380
|
+
{#if actions.includes('view')}
|
|
381
|
+
<a
|
|
382
|
+
class="icon-btn"
|
|
383
|
+
href={viewHref(row)}
|
|
384
|
+
title={t(M['content.content_list.view_published_article'])}
|
|
385
|
+
aria-label={t(M['content.content_list.view_published_article'])}
|
|
386
|
+
>
|
|
387
|
+
<span aria-hidden="true">🔎</span>
|
|
388
|
+
</a>
|
|
389
|
+
{/if}
|
|
390
|
+
{#if actions.includes('edit')}
|
|
391
|
+
<Button
|
|
392
|
+
variant="ghost"
|
|
393
|
+
size="sm"
|
|
394
|
+
class="icon-btn"
|
|
395
|
+
type="button"
|
|
396
|
+
onclick={() => onEdit(row.content)}
|
|
397
|
+
title={t(M['content.content_list.edit'])}
|
|
398
|
+
aria-label={t(M['content.content_list.edit'])}
|
|
399
|
+
>
|
|
400
|
+
<span aria-hidden="true">✏️</span>
|
|
401
|
+
</Button>
|
|
402
|
+
{/if}
|
|
403
|
+
{#if actions.includes('delete')}
|
|
404
|
+
<Button
|
|
405
|
+
variant="ghost"
|
|
406
|
+
size="sm"
|
|
407
|
+
class="icon-btn delete-icon"
|
|
408
|
+
type="button"
|
|
409
|
+
onclick={() => handleDeleteContent(row)}
|
|
410
|
+
title={t(M['content.content_list.delete'])}
|
|
411
|
+
aria-label={t(M['content.content_list.delete'])}
|
|
412
|
+
>
|
|
413
|
+
<span aria-hidden="true">🗑️</span>
|
|
414
|
+
</Button>
|
|
415
|
+
{/if}
|
|
416
|
+
</div>
|
|
417
|
+
{/snippet}
|
|
418
|
+
|
|
155
419
|
<div class="content-list-wrapper">
|
|
156
|
-
|
|
420
|
+
|
|
157
421
|
<div class="content-controls">
|
|
158
422
|
<div class="search-filters">
|
|
159
|
-
<Input
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
423
|
+
<Input
|
|
424
|
+
type="text"
|
|
425
|
+
placeholder={t(M['content.content_list.search_placeholder'])}
|
|
426
|
+
aria-label={t(M['content.content_list.search_label'])}
|
|
427
|
+
value={tableState.search}
|
|
428
|
+
oninput={(event: Event) =>
|
|
429
|
+
handleSearch((event.currentTarget as HTMLInputElement).value)}
|
|
430
|
+
/>
|
|
431
|
+
|
|
432
|
+
{#if !lockedType}
|
|
433
|
+
<Select
|
|
434
|
+
aria-label={t(M['content.content_list.filter_type'])}
|
|
435
|
+
value={selectedType}
|
|
436
|
+
onchange={(event: Event) =>
|
|
437
|
+
handleFilter(
|
|
438
|
+
CONTENT_LIST_TYPE_FILTER_ID,
|
|
439
|
+
(event.currentTarget as HTMLSelectElement).value,
|
|
440
|
+
)}
|
|
441
|
+
>
|
|
442
|
+
<option value="">{t(M['content.content_list.all_types'])}</option>
|
|
443
|
+
<option value="article">{t(M['content.content_list.type_articles'])}</option>
|
|
444
|
+
<option value="document">{t(M['content.content_list.type_documents'])}</option>
|
|
445
|
+
<option value="mirror">{t(M['content.content_list.type_mirrors'])}</option>
|
|
167
446
|
</Select>
|
|
168
447
|
{/if}
|
|
169
448
|
|
|
170
|
-
<Select
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
449
|
+
<Select
|
|
450
|
+
aria-label={t(M['content.content_list.filter_status'])}
|
|
451
|
+
value={selectedStatus}
|
|
452
|
+
onchange={(event: Event) =>
|
|
453
|
+
handleFilter(
|
|
454
|
+
CONTENT_LIST_STATUS_FILTER_ID,
|
|
455
|
+
(event.currentTarget as HTMLSelectElement).value,
|
|
456
|
+
)}
|
|
457
|
+
>
|
|
458
|
+
<option value="">{t(M['content.content_list.all_statuses'])}</option>
|
|
459
|
+
<option value="published">{t(M['content.content_list.status_published'])}</option>
|
|
460
|
+
<option value="draft">{t(M['content.content_list.status_draft'])}</option>
|
|
461
|
+
<option value="archived">{t(M['content.content_list.status_archived'])}</option>
|
|
175
462
|
</Select>
|
|
176
|
-
|
|
463
|
+
|
|
177
464
|
{#if controls}
|
|
178
465
|
{@render controls()}
|
|
179
466
|
{/if}
|
|
180
467
|
</div>
|
|
181
|
-
|
|
468
|
+
|
|
182
469
|
<div class="actions-group">
|
|
183
|
-
<div class="view-toggles">
|
|
470
|
+
<div class="view-toggles" role="group" aria-label={t(M['content.content_list.view_mode'])}>
|
|
184
471
|
<Button
|
|
185
472
|
variant="ghost"
|
|
186
473
|
size="sm"
|
|
@@ -191,7 +478,7 @@ function cancelDelete() {
|
|
|
191
478
|
aria-label={t(M['content.content_list.grid_view'])}
|
|
192
479
|
title={t(M['content.content_list.grid_view'])}
|
|
193
480
|
>
|
|
194
|
-
<svg viewBox="0 0 24 24" width="18" height="18" stroke="currentColor" stroke-width="2" fill="none">
|
|
481
|
+
<svg viewBox="0 0 24 24" width="18" height="18" stroke="currentColor" stroke-width="2" fill="none" aria-hidden="true">
|
|
195
482
|
<rect x="3" y="3" width="7" height="7"></rect>
|
|
196
483
|
<rect x="14" y="3" width="7" height="7"></rect>
|
|
197
484
|
<rect x="14" y="14" width="7" height="7"></rect>
|
|
@@ -208,7 +495,7 @@ function cancelDelete() {
|
|
|
208
495
|
aria-label={t(M['content.content_list.detailed_list'])}
|
|
209
496
|
title={t(M['content.content_list.detailed_list'])}
|
|
210
497
|
>
|
|
211
|
-
<svg viewBox="0 0 24 24" width="18" height="18" stroke="currentColor" stroke-width="2" fill="none">
|
|
498
|
+
<svg viewBox="0 0 24 24" width="18" height="18" stroke="currentColor" stroke-width="2" fill="none" aria-hidden="true">
|
|
212
499
|
<line x1="8" y1="6" x2="21" y2="6"></line>
|
|
213
500
|
<line x1="8" y1="12" x2="21" y2="12"></line>
|
|
214
501
|
<line x1="8" y1="18" x2="21" y2="18"></line>
|
|
@@ -227,7 +514,7 @@ function cancelDelete() {
|
|
|
227
514
|
aria-label={t(M['content.content_list.compact_list'])}
|
|
228
515
|
title={t(M['content.content_list.compact_list'])}
|
|
229
516
|
>
|
|
230
|
-
<svg viewBox="0 0 24 24" width="18" height="18" stroke="currentColor" stroke-width="2" fill="none">
|
|
517
|
+
<svg viewBox="0 0 24 24" width="18" height="18" stroke="currentColor" stroke-width="2" fill="none" aria-hidden="true">
|
|
231
518
|
<line x1="3" y1="6" x2="21" y2="6"></line>
|
|
232
519
|
<line x1="3" y1="12" x2="21" y2="12"></line>
|
|
233
520
|
<line x1="3" y1="18" x2="21" y2="18"></line>
|
|
@@ -236,7 +523,7 @@ function cancelDelete() {
|
|
|
236
523
|
</div>
|
|
237
524
|
|
|
238
525
|
<Button variant="ghost" class="add-button" type="button" onclick={() => onAdd()}>
|
|
239
|
-
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
|
|
526
|
+
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none" aria-hidden="true">
|
|
240
527
|
<line x1="12" y1="5" x2="12" y2="19"></line>
|
|
241
528
|
<line x1="5" y1="12" x2="19" y2="12"></line>
|
|
242
529
|
</svg>
|
|
@@ -245,160 +532,235 @@ function cancelDelete() {
|
|
|
245
532
|
</div>
|
|
246
533
|
</div>
|
|
247
534
|
|
|
248
|
-
{#if
|
|
249
|
-
<div class="
|
|
250
|
-
{t(M['content.content_list.
|
|
535
|
+
{#if error}
|
|
536
|
+
<div class="state-panel state-panel--error" role="alert">
|
|
537
|
+
<p class="state-panel__title">{t(M['content.content_list.error_title'])}</p>
|
|
538
|
+
<p class="state-panel__detail">{error}</p>
|
|
539
|
+
{#if onRetry}
|
|
540
|
+
<Button variant="ghost" type="button" class="retry-button" onclick={() => onRetry?.()}>
|
|
541
|
+
{t(M['content.content_list.retry'])}
|
|
542
|
+
</Button>
|
|
543
|
+
{/if}
|
|
251
544
|
</div>
|
|
252
|
-
{:else
|
|
253
|
-
|
|
254
|
-
<
|
|
255
|
-
|
|
256
|
-
<
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
545
|
+
{:else}
|
|
546
|
+
{#if pageRows.length > 0 || selectedCount > 0}
|
|
547
|
+
<div class="content-selection">
|
|
548
|
+
{#if viewMode !== 'compact'}
|
|
549
|
+
<Checkbox
|
|
550
|
+
checked={allPageSelected}
|
|
551
|
+
indeterminate={somePageSelected}
|
|
552
|
+
aria-label={t(M['content.content_list.select_all'])}
|
|
553
|
+
onchange={togglePageSelection}
|
|
554
|
+
/>
|
|
555
|
+
{/if}
|
|
556
|
+
<span class="content-selection__count" aria-live="polite">
|
|
557
|
+
{t(M['content.content_list.selection_count'], { count: selectedCount })}
|
|
558
|
+
</span>
|
|
559
|
+
{#if selectedCount > 0}
|
|
560
|
+
<Button variant="ghost" size="sm" type="button" class="clear-selection" onclick={clearSelection}>
|
|
561
|
+
{t(M['content.content_list.clear_selection'])}
|
|
562
|
+
</Button>
|
|
563
|
+
{/if}
|
|
564
|
+
</div>
|
|
565
|
+
{/if}
|
|
566
|
+
|
|
567
|
+
{#if refreshing && viewMode !== 'compact'}
|
|
568
|
+
<!-- DataTable announces its own refresh; the card views need their own. -->
|
|
569
|
+
<p class="content-refreshing" role="status" aria-live="polite">
|
|
570
|
+
{t(M['content.content_list.refreshing'])}
|
|
571
|
+
</p>
|
|
572
|
+
{/if}
|
|
573
|
+
|
|
574
|
+
{#if viewMode === 'compact'}
|
|
575
|
+
<!--
|
|
576
|
+
The compact table stays mounted for empty and loading results: it owns
|
|
577
|
+
the mounted data surface, so unmounting it on a zero-row query would
|
|
578
|
+
unregister the surface and leave an agent unable to undo its own search.
|
|
579
|
+
-->
|
|
580
|
+
<div class="content-table-wrapper">
|
|
581
|
+
<DataTable
|
|
582
|
+
data={pageRows}
|
|
583
|
+
totalRows={queryRows.length}
|
|
584
|
+
columns={tableColumns}
|
|
585
|
+
rowKey={CONTENT_LIST_ROW_KEY}
|
|
586
|
+
{controller}
|
|
587
|
+
sortable
|
|
588
|
+
agentAddressable
|
|
589
|
+
{loading}
|
|
590
|
+
caption={t(M['content.content_list.table_caption'])}
|
|
591
|
+
rowLabel={(row: ContentListRow) => row.title}
|
|
592
|
+
dataSurface={surfaceOptions}
|
|
593
|
+
empty={tableEmptyState}
|
|
594
|
+
/>
|
|
595
|
+
</div>
|
|
596
|
+
{:else if loading && pageRows.length === 0}
|
|
597
|
+
<div class="state-panel" role="status">
|
|
598
|
+
{t(M['content.content_list.loading'])}
|
|
599
|
+
</div>
|
|
600
|
+
{:else if pageRows.length === 0}
|
|
601
|
+
<div class="state-panel empty-state">
|
|
602
|
+
{t(M['content.content_list.empty'])}
|
|
603
|
+
</div>
|
|
604
|
+
{:else if viewMode === 'detailed'}
|
|
605
|
+
<div class="content-detailed">
|
|
606
|
+
{#each pageRows as row (row.id)}
|
|
607
|
+
{@const content = row.content}
|
|
608
|
+
{@const actions = rowActions(row)}
|
|
609
|
+
<article class="content-row">
|
|
610
|
+
<div class="content-row__select">
|
|
611
|
+
<Checkbox
|
|
612
|
+
checked={isSelected(row)}
|
|
613
|
+
disabled={!row.identified}
|
|
614
|
+
aria-label={selectRowLabel(row)}
|
|
615
|
+
title={row.identified
|
|
616
|
+
? undefined
|
|
617
|
+
: t(M['content.content_list.row_not_selectable'])}
|
|
618
|
+
onchange={() => toggleRow(row)}
|
|
619
|
+
/>
|
|
620
|
+
</div>
|
|
621
|
+
|
|
622
|
+
<div class="content-row__main">
|
|
623
|
+
<div class="content-row__eyebrow">
|
|
624
|
+
<span class={`type-pill type-pill--${row.type}`}>{row.typeLabel}</span>
|
|
625
|
+
{#if row.author}
|
|
626
|
+
<span class="content-row__author">By {row.author}</span>
|
|
280
627
|
{/if}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
</
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
628
|
+
</div>
|
|
629
|
+
|
|
630
|
+
<h3>{row.title}</h3>
|
|
631
|
+
|
|
632
|
+
{#if row.description}
|
|
633
|
+
<p class="content-row__description">{row.description}</p>
|
|
634
|
+
{/if}
|
|
635
|
+
|
|
636
|
+
{#if content.url || content.fileKey}
|
|
637
|
+
<div class="content-row__links">
|
|
638
|
+
{#if content.url}
|
|
639
|
+
<a href={content.url} target="_blank" rel="noreferrer">
|
|
640
|
+
{t(M['content.content_list.source_material'])}
|
|
641
|
+
</a>
|
|
642
|
+
{/if}
|
|
643
|
+
{#if content.fileKey}
|
|
644
|
+
<span>{content.fileKey}</span>
|
|
645
|
+
{/if}
|
|
646
|
+
</div>
|
|
300
647
|
{/if}
|
|
301
648
|
</div>
|
|
302
649
|
|
|
303
|
-
<
|
|
650
|
+
<div class="content-row__meta">
|
|
651
|
+
<span class="meta-label">{t(M['content.content_list.column_status'])}</span>
|
|
652
|
+
<span class="badge status-{contentStatusVariant(row.status)}">{row.statusLabel}</span>
|
|
653
|
+
<span class="meta-label">{t(M['content.content_list.column_state'])}</span>
|
|
654
|
+
<span class="badge state-{contentStateVariant(row.state)}">{row.stateLabel}</span>
|
|
655
|
+
</div>
|
|
304
656
|
|
|
305
|
-
|
|
306
|
-
|
|
657
|
+
<div class="content-row__actions">
|
|
658
|
+
{#if actions.includes('view')}
|
|
659
|
+
<a href={viewHref(row)} class="quiet-action">{t(M['content.content_list.view_article'])}</a>
|
|
660
|
+
{/if}
|
|
661
|
+
{#if actions.includes('edit')}
|
|
662
|
+
<Button variant="ghost" type="button" class="quiet-action" onclick={() => onEdit(content)}>
|
|
663
|
+
{t(M['content.content_list.edit'])}
|
|
664
|
+
</Button>
|
|
665
|
+
{/if}
|
|
666
|
+
{#if actions.includes('delete')}
|
|
667
|
+
<Button
|
|
668
|
+
variant="ghost"
|
|
669
|
+
type="button"
|
|
670
|
+
class="quiet-action quiet-action--danger"
|
|
671
|
+
onclick={() => handleDeleteContent(row)}
|
|
672
|
+
>
|
|
673
|
+
{t(M['content.content_list.delete'])}
|
|
674
|
+
</Button>
|
|
675
|
+
{/if}
|
|
676
|
+
</div>
|
|
677
|
+
</article>
|
|
678
|
+
{/each}
|
|
679
|
+
</div>
|
|
680
|
+
{:else}
|
|
681
|
+
<div class="content-grid">
|
|
682
|
+
{#each pageRows as row (row.id)}
|
|
683
|
+
{@const content = row.content}
|
|
684
|
+
{@const actions = rowActions(row)}
|
|
685
|
+
<div class="content-card">
|
|
686
|
+
{#if content.thumbnailAssetId}
|
|
687
|
+
<div class="card-thumbnail">
|
|
688
|
+
<ImageThumbnail
|
|
689
|
+
apiBaseUrl={apiBaseUrl}
|
|
690
|
+
assetId={content.thumbnailAssetId}
|
|
691
|
+
/>
|
|
692
|
+
</div>
|
|
307
693
|
{/if}
|
|
694
|
+
<div class="content-header">
|
|
695
|
+
<div class="content-header__eyebrow">
|
|
696
|
+
<Checkbox
|
|
697
|
+
checked={isSelected(row)}
|
|
698
|
+
disabled={!row.identified}
|
|
699
|
+
aria-label={selectRowLabel(row)}
|
|
700
|
+
title={row.identified
|
|
701
|
+
? undefined
|
|
702
|
+
: t(M['content.content_list.row_not_selectable'])}
|
|
703
|
+
onchange={() => toggleRow(row)}
|
|
704
|
+
/>
|
|
705
|
+
<span class={`type-pill type-pill--${row.type}`}>{row.typeLabel}</span>
|
|
706
|
+
{#if row.author}
|
|
707
|
+
<div class="author">{row.author}</div>
|
|
708
|
+
{/if}
|
|
709
|
+
</div>
|
|
710
|
+
<h3>{row.title}</h3>
|
|
711
|
+
</div>
|
|
308
712
|
|
|
309
|
-
|
|
310
|
-
<div
|
|
713
|
+
<div class="content-meta">
|
|
714
|
+
<div>{row.typeLabel}</div>
|
|
715
|
+
<div class="badges">
|
|
716
|
+
<span class="badge status-{contentStatusVariant(row.status)}">{row.statusLabel}</span>
|
|
717
|
+
<span class="badge state-{contentStateVariant(row.state)}">{row.stateLabel}</span>
|
|
718
|
+
</div>
|
|
719
|
+
</div>
|
|
720
|
+
|
|
721
|
+
<p class="content-description">{row.description}</p>
|
|
722
|
+
|
|
723
|
+
<div class="content-footer">
|
|
724
|
+
<div class="meta-links">
|
|
311
725
|
{#if content.url}
|
|
312
|
-
<a href={content.url} target="_blank" rel="noreferrer">
|
|
313
|
-
{t(M['content.content_list.source_material'])}
|
|
314
|
-
</a>
|
|
726
|
+
<div class="source">Source: <a href={content.url} target="_blank" rel="noreferrer">{content.url}</a></div>
|
|
315
727
|
{/if}
|
|
316
728
|
{#if content.fileKey}
|
|
317
|
-
<
|
|
729
|
+
<div class="file">File: {content.fileKey}</div>
|
|
318
730
|
{/if}
|
|
319
731
|
</div>
|
|
320
|
-
{/if}
|
|
321
|
-
</div>
|
|
322
|
-
|
|
323
|
-
<div class="content-row__meta">
|
|
324
|
-
<span class="meta-label">Status</span>
|
|
325
|
-
<span class="badge status-{getStatusBadge(content.status)}">{content.status}</span>
|
|
326
|
-
<span class="meta-label">State</span>
|
|
327
|
-
<span class="badge state-{getStateBadge(content.state)}">{content.state}</span>
|
|
328
|
-
</div>
|
|
329
732
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
{/each}
|
|
346
|
-
</div>
|
|
347
|
-
{:else}
|
|
348
|
-
<div class="content-{viewMode}">
|
|
349
|
-
{#each filteredContents as content (content.id)}
|
|
350
|
-
<div class="content-card">
|
|
351
|
-
{#if content.thumbnailAssetId}
|
|
352
|
-
<div class="card-thumbnail">
|
|
353
|
-
<ImageThumbnail
|
|
354
|
-
apiBaseUrl={apiBaseUrl}
|
|
355
|
-
assetId={content.thumbnailAssetId}
|
|
356
|
-
/>
|
|
357
|
-
</div>
|
|
358
|
-
{/if}
|
|
359
|
-
<div class="content-header">
|
|
360
|
-
<div class="content-header__eyebrow">
|
|
361
|
-
<span class={`type-pill type-pill--${getNormalizedType(content.type)}`}>
|
|
362
|
-
{getTypeLabel(content.type)}
|
|
363
|
-
</span>
|
|
364
|
-
{#if getDisplayAuthor(content)}
|
|
365
|
-
<div class="author">{getDisplayAuthor(content)}</div>
|
|
366
|
-
{/if}
|
|
367
|
-
</div>
|
|
368
|
-
<h3>{getDisplayTitle(content)}</h3>
|
|
369
|
-
</div>
|
|
370
|
-
|
|
371
|
-
<div class="content-meta">
|
|
372
|
-
<div>{getTypeLabel(content.type)}</div>
|
|
373
|
-
<div class="badges">
|
|
374
|
-
<span class="badge status-{getStatusBadge(content.status)}">{content.status}</span>
|
|
375
|
-
<span class="badge state-{getStateBadge(content.state)}">{content.state}</span>
|
|
376
|
-
</div>
|
|
377
|
-
</div>
|
|
378
|
-
|
|
379
|
-
<p class="content-description">{getDisplayDescription(content)}</p>
|
|
380
|
-
|
|
381
|
-
<div class="content-footer">
|
|
382
|
-
<div class="meta-links">
|
|
383
|
-
{#if content.url}
|
|
384
|
-
<div class="source">Source: <a href={content.url} target="_blank">{content.url}</a></div>
|
|
385
|
-
{/if}
|
|
386
|
-
{#if content.fileKey}
|
|
387
|
-
<div class="file">File: {content.fileKey}</div>
|
|
388
|
-
{/if}
|
|
389
|
-
</div>
|
|
390
|
-
|
|
391
|
-
<div class="content-actions">
|
|
392
|
-
{#if getViewHref?.(content)}
|
|
393
|
-
<a href={getViewHref(content) || '#'} class="view-btn">{t(M['content.content_list.view_article_button'])}</a>
|
|
394
|
-
{/if}
|
|
395
|
-
<Button variant="ghost" type="button" class="content-action-btn" onclick={() => onEdit(content)}>{t(M['content.content_list.edit'])}</Button>
|
|
396
|
-
<Button variant="ghost" type="button" class="content-action-btn delete-btn" onclick={() => handleDeleteContent(content)}>{t(M['content.content_list.delete'])}</Button>
|
|
733
|
+
<div class="content-actions">
|
|
734
|
+
{#if actions.includes('view')}
|
|
735
|
+
<a href={viewHref(row)} class="view-btn">{t(M['content.content_list.view_article_button'])}</a>
|
|
736
|
+
{/if}
|
|
737
|
+
{#if actions.includes('edit')}
|
|
738
|
+
<Button variant="ghost" type="button" class="content-action-btn" onclick={() => onEdit(content)}>
|
|
739
|
+
{t(M['content.content_list.edit'])}
|
|
740
|
+
</Button>
|
|
741
|
+
{/if}
|
|
742
|
+
{#if actions.includes('delete')}
|
|
743
|
+
<Button variant="ghost" type="button" class="content-action-btn delete-btn" onclick={() => handleDeleteContent(row)}>
|
|
744
|
+
{t(M['content.content_list.delete'])}
|
|
745
|
+
</Button>
|
|
746
|
+
{/if}
|
|
747
|
+
</div>
|
|
397
748
|
</div>
|
|
398
749
|
</div>
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
750
|
+
{/each}
|
|
751
|
+
</div>
|
|
752
|
+
{/if}
|
|
753
|
+
|
|
754
|
+
{#if showPagination && viewMode !== 'compact'}
|
|
755
|
+
<div class="content-pagination">
|
|
756
|
+
<Pagination
|
|
757
|
+
currentPage={tableState.page}
|
|
758
|
+
{totalPages}
|
|
759
|
+
onPageChange={handlePageChange}
|
|
760
|
+
aria-label={t(M['content.content_list.pagination'])}
|
|
761
|
+
/>
|
|
762
|
+
</div>
|
|
763
|
+
{/if}
|
|
402
764
|
{/if}
|
|
403
765
|
|
|
404
766
|
</div>
|
|
@@ -407,7 +769,7 @@ function cancelDelete() {
|
|
|
407
769
|
open={pendingDelete !== null}
|
|
408
770
|
title={t(M['content.content_list.delete_confirm_title'])}
|
|
409
771
|
message={t(M['content.content_list.delete_confirm_message'], {
|
|
410
|
-
title: pendingDelete ?
|
|
772
|
+
title: pendingDelete ? pendingDelete.title : '',
|
|
411
773
|
})}
|
|
412
774
|
confirmLabel={t(M['content.content_list.delete'])}
|
|
413
775
|
cancelLabel={t(M['content.content_list.cancel'])}
|
|
@@ -516,6 +878,20 @@ function cancelDelete() {
|
|
|
516
878
|
box-shadow: 0 4px 6px -1px color-mix(in srgb, var(--smrt-color-primary) 50%, transparent);
|
|
517
879
|
}
|
|
518
880
|
|
|
881
|
+
/* Selection summary shared by every presentation. */
|
|
882
|
+
.content-selection {
|
|
883
|
+
display: flex;
|
|
884
|
+
align-items: center;
|
|
885
|
+
gap: 0.75rem;
|
|
886
|
+
padding: 0.35rem 0.1rem 0.75rem;
|
|
887
|
+
color: var(--smrt-color-on-surface-variant);
|
|
888
|
+
font-size: var(--smrt-typography-body-medium-size, 0.875rem);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
.content-selection :global(input[type='checkbox']) {
|
|
892
|
+
cursor: pointer;
|
|
893
|
+
}
|
|
894
|
+
|
|
519
895
|
.content-header__eyebrow,
|
|
520
896
|
.content-row__eyebrow {
|
|
521
897
|
display: flex;
|
|
@@ -658,7 +1034,7 @@ function cancelDelete() {
|
|
|
658
1034
|
color: var(--smrt-color-primary);
|
|
659
1035
|
text-decoration: none;
|
|
660
1036
|
}
|
|
661
|
-
|
|
1037
|
+
|
|
662
1038
|
.source a:hover {
|
|
663
1039
|
text-decoration: underline;
|
|
664
1040
|
}
|
|
@@ -723,13 +1099,17 @@ function cancelDelete() {
|
|
|
723
1099
|
|
|
724
1100
|
.content-row {
|
|
725
1101
|
display: grid;
|
|
726
|
-
grid-template-columns: minmax(0, 1.8fr) auto auto;
|
|
1102
|
+
grid-template-columns: auto minmax(0, 1.8fr) auto auto;
|
|
727
1103
|
gap: 1.25rem;
|
|
728
1104
|
align-items: start;
|
|
729
1105
|
padding: 1.1rem 0;
|
|
730
1106
|
border-bottom: 1px solid var(--smrt-color-outline-variant);
|
|
731
1107
|
}
|
|
732
1108
|
|
|
1109
|
+
.content-row__select {
|
|
1110
|
+
padding-top: 0.25rem;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
733
1113
|
.content-row h3 {
|
|
734
1114
|
margin: 0;
|
|
735
1115
|
font-size: var(--smrt-typography-title-medium-size, 1.1rem);
|
|
@@ -816,53 +1196,39 @@ function cancelDelete() {
|
|
|
816
1196
|
box-shadow: var(--smrt-elevation-1, 0 1px 3px rgba(0,0,0,0.05));
|
|
817
1197
|
}
|
|
818
1198
|
|
|
819
|
-
.content-
|
|
820
|
-
|
|
821
|
-
border-collapse: collapse;
|
|
822
|
-
text-align: left;
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
.content-table th {
|
|
826
|
-
background: var(--smrt-color-surface-container-low);
|
|
827
|
-
padding: 1rem;
|
|
828
|
-
font-size: var(--smrt-typography-title-small-size, 0.875rem);
|
|
829
|
-
font-weight: var(--smrt-typography-weight-semibold, 600);
|
|
1199
|
+
.content-refreshing {
|
|
1200
|
+
margin: 0 0 0.75rem;
|
|
830
1201
|
color: var(--smrt-color-on-surface-variant);
|
|
831
|
-
border-bottom: 1px solid var(--smrt-color-outline-variant);
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
.content-table td {
|
|
835
|
-
padding: 1rem;
|
|
836
|
-
border-bottom: 1px solid var(--smrt-color-outline-variant);
|
|
837
|
-
color: var(--smrt-color-on-surface);
|
|
838
1202
|
font-size: var(--smrt-typography-body-medium-size, 0.875rem);
|
|
839
|
-
vertical-align: middle;
|
|
840
1203
|
}
|
|
841
1204
|
|
|
842
|
-
.content-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
.content-table tr:hover {
|
|
847
|
-
background: var(--smrt-color-surface-container-low);
|
|
1205
|
+
.content-pagination {
|
|
1206
|
+
display: flex;
|
|
1207
|
+
justify-content: center;
|
|
1208
|
+
margin-top: 1.25rem;
|
|
848
1209
|
}
|
|
849
1210
|
|
|
850
|
-
.
|
|
851
|
-
|
|
1211
|
+
.table-empty-state {
|
|
1212
|
+
margin: 0;
|
|
1213
|
+
padding: 1.5rem 0;
|
|
1214
|
+
text-align: center;
|
|
1215
|
+
color: var(--smrt-color-on-surface-variant);
|
|
852
1216
|
}
|
|
853
1217
|
|
|
854
|
-
.title-
|
|
855
|
-
color: var(--smrt-color-
|
|
1218
|
+
.title-link {
|
|
1219
|
+
color: var(--smrt-color-primary);
|
|
856
1220
|
font-weight: var(--smrt-typography-weight-semibold, 600);
|
|
1221
|
+
text-decoration: none;
|
|
857
1222
|
}
|
|
858
1223
|
|
|
859
|
-
.
|
|
860
|
-
|
|
861
|
-
text-align: right;
|
|
1224
|
+
.title-link:hover {
|
|
1225
|
+
text-decoration: underline;
|
|
862
1226
|
}
|
|
863
1227
|
|
|
864
1228
|
.actions-cell {
|
|
865
|
-
|
|
1229
|
+
display: flex;
|
|
1230
|
+
justify-content: flex-end;
|
|
1231
|
+
gap: 0.15rem;
|
|
866
1232
|
white-space: nowrap;
|
|
867
1233
|
}
|
|
868
1234
|
|
|
@@ -875,6 +1241,7 @@ function cancelDelete() {
|
|
|
875
1241
|
border-radius: 0.25rem;
|
|
876
1242
|
transition: background 0.2s;
|
|
877
1243
|
opacity: 0.7;
|
|
1244
|
+
text-decoration: none;
|
|
878
1245
|
}
|
|
879
1246
|
|
|
880
1247
|
.actions-cell :global(.icon-btn:hover) {
|
|
@@ -886,7 +1253,8 @@ function cancelDelete() {
|
|
|
886
1253
|
background: var(--smrt-color-error-container);
|
|
887
1254
|
}
|
|
888
1255
|
|
|
889
|
-
|
|
1256
|
+
/* Shared empty, loading, and error presentation. */
|
|
1257
|
+
.state-panel {
|
|
890
1258
|
background: var(--smrt-color-surface);
|
|
891
1259
|
padding: 4rem;
|
|
892
1260
|
text-align: center;
|
|
@@ -896,13 +1264,33 @@ function cancelDelete() {
|
|
|
896
1264
|
font-size: var(--smrt-typography-body-large-size, 1.1rem);
|
|
897
1265
|
}
|
|
898
1266
|
|
|
899
|
-
.
|
|
1267
|
+
.state-panel--error {
|
|
1268
|
+
border-style: solid;
|
|
1269
|
+
border-color: var(--smrt-color-error);
|
|
1270
|
+
color: var(--smrt-color-on-surface);
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
.state-panel__title {
|
|
1274
|
+
margin: 0 0 0.5rem;
|
|
1275
|
+
font-weight: var(--smrt-typography-weight-semibold, 600);
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
.state-panel__detail {
|
|
1279
|
+
margin: 0 0 1rem;
|
|
900
1280
|
color: var(--smrt-color-on-surface-variant);
|
|
1281
|
+
font-size: var(--smrt-typography-body-medium-size, 0.875rem);
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
.state-panel :global(.retry-button) {
|
|
1285
|
+
border: 1px solid var(--smrt-color-outline);
|
|
1286
|
+
border-radius: 0.5rem;
|
|
1287
|
+
padding: 0.5rem 1rem;
|
|
1288
|
+
cursor: pointer;
|
|
901
1289
|
}
|
|
902
1290
|
|
|
903
1291
|
@media (max-width: 960px) {
|
|
904
1292
|
.content-row {
|
|
905
|
-
grid-template-columns: minmax(0, 1fr);
|
|
1293
|
+
grid-template-columns: auto minmax(0, 1fr);
|
|
906
1294
|
gap: 0.9rem;
|
|
907
1295
|
}
|
|
908
1296
|
|