@adecore/database 0.17.0 → 0.18.1

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 (135) hide show
  1. package/README.md +1 -1
  2. package/dist/DatabaseProvider.d.ts +4 -3
  3. package/dist/DatabaseProvider.js +15 -3
  4. package/dist/KeyIcon.d.ts +4 -0
  5. package/dist/KeyIcon.js +14 -0
  6. package/dist/actions.d.ts +12 -0
  7. package/dist/client-context.d.ts +3 -1
  8. package/dist/client-context.js +2 -0
  9. package/dist/code-text.d.ts +1 -0
  10. package/dist/code-text.js +2 -0
  11. package/dist/column-keys.d.ts +5 -0
  12. package/dist/column-keys.js +2 -0
  13. package/dist/connections/ConnectionManager.d.ts +3 -2
  14. package/dist/connections/ConnectionManager.js +27 -16
  15. package/dist/connections/connection-config.js +2 -0
  16. package/dist/console/QueryConsole.d.ts +8 -3
  17. package/dist/console/QueryConsole.js +110 -50
  18. package/dist/console/SqlEditor.d.ts +2 -12
  19. package/dist/console/SqlEditor.js +29 -18
  20. package/dist/console/StatementResultView.d.ts +5 -1
  21. package/dist/console/StatementResultView.js +18 -5
  22. package/dist/console/TransactionControls.js +3 -6
  23. package/dist/console/editor-slot.d.ts +18 -0
  24. package/dist/console/editor-slot.js +1 -0
  25. package/dist/console/result-keys.d.ts +15 -0
  26. package/dist/console/result-keys.js +79 -0
  27. package/dist/designer/ColumnsEditor.js +2 -2
  28. package/dist/designer/DesignerView.d.ts +3 -2
  29. package/dist/designer/DesignerView.js +5 -4
  30. package/dist/designer/SqlPreview.js +2 -1
  31. package/dist/designer/TableDesigner.d.ts +3 -2
  32. package/dist/designer/TableDesigner.js +2 -2
  33. package/dist/explorer/DatabaseExplorer.d.ts +4 -1
  34. package/dist/explorer/DatabaseExplorer.js +79 -23
  35. package/dist/explorer/RowMenu.js +2 -2
  36. package/dist/explorer/expansion.js +2 -2
  37. package/dist/explorer/folders.d.ts +16 -0
  38. package/dist/explorer/folders.js +1 -0
  39. package/dist/explorer/tree.d.ts +55 -4
  40. package/dist/explorer/tree.js +137 -23
  41. package/dist/grid/CellEditor.js +3 -1
  42. package/dist/grid/DataGrid.d.ts +2 -2
  43. package/dist/grid/DataGrid.js +61 -59
  44. package/dist/grid/EnumPicker.js +2 -1
  45. package/dist/grid/GridHeaderCell.d.ts +1 -2
  46. package/dist/grid/GridHeaderCell.js +12 -5
  47. package/dist/grid/GridRowView.d.ts +49 -0
  48. package/dist/grid/GridRowView.js +51 -0
  49. package/dist/grid/RecordView.d.ts +4 -2
  50. package/dist/grid/RecordView.js +71 -22
  51. package/dist/grid/ValueDock.d.ts +3 -1
  52. package/dist/grid/ValueDock.js +9 -3
  53. package/dist/grid/cell-id.d.ts +1 -0
  54. package/dist/grid/cell-id.js +2 -0
  55. package/dist/grid/layout.d.ts +3 -2
  56. package/dist/grid/layout.js +15 -6
  57. package/dist/grid/types.d.ts +8 -1
  58. package/dist/grid/types.js +4 -3
  59. package/dist/index.d.ts +5 -1
  60. package/dist/index.js +2 -0
  61. package/dist/locales/en.json +32 -15
  62. package/dist/locales/nl.json +32 -15
  63. package/dist/protocol/index.d.ts +1 -1
  64. package/dist/protocol/messages.d.ts +1 -1
  65. package/dist/protocol/messages.js +1 -1
  66. package/dist/protocol/schema.d.ts +17 -0
  67. package/dist/structure/StructureView.d.ts +5 -3
  68. package/dist/structure/StructureView.js +8 -6
  69. package/dist/table/CommandField.js +2 -2
  70. package/dist/table/TableStatusBar.js +2 -2
  71. package/dist/table/TableToolbar.d.ts +3 -4
  72. package/dist/table/TableToolbar.js +4 -3
  73. package/dist/table/TableView.d.ts +4 -2
  74. package/dist/table/TableView.js +63 -91
  75. package/dist/table/grid-rows.js +4 -1
  76. package/dist/table/useTableTransfer.d.ts +3 -1
  77. package/dist/table/useTableTransfer.js +19 -5
  78. package/dist/testing/fake.d.ts +3 -1
  79. package/dist/testing/fake.js +7 -1
  80. package/dist/value/ValueEditor.d.ts +9 -0
  81. package/dist/value/ValueEditor.js +102 -0
  82. package/dist/value/ValuePanel.d.ts +1 -1
  83. package/dist/value/ValuePanel.js +3 -94
  84. package/dist/workbench/DatabaseWorkbench.js +4 -7
  85. package/package.json +6 -6
  86. package/src/DatabaseProvider.tsx +24 -5
  87. package/src/KeyIcon.tsx +15 -0
  88. package/src/actions.ts +28 -2
  89. package/src/client-context.ts +5 -1
  90. package/src/code-text.ts +2 -0
  91. package/src/column-keys.ts +8 -0
  92. package/src/connections/ConnectionManager.tsx +98 -68
  93. package/src/connections/connection-config.ts +2 -0
  94. package/src/console/QueryConsole.tsx +246 -150
  95. package/src/console/SqlEditor.tsx +33 -37
  96. package/src/console/StatementResultView.tsx +61 -27
  97. package/src/console/TransactionControls.tsx +8 -11
  98. package/src/console/editor-slot.ts +27 -0
  99. package/src/console/result-keys.ts +109 -0
  100. package/src/designer/ColumnsEditor.tsx +3 -3
  101. package/src/designer/DesignerView.tsx +14 -3
  102. package/src/designer/SqlPreview.tsx +2 -1
  103. package/src/designer/TableDesigner.tsx +5 -2
  104. package/src/explorer/DatabaseExplorer.tsx +170 -26
  105. package/src/explorer/RowMenu.tsx +2 -2
  106. package/src/explorer/expansion.ts +4 -4
  107. package/src/explorer/folders.ts +25 -0
  108. package/src/explorer/tree.ts +219 -28
  109. package/src/grid/CellEditor.tsx +3 -2
  110. package/src/grid/DataGrid.tsx +114 -176
  111. package/src/grid/EnumPicker.tsx +2 -1
  112. package/src/grid/GridHeaderCell.tsx +14 -9
  113. package/src/grid/GridRowView.tsx +184 -0
  114. package/src/grid/RecordView.tsx +188 -75
  115. package/src/grid/ValueDock.tsx +12 -3
  116. package/src/grid/cell-id.ts +2 -0
  117. package/src/grid/layout.ts +16 -6
  118. package/src/grid/types.ts +15 -3
  119. package/src/index.ts +5 -1
  120. package/src/locales/en.json +32 -15
  121. package/src/locales/nl.json +32 -15
  122. package/src/protocol/index.ts +13 -1
  123. package/src/protocol/messages.ts +1 -1
  124. package/src/protocol/schema.ts +28 -0
  125. package/src/structure/StructureView.tsx +16 -6
  126. package/src/table/CommandField.tsx +2 -2
  127. package/src/table/TableStatusBar.tsx +3 -3
  128. package/src/table/TableToolbar.tsx +107 -92
  129. package/src/table/TableView.tsx +95 -132
  130. package/src/table/grid-rows.ts +5 -3
  131. package/src/table/useTableTransfer.ts +21 -6
  132. package/src/testing/fake.ts +18 -1
  133. package/src/value/ValueEditor.tsx +213 -0
  134. package/src/value/ValuePanel.tsx +4 -206
  135. package/src/workbench/DatabaseWorkbench.tsx +9 -10
