@byline/host-tanstack-start 1.10.3 → 1.11.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.
@@ -35,3 +35,9 @@
35
35
  height: 1.5rem;
36
36
  }
37
37
 
38
+ .byline-sort-icon {
39
+ flex-shrink: 0;
40
+ width: 1.25rem;
41
+ height: 1.25rem;
42
+ }
43
+
@@ -7,9 +7,19 @@
7
7
  */
8
8
  import type { ColumnDefinition, WorkflowStatus } from '@byline/core';
9
9
  import type { AnyCollectionSchemaTypes } from '@byline/core/zod-schemas';
10
- export declare const ListView: ({ data, columns, workflowStatuses, useAsTitle, }: {
10
+ type ReorderFn = (params: {
11
+ documentId: string;
12
+ beforeDocumentId: string | null;
13
+ afterDocumentId: string | null;
14
+ }) => Promise<unknown>;
15
+ export declare const ListView: ({ data, columns, workflowStatuses, useAsTitle, orderable, onReorder, }: {
11
16
  data: AnyCollectionSchemaTypes["ListType"];
12
17
  columns: ColumnDefinition[];
13
18
  workflowStatuses?: WorkflowStatus[];
14
19
  useAsTitle?: string;
20
+ /** When true, render a drag handle column and enable drag-to-reorder. */
21
+ orderable?: boolean;
22
+ /** Persists a single-row reorder via the host's reorder server fn. */
23
+ onReorder?: ReorderFn;
15
24
  }) => import("react").JSX.Element;
25
+ export {};
@@ -1,10 +1,13 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
- import { useMemo, useState } from "react";
3
- import { useRouterState } from "@tanstack/react-router";
4
- import { Container, IconButton, LoaderRing, PlusIcon, Search, Section, Select, StatusBadge, Table, renderFormatted } from "@byline/ui/react";
2
+ import { useEffect, useMemo, useState } from "react";
3
+ import { useRouter, useRouterState } from "@tanstack/react-router";
4
+ import { Container, GripperVerticalIcon, IconButton, LoaderRing, PlusIcon, Search, Section, Select, StatusBadge, Table, renderFormatted, useToastManager } from "@byline/ui/react";
5
+ import { DndContext, KeyboardSensor, PointerSensor, useSensor, useSensors } from "@dnd-kit/core";
6
+ import { SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable";
5
7
  import classnames from "classnames";
6
8
  import { Link, useNavigate } from "../chrome/loose-router.js";
7
9
  import { RouterPager } from "../chrome/router-pager.js";
10
+ import { SortAscendingIcon } from "../chrome/sort-icons.js";
8
11
  import { TableHeadingCellSortable } from "../chrome/th-sortable.js";
9
12
  import { formatNumber } from "../chrome/utils.js";
10
13
  import list_module from "./list.module.js";
@@ -32,11 +35,104 @@ function padRows(value) {
32
35
  children: "\xa0"
33
36
  }, `empty-row-${index}`));
34
37
  }