@@ -12,15 +12,14 @@ import {
12
12
  type Ref
13
13
  } from 'react';
14
14
  import clsx from 'clsx';
15
- import { ArrowUpRight, Ban, Copy, RotateCcw } from 'lucide-react';
15
+ import { Ban, Copy, RotateCcw } from 'lucide-react';
16
16
  import { useTranslation } from 'react-i18next';
17
- import { ContextMenu, copyText, EDIT_SHORTCUTS, Icon, IconButton, isApplePlatform, isModHeld, Kbd, Spinner, useContentSize } from '@adecore/ui';
18
- import { formatNumber } from '@adecore/ui/format';
17
+ import { ContextMenu, copyText, EDIT_SHORTCUTS, Icon, isApplePlatform, isModHeld, Kbd, useContentSize } from '@adecore/ui';
19
18
  import type { EditValue, Value } from '../protocol/index.ts';
20
19
  import type { SqlTarget } from '../sql.ts';
21
20
  import type { RangeBlock } from './aggregates.ts';
22
21
  import { fullRect, rectBetween, rectContains } from './cell-range.ts';
23
- import { CellEditor, type EditMove } from './CellEditor.tsx';
22
+ import type { EditMove } from './CellEditor.tsx';
24
23
  import { captureLayout, restoreLayout, sameLayout, type GridLayout } from './column-layout.ts';
25
24
  import { NO_COLUMN_SELECTION, selectColumn, shownSelection, type ColumnSelection } from './column-selection.ts';
26
25
  import { displayOrder, hideColumn, pinnedCount, showAllColumns, togglePin, type ColumnView } from './column-view.ts';
@@ -29,8 +28,9 @@ import { CopyAsMenu } from './CopyAsMenu.tsx';
29
28
  import { cellView, copyTextOf, isPreview, type CellView, type NumberNotation } from './display.ts';
30
29
  import { draftOf, parseDraft } from './edit-value.ts';
31
30
  import { parseEnumType } from './enum-type.ts';
32
- import { EnumPicker } from './EnumPicker.tsx';
33
31
  import { GridHeaderCell, type HeaderActions } from './GridHeaderCell.tsx';
32
+ import { cellIdOf } from './cell-id.ts';
33
+ import { GridRowView, PINNED_GROUP, type CellEdit, type RowActions, type RowLook } from './GridRowView.tsx';
34
34
  import {
35
35
  clampColumnWidth,
36
36
  columnOffsets,
@@ -39,18 +39,19 @@ import {
39
39
  FALLBACK_VIEWPORT_HEIGHT,
40
40
  growWidths,
41
41
  gutterWidth,
42
+ drawnRange,
42
43
  HEADER_HEIGHT,
43
- OVERSCAN_ROWS,
44
44
  ROW_HEIGHT,
45
- scrollToReveal,
46
- visibleRange
45
+ scrollBlock,
46
+ scrollToReveal
47
47
  } from './layout.ts';
48
48
  import { moveFocus, type CellPosition, type NavigationKey } from './navigation.ts';
49
49
  import { selectRow } from './row-selection.ts';
50
50
  import { sortOnly, sortStateOf, type GridSort } from './sort.ts';
51
- import type { ColumnRequest, FocusedCell, GridColumn, GridMenuContext, GridRow } from './types.ts';
51
+ import type { ColumnRequest, FocusedCell, FocusRequest, GridColumn, GridMenuContext, GridRow } from './types.ts';
52
52
  import { useNumberNotation } from '../client-context.ts';
53
53
  import { usePopupPress } from '../use-popup-press.ts';
54
+ import { useStableCallback } from '../use-stable-callback.ts';
54
55
 
55
56
  export interface DataGridProps {
56
57
  columns: readonly GridColumn[];
@@ -92,7 +93,7 @@ export interface DataGridProps {
92
93
  /* A person resized, hid or pinned a column. */
93
94
  onLayoutChange?(layout: GridLayout): void;
94
95
  /* Puts the focus on a cell, whenever the object changes. */
95
- focusRequest?: FocusedCell | null;
96
+ focusRequest?: FocusRequest | null;
96
97
  /* Picks a column and brings it into view, whenever the object changes. */
97
98
  columnRequest?: ColumnRequest | null;
98
99
  /* Drawn under the header while there are no rows. */
@@ -101,12 +102,6 @@ export interface DataGridProps {
101
102
  ref?: Ref<HTMLDivElement>;
102
103
  }
103
104
 
104
- interface Editing extends CellPosition {
105
- draft: string;
106
- initial: string;
107
- loading: boolean;
108
- }
109
-
110
105
  interface MenuTarget {
111
106
  readonly row: number;
112
107
  /* `null` for the row number. */
@@ -139,17 +134,6 @@ const estimateWidths = (columns: readonly GridColumn[], rows: readonly GridRow[]
139
134
  )
140
135
  );
141
136
 
142
- const TONE: Record<CellView['tone'], string> = {
143
- value: '',
144
- null: 'text-text-faint',
145
- default: 'text-text-faint italic'
146
- };
147
-
148
- const ROW_STATE = {
149
- inserted: 'bg-positive/10',
150
- deleted: 'bg-status-error/10 text-text-faint'
151
- } as const;
152
-
153
137
  /*
154
138
  * The table of the table view and of the console. Only the rows in view are drawn, at a fixed
155
139
  * height, inside a box as tall as all of them; the header and the row-number gutter stick to the edges
@@ -198,12 +182,12 @@ export function DataGrid({
198
182
  const reportRange = useRef(onRangeChange);
199
183
  const reportLayout = useRef(onLayoutChange);
200
184
  const [measure, size] = useContentSize();
201
- const [scrollTop, setScrollTop] = useState(0);
185
+ const [scrolledBlock, setScrolledBlock] = useState(0);
202
186
  const [focused, setFocused] = useState<CellPosition | null>(null);
203
187
  const [rangeAnchor, setRangeAnchor] = useState<CellPosition | null>(null);
204
188
  const [columnSelection, setColumnSelection] = useState<ColumnSelection>(NO_COLUMN_SELECTION);
205
189
  const [menuTarget, setMenuTarget] = useState<MenuTarget | null>(null);
206
- const [editing, setEditing] = useState<Editing | null>(null);
190
+ const [editing, setEditing] = useState<CellEdit | null>(null);
207
191
  const [ownSelection, setOwnSelection] = useState<ReadonlySet<string>>(NO_KEYS);
208
192
  const [layout, setLayout] = useState<Layout>(() => ({
209
193
  signature: signatureOf(columns),
@@ -229,17 +213,21 @@ export function DataGrid({
229
213
  setLayout(current);
230
214
  }
231
215
 
232
- const order = displayOrder(columns.length, current.view);
216
+ // Kept by identity while nothing about the columns changes, since every drawn row compares them.
217
+ const order = useMemo(() => displayOrder(columns.length, current.view), [columns.length, current.view]);
233
218
  const pinnedShown = pinnedCount(columns.length, current.view);
234
- const displayOf = new Map(order.map((index, position) => [index, position]));
235
- const widths = columns.map((_, index) => current.resized[index] ?? current.estimated[index] ?? clampColumnWidth(0));
236
- const shownWidths = order.map((index) => widths[index]!);
219
+ const displayOf = useMemo(() => new Map(order.map((index, position) => [index, position])), [order]);
220
+ const widths = useMemo(
221
+ () => columns.map((_, index) => current.resized[index] ?? current.estimated[index] ?? clampColumnWidth(0)),
222
+ [columns, current.resized, current.estimated]
223
+ );
224
+ const shownWidths = useMemo(() => order.map((index) => widths[index]!), [order, widths]);
237
225
  const offsets = columnOffsets(shownWidths);
238
226
  const gutter = gutterWidth(rows.length);
239
227
  const totalWidth = gutter + shownWidths.reduce((sum, width) => sum + width, 0);
240
228
  const pinnedWidth = shownWidths.slice(0, pinnedShown).reduce((sum, width) => sum + width, 0);
241
229
  const viewportHeight = size.height > 0 ? size.height : FALLBACK_VIEWPORT_HEIGHT;
242
- const range = visibleRange(scrollTop - HEADER_HEIGHT, viewportHeight, ROW_HEIGHT, rows.length, OVERSCAN_ROWS);
230
+ const range = drawnRange(scrolledBlock, viewportHeight, rows.length);
243
231
  const selection = selectedKeys ?? ownSelection;
244
232
  const isShown = (position: CellPosition | null): position is CellPosition =>
245
233
  position !== null && position.row < rows.length && displayOf.has(position.column);
@@ -253,8 +241,8 @@ export function DataGrid({
253
241
  const sortList = sorts ?? NO_SORTS;
254
242
  const focusedRowKey = focusCell === null ? null : (rows[focusCell.row]?.key ?? null);
255
243
  const focusedColumn = focusCell?.column ?? -1;
256
- const pickedColumns = shownSelection(columnSelection, order);
257
- const pickedSet = new Set(pickedColumns);
244
+ const pickedColumns = useMemo(() => shownSelection(columnSelection, order), [columnSelection, order]);
245
+ const pickedSet = useMemo(() => new Set(pickedColumns), [pickedColumns]);
258
246
  const columnBlock: RangeBlock | null = pickedColumns.length > 0 && rows.length > 0 ? { top: 0, bottom: rows.length - 1, columns: pickedColumns } : null;
259
247
  const cellBlock: RangeBlock | null = rect === null ? null : { top: rect.top, bottom: rect.bottom, columns: order.slice(rect.left, rect.right + 1) };
260
248
  const block = columnBlock ?? cellBlock;
@@ -296,7 +284,9 @@ export function DataGrid({
296
284
  const index = focusRequest === null || focusRequest === undefined ? -1 : rows.findIndex((row) => row.key === focusRequest.rowKey);
297
285
  if (focusRequest !== null && focusRequest !== undefined && index >= 0) {
298
286
  focusAt({ row: index, column: focusRequest.column });
299
- scroller.current?.focus();
287
+ if (focusRequest.quiet !== true) {
288
+ scroller.current?.focus();
289
+ }
300
290
  }
301
291
  // Only a new request moves the focus; the rows changing under a request already handled must not.
302
292
  // oxlint-disable-next-line react-hooks/exhaustive-deps
@@ -337,7 +327,7 @@ export function DataGrid({
337
327
  [measure, ref]
338
328
  );
339
329
 
340
- const cellId = (row: number, column: number): string => `${gridId}-${row}-${column}`;
330
+ const cellId = (row: number, column: number): string => cellIdOf(gridId, row, column);
341
331
 
342
332
  const canEdit = (position: CellPosition): boolean => {
343
333
  const row = rows[position.row];
@@ -649,124 +639,93 @@ export function DataGrid({
649
639
  onTogglePin: (index) => changeView((view) => togglePin(view, index))
650
640
  };
651
641
 
642
+ const look = useMemo(
643
+ (): RowLook => ({
644
+ gridId,
645
+ columns,
646
+ order,
647
+ widths: shownWidths,
648
+ pinnedShown,
649
+ gutter,
650
+ notation,
651
+ enumTypes,
652
+ picked: pickedSet,
653
+ loadingLabel: t('grid.loadingValue')
654
+ }),
655
+ [gridId, columns, order, shownWidths, pinnedShown, gutter, notation, enumTypes, pickedSet, t]
656
+ );
657
+
658
+ const stablePressCell = useStableCallback(pressCell);
659
+ const stableDragTo = useStableCallback(dragTo);
660
+ const stableBeginEdit = useStableCallback((position: CellPosition) => void beginEdit(position));
661
+ const stableOpenCellMenu = useStableCallback(openCellMenu);
662
+ const stablePressRowNumber = useStableCallback((index: number, event: MouseEvent) => {
663
+ if (!pressedInPopup()) {
664
+ selectRowAt(index, { shiftKey: event.shiftKey, mod: isModHeld(event, isApplePlatform()) });
665
+ }
666
+ });
667
+ const stableOpenRowMenu = useStableCallback(openRowMenu);
668
+ const stableFinishPick = useStableCallback(finishPick);
669
+ const stableFinishEdit = useStableCallback(finishEdit);
670
+ const rowActions = useMemo(
671
+ (): RowActions => ({
672
+ pressCell: stablePressCell,
673
+ dragTo: stableDragTo,
674
+ beginEdit: stableBeginEdit,
675
+ openCellMenu: stableOpenCellMenu,
676
+ pressRowNumber: stablePressRowNumber,
677
+ openRowMenu: stableOpenRowMenu,
678
+ finishPick: stableFinishPick,
679
+ finishEdit: stableFinishEdit,
680
+ changeDraft: (draft) => setEditing((now) => (now === null ? now : { ...now, draft }))
681
+ }),
682
+ [stablePressCell, stableDragTo, stableBeginEdit, stableOpenCellMenu, stablePressRowNumber, stableOpenRowMenu, stableFinishPick, stableFinishEdit]
683
+ );
684
+
652
685
  const renderRow = (index: number): ReactNode => {
653
686
  const row = rows[index]!;
654
- const selected = selection.has(row.key);
655
- const tint = row.state === undefined ? (selected ? 'bg-accent-soft/40' : '') : ROW_STATE[row.state];
687
+ const inRect = columnBlock === null && rect !== null && index >= rect.top && index <= rect.bottom;
656
688
  return (
657
- <div
689
+ <GridRowView
658
690
  key={row.key}
659
- role="row"
660
- aria-rowindex={index + 2}
661
- aria-selected={selected}
662
- className={clsx('group/row absolute inset-x-0 flex border-b border-border-soft', tint, tint === '' && 'hover:bg-surface-hover')}
663
- style={{ top: index * ROW_HEIGHT, height: ROW_HEIGHT }}
664
- >
665
- <div
666
- role="rowheader"
667
- data-selected={selected ? '' : undefined}
668
- className={clsx(
669
- 'sticky left-0 z-10 flex shrink-0 cursor-default items-center justify-end border-r border-border bg-surface pr-2 pl-1 font-mono text-xs text-text-faint tabular-nums select-none data-[selected]:bg-accent-soft data-[selected]:text-text',
670
- row.state === 'inserted' && 'text-positive'
671
- )}
672
- style={{ width: gutter }}
673
- onClick={(event: MouseEvent) => {
674
- if (!pressedInPopup()) {
675
- selectRowAt(index, { shiftKey: event.shiftKey, mod: isModHeld(event, isApplePlatform()) });
676
- }
677
- }}
678
- onContextMenu={() => openRowMenu(index)}
679
- >
680
- {row.number === null ? '+' : formatNumber(row.number)}
681
- </div>
682
- {order.map((columnIndex, position) => {
683
- const column = columns[columnIndex]!;
684
- const cell = row.cells[columnIndex] ?? null;
685
- const view = cellView(cell, column.kind, notation);
686
- const cellPosition = { row: index, column: columnIndex };
687
- const isFocused = focusCell !== null && focusCell.row === index && focusCell.column === columnIndex;
688
- const ranged = pickedSet.has(columnIndex) || (columnBlock === null && rectContains(rect, index, position));
689
- const edited = row.edited?.has(columnIndex) === true;
690
- const isEditing = editing !== null && editing.row === index && editing.column === columnIndex;
691
- const pinned = position < pinnedShown;
692
- const enumType = enumTypes[columnIndex] ?? null;
693
- const followable = !isEditing && followCell({ rowKey: row.key, column: columnIndex });
694
- return (
695
- <div
696
- key={columnIndex}
697
- id={cellId(index, columnIndex)}
698
- role="gridcell"
699
- aria-colindex={position + 2}
700
- aria-selected={ranged}
701
- data-focused={isFocused ? '' : undefined}
702
- data-ranged={ranged ? '' : undefined}
703
- className={clsx(
704
- 'group/cell flex h-full shrink-0 items-center overflow-hidden px-3 font-mono text-code whitespace-nowrap outline-0 select-none data-[focused]:outline-1 data-[focused]:-outline-offset-1 data-[focused]:outline-border-strong group-focus-within/grid:data-[focused]:outline-accent',
705
- position === order.length - 1 ? 'border-r-0' : 'border-r',
706
- view.align === 'end' ? 'justify-end tabular-nums' : 'justify-start',
707
- pinned
708
- ? [
709
- 'sticky z-5',
710
- position === pinnedShown - 1 ? 'border-border-strong' : 'border-border-soft',
711
- ranged ? 'bg-accent-soft' : ['bg-surface', tint === '' && 'group-hover/row:bg-surface-hover']
712
- ]
713
- : ['relative border-border-soft', ranged ? 'bg-accent-soft' : edited && 'bg-accent/10']
714
- )}
715
- style={{ width: shownWidths[position], left: pinned ? gutter + (offsets[position] ?? 0) : undefined }}
716
- onPointerDown={(event) => pressCell(event, cellPosition, isEditing)}
717
- onPointerEnter={() => dragTo(cellPosition)}
718
- onDoubleClick={() => void beginEdit(cellPosition)}
719
- onContextMenu={() => openCellMenu(cellPosition)}
720
- >
721
- {pinned && !ranged && <span aria-hidden className={clsx('pointer-events-none absolute inset-0', tint, edited && 'bg-accent/10')} />}
722
- {isEditing && editing.loading && <Spinner size={12} label={t('grid.loadingValue')} />}
723
- {isEditing && !editing.loading && enumType !== null && (
724
- <EnumPicker
725
- autoOpen
726
- type={enumType}
727
- value={typeof cell === 'string' ? cell : null}
728
- nullable={column.nullable !== false}
729
- label={column.name}
730
- onDone={finishPick}
731
- />
732
- )}
733
- {isEditing && !editing.loading && enumType === null && (
734
- <CellEditor
735
- value={editing.draft}
736
- label={column.name}
737
- onValueChange={(draft) => setEditing({ ...editing, draft })}
738
- onCommit={(move) => finishEdit(true, move)}
739
- onCancel={() => finishEdit(false)}
740
- />
741
- )}
742
- {!isEditing && (
743
- <>
744
- <span className={clsx('min-w-0 truncate', TONE[view.tone], row.state === 'deleted' && 'line-through')}>{view.text}</span>
745
- {view.suffix !== undefined && <span className="ml-2 shrink-0 text-text-faint">{view.suffix}</span>}
746
- </>
747
- )}
748
- {followable && (
749
- <IconButton
750
- icon={ArrowUpRight}
751
- size="2xs"
752
- label={t('grid.followReference')}
753
- tabIndex={-1}
754
- className="absolute top-1/2 right-1 -translate-y-1/2 bg-surface-raised opacity-0 group-hover/cell:opacity-100 group-data-[focused]/cell:opacity-100 focus-visible:opacity-100"
755
- onPointerDown={(event) => event.stopPropagation()}
756
- onClick={() => {
757
- if (!pressedInPopup()) {
758
- onFollow?.({ rowKey: row.key, column: columnIndex });
759
- }
760
- }}
761
- />
762
- )}
763
- </div>
764
- );
765
- })}
766
- </div>
691
+ row={row}
692
+ index={index}
693
+ selected={selection.has(row.key)}
694
+ focusedColumn={focusCell !== null && focusCell.row === index ? focusCell.column : -1}
695
+ rangeStart={inRect ? rect.left : -1}
696
+ rangeEnd={inRect ? rect.right : -1}
697
+ editing={editing !== null && editing.row === index ? editing : null}
698
+ look={look}
699
+ actions={rowActions}
700
+ />
767
701
  );
768
702
  };
769
703
 
704
+ const headerCells = order.map((columnIndex, position) => {
705
+ const column = columns[columnIndex]!;
706
+ return (
707
+ <GridHeaderCell
708
+ key={columnIndex}
709
+ column={column}
710
+ index={columnIndex}
711
+ position={position}
712
+ width={shownWidths[position]!}
713
+ sortable={sortable}
714
+ selected={pickedSet.has(columnIndex)}
715
+ id={headerId(columnIndex)}
716
+ sort={sortable ? sortStateOf(sortList, column.name) : null}
717
+ multipleSorts={sortList.length > 1}
718
+ hasSorts={sortList.length > 0}
719
+ pinned={position < pinnedShown}
720
+ lastPinned={position === pinnedShown - 1}
721
+ last={position === order.length - 1}
722
+ hasHidden={current.view.hidden.size > 0}
723
+ canHide={order.length > 1}
724
+ actions={headerActions}
725
+ />
726
+ );
727
+ });
728
+
770
729
  const menuRow = menuTarget === null ? undefined : rows[menuTarget.row];
771
730
  const menuCell: CellPosition | null = menuTarget === null || menuTarget.column === null ? null : { row: menuTarget.row, column: menuTarget.column };
772
731
  const settable = menuCell !== null && canEdit(menuCell);
@@ -794,36 +753,15 @@ export function DataGrid({
794
753
  tabIndex={0}
795
754
  className={clsx('group/grid relative min-h-0 flex-1 overflow-auto bg-surface text-text outline-none', className)}
796
755
  onKeyDown={handleKeyDown}
797
- onScroll={(event) => setScrollTop(Math.floor(event.currentTarget.scrollTop / ROW_HEIGHT) * ROW_HEIGHT)}
756
+ onScroll={(event) => setScrolledBlock(scrollBlock(event.currentTarget.scrollTop - HEADER_HEIGHT))}
798
757
  >
799
758
  <div className="relative min-w-full" style={{ width: totalWidth }}>
800
759
  <div role="row" aria-rowindex={1} className="sticky top-0 z-20 flex w-full border-b border-border bg-surface" style={{ height: HEADER_HEIGHT }}>
801
- <div className="sticky left-0 z-10 shrink-0 border-r border-border bg-surface" style={{ width: gutter }} />
802
- {order.map((columnIndex, position) => {
803
- const column = columns[columnIndex]!;
804
- return (
805
- <GridHeaderCell
806
- key={columnIndex}
807
- column={column}
808
- index={columnIndex}
809
- position={position}
810
- width={shownWidths[position]!}
811
- sortable={sortable}
812
- selected={pickedSet.has(columnIndex)}
813
- id={headerId(columnIndex)}
814
- sort={sortable ? sortStateOf(sortList, column.name) : null}
815
- multipleSorts={sortList.length > 1}
816
- hasSorts={sortList.length > 0}
817
- pinned={position < pinnedShown}
818
- stickyLeft={gutter + (offsets[position] ?? 0)}
819
- lastPinned={position === pinnedShown - 1}
820
- last={position === order.length - 1}
821
- hasHidden={current.view.hidden.size > 0}
822
- canHide={order.length > 1}
823
- actions={headerActions}
824
- />
825
- );
826
- })}
760
+ <div className={PINNED_GROUP}>
761
+ <div className="shrink-0 border-r border-border bg-surface bg-clip-border" style={{ width: gutter }} />
762
+ {headerCells.slice(0, pinnedShown)}
763
+ </div>
764
+ {headerCells.slice(pinnedShown)}
827
765
  </div>
828
766
  <ContextMenu.Root>
829
767
  <ContextMenu.Trigger role="rowgroup" className="relative" style={{ height: rows.length * ROW_HEIGHT }}>
@@ -3,6 +3,7 @@ import clsx from 'clsx';
3
3
  import { Ban, ChevronDown } from 'lucide-react';
4
4
  import { useTranslation } from 'react-i18next';
5
5
  import { Icon, Menu } from '@adecore/ui';
6
+ import { CODE_TEXT } from '../code-text.ts';
6
7
  import { joinMembers, membersOf, type EnumType } from './enum-type.ts';
7
8
 
8
9
  export interface EnumPickerProps {
@@ -24,7 +25,7 @@ export interface EnumPickerProps {
24
25
  }
25
26
 
26
27
  const LOOK = {
27
- cell: 'absolute inset-0 z-10 flex items-center gap-2 border border-accent bg-surface-raised px-3 font-mono text-code outline-0',
28
+ cell: `absolute inset-0 z-10 flex items-center gap-2 border border-accent bg-surface-raised px-3 ${CODE_TEXT} outline-0`,
28
29
  field: 'field field-sm flex w-full items-center gap-2 font-mono text-code data-disabled:opacity-50'
29
30
  } as const;
30
31
 
@@ -1,9 +1,12 @@
1
1
  import { useRef, type MouseEvent, type PointerEvent } from 'react';
2
2
  import clsx from 'clsx';
3
- import { ArrowDown, ArrowUp, ArrowUpDown, ChevronDown, Copy, Eye, EyeOff, Key, Pin, PinOff } from 'lucide-react';
3
+ import { ArrowDown, ArrowUp, ArrowUpDown, ChevronDown, Copy, Eye, EyeOff, Pin, PinOff } from 'lucide-react';
4
4
  import { useTranslation } from 'react-i18next';
5
5
  import { ColumnResizeHandle, ContextMenu, copyText, Icon, IconButton, isApplePlatform, isModHeld, Menu, Tooltip, useColumnResize } from '@adecore/ui';
6
6
  import { formatNumber } from '@adecore/ui/format';
7
+ import { CODE_TEXT } from '../code-text.ts';
8
+ import { keyLabelOf } from '../column-keys.ts';
9
+ import { KeyIcon } from '../KeyIcon.tsx';
7
10
  import type { SortDirection } from '../sql.ts';
8
11
  import type { ColumnClick } from './column-selection.ts';
9
12
  import { isNumericKind } from './display.ts';
@@ -41,8 +44,6 @@ export interface GridHeaderCellProps {
41
44
  multipleSorts: boolean;
42
45
  hasSorts: boolean;
43
46
  pinned: boolean;
44
- /* Where a pinned header sticks, counted from the left edge of the scroll box. */
45
- stickyLeft: number;
46
47
  /* The pinned column the others scroll under, which carries the stronger edge. */
47
48
  lastPinned: boolean;
48
49
  last: boolean;
@@ -101,7 +102,9 @@ function HeaderMenuItems({ column, index, sortable, hasSorts, pinned, hasHidden,
101
102
  /* The facts about a table column that a tooltip can carry: its type, whether it takes NULL, its default. */
102
103
  function ColumnFacts({ column }: { column: GridColumn }) {
103
104
  const { t } = useTranslation('database');
105
+ const keys = { primaryKey: column.primaryKey === true, foreignKey: column.foreignKey === true };
104
106
  const facts = [
107
+ keys.primaryKey || keys.foreignKey ? t(keyLabelOf(keys)) : null,
105
108
  column.type === '' ? null : column.type,
106
109
  column.nullable === undefined ? null : t(column.nullable ? 'grid.column.nullable' : 'grid.column.notNull'),
107
110
  column.defaultValue === undefined || column.defaultValue === null ? null : t('grid.column.default', { value: column.defaultValue }),
@@ -115,7 +118,7 @@ function ColumnFacts({ column }: { column: GridColumn }) {
115
118
  );
116
119
  }
117
120
 
118
- /* One column's head: its name, the sort it carries, a key on a primary key column, a menu and the strip that resizes it. Numbers sit against the end, like their cells. */
121
+ /* One column's head: its name, the sort it carries, the key of a key column, a menu and the strip that resizes it. Numbers sit against the end, like their cells. */
119
122
  export function GridHeaderCell({
120
123
  column,
121
124
  index,
@@ -128,7 +131,6 @@ export function GridHeaderCell({
128
131
  multipleSorts,
129
132
  hasSorts,
130
133
  pinned,
131
- stickyLeft,
132
134
  lastPinned,
133
135
  last,
134
136
  hasHidden,
@@ -186,13 +188,16 @@ export function GridHeaderCell({
186
188
  aria-sort={sortable ? (sort === null ? 'none' : sort.direction === 'asc' ? 'ascending' : 'descending') : undefined}
187
189
  data-selected={selected ? '' : undefined}
188
190
  className={clsx(
189
- 'group/header flex h-full shrink-0 cursor-default text-xs font-medium select-none',
190
- pinned ? 'sticky z-10' : 'relative',
191
+ // The name in the face and size of the cells under it, which is how the column is written in SQL too.
192
+ 'group/header flex h-full shrink-0 cursor-default font-medium select-none',
193
+ CODE_TEXT,
194
+ 'relative',
195
+ pinned && 'bg-clip-border',
191
196
  selected ? 'bg-accent text-accent-text' : [pinned && 'bg-surface', sort === null ? 'text-text-muted' : 'text-text'],
192
197
  last ? 'border-r-0' : 'border-r',
193
198
  pinned && lastPinned ? 'border-border-strong' : 'border-border-soft'
194
199
  )}
195
- style={{ width, left: pinned ? stickyLeft : undefined }}
200
+ style={{ width }}
196
201
  onPointerDownCapture={() => {
197
202
  resized.current = false;
198
203
  }}
@@ -204,7 +209,7 @@ export function GridHeaderCell({
204
209
  numeric && 'justify-end group-data-[selected]/header:pr-8'
205
210
  )}
206
211
  >
207
- {column.primaryKey === true && <Icon icon={Key} size={12} className="shrink-0 text-(--file-icon-yellow)" />}
212
+ <KeyIcon primaryKey={column.primaryKey === true} foreignKey={column.foreignKey === true} size={12} />
208
213
  <Tooltip label={<ColumnFacts column={column} />} side="bottom">
209
214
  <span className="min-w-0 truncate">{column.name}</span>
210
215
  </Tooltip>