35
- const ListView = ({ data, columns, workflowStatuses, useAsTitle })=>{
38
+ function SortableTableRow({ id, disabled, children }) {
39
+ const { setNodeRef, attributes, listeners, transform, transition, isDragging } = useSortable({
40
+ id,
41
+ disabled
42
+ });
43
+ const style = {
44
+ transform: transform ? `translate3d(${transform.x}px, ${transform.y}px, 0)` : void 0,
45
+ transition,
46
+ position: 'relative',
47
+ zIndex: isDragging ? 10 : 'auto',
48
+ opacity: isDragging ? 0.6 : 1
49
+ };
50
+ return /*#__PURE__*/ jsxs("tr", {
51
+ ref: setNodeRef,
52
+ className: "byline-table-row",
53
+ style: style,
54
+ children: [
55
+ /*#__PURE__*/ jsx("td", {
56
+ className: classnames('byline-coll-list-drag-cell', list_module.dragCell),
57
+ children: /*#__PURE__*/ jsx("button", {
58
+ type: "button",
59
+ className: classnames('byline-coll-list-drag-handle', list_module.dragHandle),
60
+ "aria-label": disabled ? 'Drag disabled while filters or search are active' : 'Drag to reorder',
61
+ disabled: disabled,
62
+ ...attributes,
63
+ ...listeners,
64
+ children: /*#__PURE__*/ jsx(GripperVerticalIcon, {})
65
+ })
66
+ }),
67
+ children
68
+ ]
69
+ });
70
+ }
71
+ const ListView = ({ data, columns, workflowStatuses, useAsTitle, orderable = false, onReorder })=>{
36
72
  const navigate = useNavigate();
73
+ const router = useRouter();
74
+ const toastManager = useToastManager();
37
75
  const location = useRouterState({
38
76
  select: (s)=>s.location
39
77
  });
78
+ const [localDocs, setLocalDocs] = useState(data.docs);
79
+ const [isReordering, setIsReordering] = useState(false);
80
+ useEffect(()=>{
81
+ if (!isReordering) setLocalDocs(data.docs);
82
+ }, [
83
+ data.docs,
84
+ isReordering
85
+ ]);
86
+ const searchParams = location.search;
87
+ const isCanonicalView = !searchParams.order && !searchParams.desc && !searchParams.query && !searchParams.status;
88
+ const dragEnabled = orderable && isCanonicalView && !!onReorder;
89
+ const sensors = useSensors(useSensor(PointerSensor, {
90
+ activationConstraint: {
91
+ distance: 5
92
+ }
93
+ }), useSensor(KeyboardSensor, {
94
+ coordinateGetter: sortableKeyboardCoordinates
95
+ }));
96
+ const handleDragEnd = async (event)=>{
97
+ const { active, over } = event;
98
+ if (!over || active.id === over.id || !onReorder) return;
99
+ const oldIndex = localDocs.findIndex((d)=>d.id === active.id);
100
+ const newIndex = localDocs.findIndex((d)=>d.id === over.id);
101
+ if (oldIndex < 0 || newIndex < 0 || oldIndex === newIndex) return;
102
+ const previousDocs = localDocs;
103
+ const next = [
104
+ ...localDocs
105
+ ];
106
+ const [moved] = next.splice(oldIndex, 1);
107
+ if (!moved) return;
108
+ next.splice(newIndex, 0, moved);
109
+ setLocalDocs(next);
110
+ setIsReordering(true);
111
+ const before = next[newIndex - 1]?.id ?? null;
112
+ const after = next[newIndex + 1]?.id ?? null;
113
+ try {
114
+ await onReorder({
115
+ documentId: String(active.id),
116
+ beforeDocumentId: before,
117
+ afterDocumentId: after
118
+ });
119
+ await router.invalidate();
120
+ } catch (_err) {
121
+ setLocalDocs(previousDocs);
122
+ toastManager.add({
123
+ title: 'Could not save the new order',
124
+ description: 'Please try again.',
125
+ data: {
126
+ intent: 'danger',
127
+ iconType: 'danger',
128
+ icon: true,
129
+ close: true
130
+ }
131
+ });
132
+ } finally{
133
+ setIsReordering(false);
134
+ }
135
+ };
40
136
  const statusItems = useMemo(()=>[
41
137
  {
42
138
  value: '_all',
@@ -162,7 +258,82 @@ const ListView = ({ data, columns, workflowStatuses, useAsTitle })=>{
162
258
  /*#__PURE__*/ jsxs(Table.Container, {
163
259
  className: classnames('byline-coll-list-table-wrap', list_module.tableWrap),
164
260
  children: [
165
- /*#__PURE__*/ jsxs(Table, {
261
+ orderable ? /*#__PURE__*/ jsx(DndContext, {
262
+ sensors: sensors,
263
+ onDragEnd: handleDragEnd,
264
+ children: /*#__PURE__*/ jsx(SortableContext, {
265
+ items: localDocs.map((d)=>d.id),
266
+ strategy: verticalListSortingStrategy,
267
+ children: /*#__PURE__*/ jsxs(Table, {
268
+ children: [
269
+ /*#__PURE__*/ jsx(Table.Header, {
270
+ children: /*#__PURE__*/ jsxs(Table.Row, {
271
+ children: [
272
+ /*#__PURE__*/ jsx("th", {
273
+ scope: "col",
274
+ className: classnames('byline-coll-list-drag-cell', list_module.dragCell),
275
+ children: /*#__PURE__*/ jsx("button", {
276
+ type: "button",
277
+ className: classnames('byline-coll-list-order-header', list_module.orderHeader, isCanonicalView && [
278
+ 'byline-coll-list-order-header-active',
279
+ list_module.orderHeaderActive
280
+ ]),
281
+ onClick: ()=>{
282
+ const params = structuredClone(location.search);
283
+ delete params.page;
284
+ delete params.order;
285
+ delete params.desc;
286
+ navigate({
287
+ to: location.pathname,
288
+ search: params
289
+ });
290
+ },
291
+ "aria-label": "Sort by manual order",
292
+ title: "Sort by manual order",
293
+ children: /*#__PURE__*/ jsx(SortAscendingIcon, {})
294
+ })
295
+ }),
296
+ columns.map((column)=>/*#__PURE__*/ jsx(TableHeadingCellSortable, {
297
+ fieldName: String(column.fieldName),
298
+ label: column.label,
299
+ sortable: column.sortable,
300
+ scope: "col",
301
+ align: column.align,
302
+ className: column.className
303
+ }, String(column.fieldName)))
304
+ ]
305
+ })
306
+ }),
307
+ /*#__PURE__*/ jsx(Table.Body, {
308
+ children: localDocs.map((document)=>/*#__PURE__*/ jsx(SortableTableRow, {
309
+ id: document.id,
310
+ disabled: !dragEnabled,
311
+ children: columns.map((column)=>/*#__PURE__*/ jsx(Table.Cell, {
312
+ className: classnames({
313
+ 'byline-coll-list-cell-right': 'right' === column.align,
314
+ [list_module.cellRight]: 'right' === column.align,
315
+ 'byline-coll-list-cell-center': 'center' === column.align,
316
+ [list_module.cellCenter]: 'center' === column.align
317
+ }),
318
+ children: useAsTitle && column.fieldName === useAsTitle ? /*#__PURE__*/ jsx(Link, {
319
+ to: '/admin/collections/$collection/$id',
320
+ params: {
321
+ collection: data.included.collection.path,
322
+ id: document.id
323
+ },
324
+ children: column.formatter ? renderFormatted(getColumnValue(document, column.fieldName), document, column.formatter) : getColumnValue(document, column.fieldName) ?? '------'
325
+ }) : column.formatter ? renderFormatted(getColumnValue(document, column.fieldName), document, column.formatter) : 'status' === column.fieldName && workflowStatuses ? /*#__PURE__*/ jsx(StatusBadge, {
326
+ status: document.status,
327
+ workflowStatuses: workflowStatuses,
328
+ hasPublishedVersion: document.hasPublishedVersion
329
+ }) : String(getColumnValue(document, column.fieldName) ?? '')
330
+ }, String(column.fieldName)))
331
+ }, document.id))
332
+ })
333
+ ]
334
+ })
335
+ })
336
+ }) : /*#__PURE__*/ jsxs(Table, {
166
337
  children: [
167
338
  /*#__PURE__*/ jsx(Table.Header, {
168
339
  children: /*#__PURE__*/ jsx(Table.Row, {
@@ -11,6 +11,10 @@ const list_module = {
11
11
  cellRight: "cellRight-KPlIvX",
12
12
  cellCenter: "cellCenter-NlTSyy",
13
13
  padRow: "padRow-mCWMHC",
14
+ dragCell: "dragCell-o9UFuB",
15
+ dragHandle: "dragHandle-kAIpKJ",
16
+ orderHeader: "orderHeader-wsz9WO",
17
+ orderHeaderActive: "orderHeaderActive-VUBEa8",
14
18
  pageSize: "pageSize-ojmZKR"
15
19
  };
16
20
  export default list_module;
@@ -80,6 +80,80 @@
80
80
  height: 32px;
81
81
  }
82
82
 
83
+ :is(.dragCell-o9UFuB, .byline-coll-list-drag-cell) {
84
+ vertical-align: middle;
85
+ white-space: nowrap;
86
+ width: 1%;
87
+ padding: 0 .5rem 0 .25rem;
88
+ }
89
+
90
+ :is(.dragHandle-kAIpKJ, .byline-coll-list-drag-handle) {
91
+ width: 20px;
92
+ height: 20px;
93
+ color: inherit;
94
+ cursor: grab;
95
+ opacity: .45;
96
+ touch-action: none;
97
+ background: none;
98
+ border: 0;
99
+ justify-content: center;
100
+ align-items: center;
101
+ padding: 0;
102
+ display: inline-flex;
103
+ }
104
+
105
+ .dragHandle-kAIpKJ:hover {
106
+ opacity: 1;
107
+ }
108
+
109
+ .byline-coll-list-drag-handle:hover {
110
+ opacity: 1;
111
+ }
112
+
113
+ .dragHandle-kAIpKJ:disabled {
114
+ cursor: not-allowed;
115
+ opacity: .2;
116
+ }
117
+
118
+ .byline-coll-list-drag-handle:disabled {
119
+ cursor: not-allowed;
120
+ opacity: .2;
121
+ }
122
+
123
+ .dragHandle-kAIpKJ:active {
124
+ cursor: grabbing;
125
+ }
126
+
127
+ .byline-coll-list-drag-handle:active {
128
+ cursor: grabbing;
129
+ }
130
+
131
+ :is(.orderHeader-wsz9WO, .byline-coll-list-order-header) {
132
+ width: 20px;
133
+ height: 20px;
134
+ color: inherit;
135
+ cursor: pointer;
136
+ opacity: .45;
137
+ background: none;
138
+ border: 0;
139
+ justify-content: center;
140
+ align-items: center;
141
+ padding: 0;
142
+ display: inline-flex;
143
+ }
144
+
145
+ .orderHeader-wsz9WO:hover {
146
+ opacity: .85;
147
+ }
148
+
149
+ .byline-coll-list-order-header:hover {
150
+ opacity: .85;
151
+ }
152
+
153
+ :is(.orderHeaderActive-VUBEa8, .byline-coll-list-order-header-active) {
154
+ opacity: 1;
155
+ }
156
+
83
157
  @media (min-width: 40rem) {
84
158
  :is(.pageSize-ojmZKR, .byline-coll-list-page-size) {
85
159
  margin-left: auto;
@@ -7,7 +7,7 @@ import { z } from "zod";
7
7
  import { BreadcrumbsClient } from "../admin-shell/chrome/breadcrumbs/breadcrumbs-client.js";
8
8
  import { useNavigate } from "../admin-shell/chrome/loose-router.js";
9
9
  import { ListView } from "../admin-shell/collections/list.js";
10
- import { getCollectionDocuments } from "../server-fns/collections/index.js";
10
+ import { getCollectionDocuments, reorderCollectionDocument } from "../server-fns/collections/index.js";
11
11
  const searchSchema = z.object({
12
12
  page: z.coerce.number().min(1).optional(),
13
13
  page_size: z.coerce.number().max(100).optional(),
@@ -119,7 +119,18 @@ function createCollectionListRoute(path) {
119
119
  data: data,
120
120
  columns: columns,
121
121
  workflowStatuses: workflowStatuses,
122
- useAsTitle: collectionDef.useAsTitle
122
+ useAsTitle: collectionDef.useAsTitle,
123
+ orderable: adminConfig?.orderable === true,
124
+ onReorder: async ({ documentId, beforeDocumentId, afterDocumentId })=>{
125
+ await reorderCollectionDocument({
126
+ data: {
127
+ collection,
128
+ documentId,
129
+ beforeDocumentId,
130
+ afterDocumentId
131
+ }
132
+ });
133
+ }
123
134
  })
124
135
  ]
125
136
  });
@@ -11,6 +11,7 @@ export * from './duplicate';
11
11
  export * from './get';
12
12
  export * from './history';
13
13
  export * from './list';
14
+ export * from './reorder';
14
15
  export * from './restore-version';
15
16
  export * from './stats';
16
17
  export * from './status';
@@ -5,6 +5,7 @@ export * from "./duplicate.js";
5
5
  export * from "./get.js";
6
6
  export * from "./history.js";
7
7
  export * from "./list.js";
8
+ export * from "./reorder.js";
8
9
  export * from "./restore-version.js";
9
10
  export * from "./stats.js";
10
11
  export * from "./status.js";
@@ -1,5 +1,5 @@
1
1
  import { createServerFn } from "@tanstack/react-start";
2
- import { ERR_NOT_FOUND, getCollectionSchemasForPath, getLogger, getServerConfig } from "@byline/core";
2
+ import { ERR_NOT_FOUND, getCollectionAdminConfig, getCollectionSchemasForPath, getLogger, getServerConfig } from "@byline/core";
3
3
  import { ensureCollection } from "../../integrations/api-utils.js";
4
4
  import { getAdminBylineClient } from "../../integrations/byline-client.js";
5
5
  import { serialise } from "./utils.js";
@@ -20,11 +20,16 @@ const getCollectionDocuments = createServerFn({
20
20
  const where = {};
21
21
  if (params.status) where.status = params.status;
22
22
  if (params.query) where.query = params.query;
23
+ const adminConfig = getCollectionAdminConfig(path);
24
+ const defaultSort = adminConfig?.orderable === true ? {
25
+ order_key: 'asc'
26
+ } : void 0;
27
+ const sortSpec = params.order ? {
28
+ [params.order]: false === params.desc ? 'asc' : 'desc'
29
+ } : defaultSort;
23
30
  const result = await handle.find({
24
31
  where: Object.keys(where).length > 0 ? where : void 0,
25
- sort: params.order ? {
26
- [params.order]: false === params.desc ? 'asc' : 'desc'
27
- } : void 0,
32
+ sort: sortSpec,
28
33
  locale: params.locale ?? 'en',
29
34
  page: params.page,
30
35
  pageSize,
@@ -0,0 +1,21 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ export declare const reorderCollectionDocument: import("@tanstack/react-start").RequiredFetcher<undefined, (input: {
9
+ collection: string;
10
+ documentId: string;
11
+ beforeDocumentId?: string | null;
12
+ afterDocumentId?: string | null;
13
+ }) => {
14
+ collection: string;
15
+ documentId: string;
16
+ beforeDocumentId?: string | null;
17
+ afterDocumentId?: string | null;
18
+ }, Promise<{
19
+ status: "ok";
20
+ orderKey: string;
21
+ }>>;
@@ -0,0 +1,97 @@
1
+ import { createServerFn } from "@tanstack/react-start";
2
+ import { ERR_NOT_FOUND, ERR_VALIDATION, assertActorCanPerform, generateKeyBetween, generateNKeysBetween, getCollectionAdminConfig, getLogger, getServerConfig } from "@byline/core";
3
+ import { getAdminRequestContext } from "../../auth/auth-context.js";
4
+ import { ensureCollection } from "../../integrations/api-utils.js";
5
+ const reorderCollectionDocument = createServerFn({
6
+ method: 'POST'
7
+ }).inputValidator((input)=>input).handler(async ({ data: input })=>{
8
+ const { collection: path, documentId, beforeDocumentId, afterDocumentId } = input;
9
+ const logger = getLogger();
10
+ const config = await ensureCollection(path);
11
+ if (!config) throw ERR_NOT_FOUND({
12
+ message: 'Collection not found',
13
+ details: {
14
+ collectionPath: path
15
+ }
16
+ }).log(logger);
17
+ const adminConfig = getCollectionAdminConfig(path);
18
+ if (adminConfig?.orderable !== true) throw ERR_VALIDATION({
19
+ message: `collection '${path}' is not orderable; set \`orderable: true\` on its admin config to enable reordering`,
20
+ details: {
21
+ collectionPath: path
22
+ }
23
+ }).log(logger);
24
+ const requestContext = await getAdminRequestContext();
25
+ assertActorCanPerform(requestContext, path, 'update');
26
+ const serverConfig = getServerConfig();
27
+ const collectionId = config.collection.id;
28
+ const canonical = await serverConfig.db.queries.documents.getCanonicalDocumentOrder({
29
+ collection_id: collectionId
30
+ });
31
+ let corrupted = false;
32
+ {
33
+ const seen = new Set();
34
+ let lastKey = null;
35
+ for (const doc of canonical)if (null != doc.order_key) {
36
+ if (seen.has(doc.order_key)) {
37
+ corrupted = true;
38
+ break;
39
+ }
40
+ if (null != lastKey && doc.order_key <= lastKey) {
41
+ corrupted = true;
42
+ break;
43
+ }
44
+ seen.add(doc.order_key);
45
+ lastKey = doc.order_key;
46
+ }
47
+ }
48
+ if (corrupted) {
49
+ const allKeys = generateNKeysBetween(null, null, canonical.length);
50
+ for(let i = 0; i < canonical.length; i++)await serverConfig.db.commands.documents.setOrderKey({
51
+ document_id: canonical[i]?.id,
52
+ order_key: allKeys[i]
53
+ });
54
+ } else {
55
+ const firstNullIdx = canonical.findIndex((d)=>null == d.order_key);
56
+ if (-1 !== firstNullIdx) {
57
+ const nullDocs = canonical.slice(firstNullIdx);
58
+ const lastExistingKey = 0 === firstNullIdx ? null : canonical[firstNullIdx - 1]?.order_key;
59
+ const newKeys = generateNKeysBetween(lastExistingKey, null, nullDocs.length);
60
+ for(let i = 0; i < nullDocs.length; i++)await serverConfig.db.commands.documents.setOrderKey({
61
+ document_id: nullDocs[i]?.id,
62
+ order_key: newKeys[i]
63
+ });
64
+ }
65
+ }
66
+ const { left, right } = await serverConfig.db.queries.documents.getNeighborOrderKeys({
67
+ collection_id: collectionId,
68
+ before_document_id: beforeDocumentId ?? null,
69
+ after_document_id: afterDocumentId ?? null
70
+ });
71
+ let newKey;
72
+ try {
73
+ newKey = generateKeyBetween(left, right);
74
+ } catch (err) {
75
+ throw ERR_VALIDATION({
76
+ message: 'cannot generate order_key between supplied neighbors',
77
+ details: {
78
+ collectionPath: path,
79
+ documentId,
80
+ beforeDocumentId,
81
+ afterDocumentId,
82
+ left,
83
+ right,
84
+ cause: err instanceof Error ? err.message : String(err)
85
+ }
86
+ }).log(logger);
87
+ }
88
+ await serverConfig.db.commands.documents.setOrderKey({
89
+ document_id: documentId,
90
+ order_key: newKey
91
+ });
92
+ return {
93
+ status: 'ok',
94
+ orderKey: newKey
95
+ };
96
+ });
97
+ export { reorderCollectionDocument };
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "private": false,
4
4
  "type": "module",
5
5
  "license": "MPL-2.0",
6
- "version": "1.10.3",
6
+ "version": "1.11.0",
7
7
  "engines": {
8
8
  "node": ">=20.9.0"
9
9
  },
@@ -99,18 +99,20 @@
99
99
  }
100
100
  },
101
101
  "dependencies": {
102
+ "@dnd-kit/core": "^6.3.1",
103
+ "@dnd-kit/sortable": "^10.0.0",
102
104
  "classnames": "^2.5.1",
103
105
  "npm-run-all": "^4.1.5",
104
106
  "react-json-view-lite": "^2.5.0",
105
107
  "react-swipeable": "^7.0.2",
106
108
  "uuid": "^14.0.0",
107
109
  "zod": "^4.4.3",
108
- "@byline/admin": "1.10.3",
109
- "@byline/auth": "1.10.3",
110
- "@byline/ai": "1.10.3",
111
- "@byline/client": "1.10.3",
112
- "@byline/core": "1.10.3",
113
- "@byline/ui": "1.10.3"
110
+ "@byline/admin": "1.11.0",
111
+ "@byline/auth": "1.11.0",
112
+ "@byline/ai": "1.11.0",
113
+ "@byline/client": "1.11.0",
114
+ "@byline/core": "1.11.0",
115
+ "@byline/ui": "1.11.0"
114
116
  },
115
117
  "peerDependencies": {
116
118
  "@tanstack/react-router": "^1.167.0",
@@ -49,3 +49,13 @@
49
49
  width: 1.5rem;
50
50
  height: 1.5rem;
51
51
  }
52
+
53
+ /* Sort indicator SVGs are rendered with class `byline-sort-icon` (see
54
+ sort-icons.tsx) inside a flex container; without explicit dimensions
55
+ they collapse to zero. Size them here so every consumer of the icons
56
+ picks the rule up automatically. */
57
+ :global(.byline-sort-icon) {
58
+ flex-shrink: 0;
59
+ width: 1.25rem;
60
+ height: 1.25rem;
61
+ }
@@ -111,6 +111,78 @@
111
111
  border: 0;
112
112
  }
113
113
 
114
+ .dragCell,
115
+ :global(.byline-coll-list-drag-cell) {
116
+ /* "Shrink to content" — table layout distributes the row width across cells
117
+ without an explicit width; setting 1% with nowrap forces this column to
118
+ take only the space its inner button needs. */
119
+ width: 1%;
120
+ padding: 0 0.5rem 0 0.25rem;
121
+ vertical-align: middle;
122
+ white-space: nowrap;
123
+ }
124
+
125
+ .dragHandle,
126
+ :global(.byline-coll-list-drag-handle) {
127
+ display: inline-flex;
128
+ align-items: center;
129
+ justify-content: center;
130
+ width: 20px;
131
+ height: 20px;
132
+ padding: 0;
133
+ border: 0;
134
+ background: transparent;
135
+ color: inherit;
136
+ cursor: grab;
137
+ opacity: 0.45;
138
+ touch-action: none;
139
+ }
140
+
141
+ .dragHandle:hover,
142
+ :global(.byline-coll-list-drag-handle:hover) {
143
+ opacity: 1;
144
+ }
145
+
146
+ .dragHandle:disabled,
147
+ :global(.byline-coll-list-drag-handle:disabled) {
148
+ cursor: not-allowed;
149
+ opacity: 0.2;
150
+ }
151
+
152
+ .dragHandle:active,
153
+ :global(.byline-coll-list-drag-handle:active) {
154
+ cursor: grabbing;
155
+ }
156
+
157
+ /* Column-header button in the drag column. Clicking returns to canonical
158
+ `order_key` sort. Active when the current view is canonical so the user
159
+ sees the affordance is "selected" — matches the visual semantics of
160
+ the sortable column headers next to it. */
161
+ .orderHeader,
162
+ :global(.byline-coll-list-order-header) {
163
+ display: inline-flex;
164
+ align-items: center;
165
+ justify-content: center;
166
+ width: 20px;
167
+ height: 20px;
168
+ padding: 0;
169
+ border: 0;
170
+ background: transparent;
171
+ color: inherit;
172
+ cursor: pointer;
173
+ opacity: 0.45;
174
+ }
175
+
176
+ .orderHeader:hover,
177
+ :global(.byline-coll-list-order-header:hover) {
178
+ opacity: 0.85;
179
+ }
180
+
181
+ .orderHeaderActive,
182
+ :global(.byline-coll-list-order-header-active) {
183
+ opacity: 1;
184
+ }
185
+
114
186
  .pageSize,
115
187
  :global(.byline-coll-list-page-size) {
116
188
  /* fallback */