@mshuja288/aksuicore 0.4.0 → 0.4.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.
@@ -21,6 +21,25 @@ function useIsClient() {
21
21
  );
22
22
  }
23
23
 
24
+ /** Hover tooltips only on fine pointers (desktop). Touch / narrow layouts never get them. */
25
+ function useHoverTooltipsEnabled() {
26
+ return useSyncExternalStore(
27
+ (onStoreChange) => {
28
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
29
+ return () => {};
30
+ }
31
+ const media = window.matchMedia("(hover: hover) and (pointer: fine)");
32
+ media.addEventListener("change", onStoreChange);
33
+ return () => media.removeEventListener("change", onStoreChange);
34
+ },
35
+ () =>
36
+ typeof window !== "undefined" &&
37
+ typeof window.matchMedia === "function" &&
38
+ window.matchMedia("(hover: hover) and (pointer: fine)").matches,
39
+ () => false,
40
+ );
41
+ }
42
+
24
43
  export type TableColumn<T> = {
25
44
  key: string;
26
45
  header: string;
@@ -195,6 +214,21 @@ function MoreIcon() {
195
214
  );
196
215
  }
197
216
 
217
+ function ToolsIcon() {
218
+ return (
219
+ <svg viewBox="0 0 20 20" aria-hidden="true">
220
+ <path
221
+ d="M11.5 3.5h5v5M16.2 3.8 10 10M8.2 5.2H3.8v11h11V12"
222
+ fill="none"
223
+ stroke="currentColor"
224
+ strokeWidth="1.6"
225
+ strokeLinecap="round"
226
+ strokeLinejoin="round"
227
+ />
228
+ </svg>
229
+ );
230
+ }
231
+
198
232
  function DetailList({ lines }: { lines: DetailLine[] }) {
199
233
  return (
200
234
  <dl className="record-detail-list">
@@ -278,6 +312,7 @@ export function RecordWorkspace<T>({
278
312
  const [filters, setFilters] = useState<Record<string, string>>({});
279
313
  const [sort, setSort] = useState<SortState | null>(null);
280
314
  const [sideOpen, setSideOpen] = useState(false);
315
+ const [toolsDrawerOpen, setToolsDrawerOpen] = useState(false);
281
316
  const [downloadOpen, setDownloadOpen] = useState(false);
282
317
  const [columnsOpen, setColumnsOpen] = useState(false);
283
318
  const [previewEnabled, setPreviewEnabled] = useState(false);
@@ -285,9 +320,15 @@ export function RecordWorkspace<T>({
285
320
  const [tooltip, setTooltip] = useState<TooltipState | null>(null);
286
321
  const [pageSize, setPageSize] = useState("15");
287
322
  const [page, setPage] = useState(1);
288
- const [actionsMenuKey, setActionsMenuKey] = useState<string | number | null>(null);
323
+ const [actionsMenu, setActionsMenu] = useState<{
324
+ key: string | number;
325
+ item: T;
326
+ x: number;
327
+ y: number;
328
+ } | null>(null);
289
329
  const actionsMenuRef = useRef<HTMLDivElement | null>(null);
290
330
  const isClient = useIsClient();
331
+ const hoverTooltipsEnabled = useHoverTooltipsEnabled();
291
332
 
292
333
  useEffect(() => {
293
334
  if (isControlled) {
@@ -376,6 +417,7 @@ export function RecordWorkspace<T>({
376
417
  );
377
418
 
378
419
  const hiddenColumnCount = columns.length - visibleColumns.length;
420
+ const toolsBadgeCount = activeFilterCount + (hiddenColumnCount > 0 ? 1 : 0) + (previewEnabled ? 1 : 0);
379
421
  const showAll = pageSize === "all";
380
422
  const numericPageSize = showAll ? Math.max(sorted.length, 1) : Number(pageSize);
381
423
  const pageCount = showAll ? 1 : Math.max(1, Math.ceil(sorted.length / numericPageSize));
@@ -391,21 +433,42 @@ export function RecordWorkspace<T>({
391
433
  key: column.exportKey ?? column.key,
392
434
  header: column.header,
393
435
  }));
436
+ const canShowTooltip = hoverTooltipsEnabled && !previewEnabled && actionsMenu === null;
394
437
 
395
438
  useEffect(() => {
396
- if (actionsMenuKey === null) {
439
+ if (actionsMenu === null) {
397
440
  return;
398
441
  }
399
442
 
400
443
  function onPointerDown(event: globalThis.MouseEvent) {
401
444
  if (actionsMenuRef.current && !actionsMenuRef.current.contains(event.target as Node)) {
402
- setActionsMenuKey(null);
445
+ setActionsMenu(null);
403
446
  }
404
447
  }
405
448
 
406
449
  document.addEventListener("mousedown", onPointerDown);
407
450
  return () => document.removeEventListener("mousedown", onPointerDown);
408
- }, [actionsMenuKey]);
451
+ }, [actionsMenu]);
452
+
453
+ useEffect(() => {
454
+ if (!toolsDrawerOpen) {
455
+ return;
456
+ }
457
+
458
+ function onKeyDown(event: KeyboardEvent) {
459
+ if (event.key === "Escape") {
460
+ setToolsDrawerOpen(false);
461
+ }
462
+ }
463
+
464
+ document.addEventListener("keydown", onKeyDown);
465
+ const previousOverflow = document.body.style.overflow;
466
+ document.body.style.overflow = "hidden";
467
+ return () => {
468
+ document.removeEventListener("keydown", onKeyDown);
469
+ document.body.style.overflow = previousOverflow;
470
+ };
471
+ }, [toolsDrawerOpen]);
409
472
 
410
473
  function requestDownload(format: ExportDownloadFormat) {
411
474
  if (!onDownload || downloadBusy) {
@@ -470,6 +533,9 @@ export function RecordWorkspace<T>({
470
533
  }
471
534
 
472
535
  function showTooltip(item: T, event: MouseEvent) {
536
+ if (!canShowTooltip) {
537
+ return;
538
+ }
473
539
  const padding = 16;
474
540
  const width = 280;
475
541
  const x = Math.min(event.clientX + 14, window.innerWidth - width - padding);
@@ -477,6 +543,22 @@ export function RecordWorkspace<T>({
477
543
  setTooltip({ x, y, lines: detailLines(item) });
478
544
  }
479
545
 
546
+ function openRowActions(item: T, event: MouseEvent<HTMLButtonElement>) {
547
+ event.stopPropagation();
548
+ setTooltip(null);
549
+ const key = rowKey(item);
550
+ if (actionsMenu?.key === key) {
551
+ setActionsMenu(null);
552
+ return;
553
+ }
554
+ const rect = event.currentTarget.getBoundingClientRect();
555
+ const menuWidth = 168;
556
+ const menuHeight = 148;
557
+ const x = Math.min(Math.max(8, rect.right - menuWidth), window.innerWidth - menuWidth - 8);
558
+ const y = Math.min(rect.bottom + 6, window.innerHeight - menuHeight - 8);
559
+ setActionsMenu({ key, item, x, y });
560
+ }
561
+
480
562
  const summary =
481
563
  sorted.length === 0
482
564
  ? "0 records"
@@ -484,6 +566,225 @@ export function RecordWorkspace<T>({
484
566
  ? `1–${sorted.length} of ${sorted.length}`
485
567
  : `${start + 1}–${Math.min(start + numericPageSize, sorted.length)} of ${sorted.length}`;
486
568
 
569
+ function renderToolsBody({ compactIcons }: { compactIcons: boolean }) {
570
+ if (compactIcons) {
571
+ return (
572
+ <div className="side-rail-icons">
573
+ <label className="side-rail-icon preview-check-icon" title="Preview">
574
+ <input
575
+ type="checkbox"
576
+ checked={previewEnabled}
577
+ onChange={(event) => setPreviewEnabledValue(event.target.checked)}
578
+ aria-label="Preview"
579
+ />
580
+ <PreviewIcon />
581
+ </label>
582
+ {showAdd ? (
583
+ <button type="button" className="ghost side-rail-icon" title={addLabel} aria-label={addLabel} onClick={onAdd}>
584
+ <PlusIcon />
585
+ </button>
586
+ ) : null}
587
+ {showColumns ? (
588
+ <button
589
+ type="button"
590
+ className="ghost side-rail-icon"
591
+ title="Columns"
592
+ aria-label="Columns"
593
+ onClick={() => {
594
+ setSideOpen(true);
595
+ setColumnsOpen(true);
596
+ }}
597
+ >
598
+ <ColumnsIcon />
599
+ {hiddenColumnCount > 0 ? <span className="filter-badge">{hiddenColumnCount}</span> : null}
600
+ </button>
601
+ ) : null}
602
+ {showDownload ? (
603
+ <button
604
+ type="button"
605
+ className="ghost side-rail-icon"
606
+ title="Download"
607
+ aria-label="Download"
608
+ disabled={downloadBusy}
609
+ onClick={() => {
610
+ setSideOpen(true);
611
+ setDownloadOpen(true);
612
+ }}
613
+ >
614
+ <DownloadIcon />
615
+ </button>
616
+ ) : null}
617
+ {showFilters ? (
618
+ <button
619
+ type="button"
620
+ className="ghost side-rail-icon"
621
+ title="Show filters"
622
+ aria-label="Show filters"
623
+ onClick={() => setSideOpen(true)}
624
+ >
625
+ <FilterIcon />
626
+ {activeFilterCount > 0 ? <span className="filter-badge">{activeFilterCount}</span> : null}
627
+ </button>
628
+ ) : null}
629
+ {showFilters ? (
630
+ <button
631
+ type="button"
632
+ className="ghost side-rail-icon"
633
+ title="Reset filters"
634
+ aria-label="Reset filters"
635
+ disabled={activeFilterCount === 0}
636
+ onClick={clearFilters}
637
+ >
638
+ <ResetIcon />
639
+ </button>
640
+ ) : null}
641
+ </div>
642
+ );
643
+ }
644
+
645
+ return (
646
+ <div className="side-rail-body">
647
+ <div className="preview-check">
648
+ <input
649
+ type="checkbox"
650
+ checked={previewEnabled}
651
+ onChange={(event) => setPreviewEnabledValue(event.target.checked)}
652
+ aria-label="Preview"
653
+ />
654
+ <PreviewIcon />
655
+ <span aria-hidden="true">Preview</span>
656
+ </div>
657
+
658
+ {showAdd ? (
659
+ <button
660
+ type="button"
661
+ className="side-rail-add"
662
+ onClick={() => {
663
+ setToolsDrawerOpen(false);
664
+ onAdd?.();
665
+ }}
666
+ >
667
+ <PlusIcon />
668
+ <span>{addLabel}</span>
669
+ </button>
670
+ ) : null}
671
+
672
+ {showColumns ? (
673
+ <div className="side-rail-columns">
674
+ <button
675
+ type="button"
676
+ className="ghost side-rail-columns-toggle"
677
+ aria-expanded={columnsOpen}
678
+ onClick={() => setColumnsOpen((open) => !open)}
679
+ >
680
+ <ColumnsIcon />
681
+ <span>Columns{hiddenColumnCount > 0 ? ` (${visibleColumns.length}/${columns.length})` : ""}</span>
682
+ </button>
683
+ {columnsOpen ? (
684
+ <div className="side-rail-columns-panel" role="group" aria-label="Visible columns">
685
+ <div className="side-rail-columns-actions">
686
+ <button type="button" className="ghost" onClick={selectAllColumns}>
687
+ Select all
688
+ </button>
689
+ <button type="button" className="ghost" onClick={resetColumns}>
690
+ Reset
691
+ </button>
692
+ </div>
693
+ <div className="side-rail-columns-list">
694
+ {columns.map((column) => {
695
+ const checked = resolvedVisibleKeys.includes(column.key);
696
+ const disableUncheck = checked && resolvedVisibleKeys.length <= 1;
697
+ return (
698
+ <label key={column.key} className="side-rail-column-option">
699
+ <input
700
+ type="checkbox"
701
+ checked={checked}
702
+ disabled={disableUncheck}
703
+ onChange={(event) => toggleColumnVisible(column.key, event.target.checked)}
704
+ />
705
+ <span>{column.header}</span>
706
+ </label>
707
+ );
708
+ })}
709
+ </div>
710
+ </div>
711
+ ) : null}
712
+ </div>
713
+ ) : null}
714
+
715
+ {showDownload ? (
716
+ <div className="side-rail-download">
717
+ <button
718
+ type="button"
719
+ className="ghost side-rail-download-toggle"
720
+ aria-expanded={downloadOpen}
721
+ disabled={downloadBusy}
722
+ onClick={() => setDownloadOpen((open) => !open)}
723
+ >
724
+ <DownloadIcon />
725
+ <span>{downloadBusy ? "Downloading…" : "Download"}</span>
726
+ </button>
727
+ {downloadOpen ? (
728
+ <div className="side-rail-download-formats" role="group" aria-label="Download format">
729
+ {EXPORT_FORMAT_OPTIONS.map((option) => (
730
+ <button
731
+ key={option.format}
732
+ type="button"
733
+ className="ghost side-rail-download-format"
734
+ disabled={downloadBusy}
735
+ onClick={() => requestDownload(option.format)}
736
+ >
737
+ {option.label}
738
+ </button>
739
+ ))}
740
+ </div>
741
+ ) : null}
742
+ </div>
743
+ ) : null}
744
+
745
+ {showFilters ? (
746
+ <div className="side-rail-filters">
747
+ <div className="filter-panel-head">
748
+ <h3>Filters</h3>
749
+ <button
750
+ type="button"
751
+ className="ghost filter-clear"
752
+ title="Reset filters"
753
+ aria-label="Reset filters"
754
+ disabled={activeFilterCount === 0}
755
+ onClick={clearFilters}
756
+ >
757
+ <ResetIcon />
758
+ <span>Reset</span>
759
+ </button>
760
+ </div>
761
+ <div className="filter-panel-stack">
762
+ {filterableColumns.map((column) => (
763
+ <label key={column.key}>
764
+ {column.header}
765
+ {column.filterOptions ? (
766
+ <Select
767
+ value={filters[column.key] ?? ""}
768
+ options={[{ value: "", label: column.filterPlaceholder ?? "All" }, ...column.filterOptions]}
769
+ onChange={(value) => changeFilter(column.key, value)}
770
+ placeholder={column.filterPlaceholder ?? "All"}
771
+ />
772
+ ) : (
773
+ <input
774
+ value={filters[column.key] ?? ""}
775
+ onChange={(event) => changeFilter(column.key, event.target.value)}
776
+ placeholder={`Filter ${column.header.toLowerCase()}`}
777
+ />
778
+ )}
779
+ </label>
780
+ ))}
781
+ </div>
782
+ </div>
783
+ ) : null}
784
+ </div>
785
+ );
786
+ }
787
+
487
788
  function renderRowActions(item: T) {
488
789
  const key = rowKey(item);
489
790
  const updateAllowed = canUpdate ? canUpdate(item) : Boolean(onUpdate);
@@ -525,69 +826,60 @@ export function RecordWorkspace<T>({
525
826
  ) : null}
526
827
  </div>
527
828
 
528
- <div className="row-actions row-actions-mobile" ref={actionsMenuKey === key ? actionsMenuRef : undefined}>
829
+ <div className="row-actions row-actions-mobile">
529
830
  <button
530
831
  type="button"
531
- className="ghost row-action-menu-trigger"
832
+ className={actionsMenu?.key === key ? "ghost row-action-menu-trigger open" : "ghost row-action-menu-trigger"}
532
833
  aria-haspopup="menu"
533
- aria-expanded={actionsMenuKey === key}
834
+ aria-expanded={actionsMenu?.key === key}
534
835
  aria-label="Row actions"
535
- onClick={() => setActionsMenuKey((current) => (current === key ? null : key))}
836
+ onClick={(event) => openRowActions(item, event)}
536
837
  >
537
838
  <MoreIcon />
538
839
  </button>
539
- {actionsMenuKey === key ? (
540
- <div className="row-action-menu" role="menu">
541
- {onView ? (
542
- <button
543
- type="button"
544
- role="menuitem"
545
- className="ghost"
546
- onClick={() => {
547
- setActionsMenuKey(null);
548
- onView(item);
549
- }}
550
- >
551
- View
552
- </button>
553
- ) : null}
554
- {onUpdate ? (
555
- <button
556
- type="button"
557
- role="menuitem"
558
- className="ghost"
559
- disabled={!updateAllowed}
560
- onClick={() => {
561
- setActionsMenuKey(null);
562
- onUpdate(item);
563
- }}
564
- >
565
- Update
566
- </button>
567
- ) : null}
568
- {onDelete ? (
569
- <button
570
- type="button"
571
- role="menuitem"
572
- className="ghost danger"
573
- disabled={!deleteAllowed}
574
- onClick={() => {
575
- setActionsMenuKey(null);
576
- onDelete(item);
577
- }}
578
- >
579
- Delete
580
- </button>
581
- ) : null}
582
- </div>
583
- ) : null}
584
840
  </div>
585
841
  </>
586
842
  );
587
843
  }
588
844
 
845
+ const actionsMenuItem = actionsMenu?.item;
846
+ const actionsUpdateAllowed = actionsMenuItem
847
+ ? canUpdate
848
+ ? canUpdate(actionsMenuItem)
849
+ : Boolean(onUpdate)
850
+ : false;
851
+ const actionsDeleteAllowed = actionsMenuItem
852
+ ? canDelete
853
+ ? canDelete(actionsMenuItem)
854
+ : Boolean(onDelete)
855
+ : false;
856
+
589
857
  return (
590
- <div className="list-workspace" aria-label={title}>
858
+ <div className={toolsDrawerOpen ? "list-workspace tools-drawer-open" : "list-workspace"} aria-label={title}>
859
+ <div className="tools-mobile-bar">
860
+ <div className="tools-mobile-bar-start">
861
+ {showAdd ? (
862
+ <button type="button" className="tools-mobile-add" onClick={onAdd}>
863
+ <PlusIcon />
864
+ <span>{addLabel}</span>
865
+ </button>
866
+ ) : (
867
+ <p className="tools-mobile-title">{title}</p>
868
+ )}
869
+ </div>
870
+ <button
871
+ type="button"
872
+ className="ghost tools-mobile-toggle"
873
+ aria-expanded={toolsDrawerOpen}
874
+ aria-controls="aksui-tools-drawer"
875
+ onClick={() => setToolsDrawerOpen(true)}
876
+ >
877
+ <ToolsIcon />
878
+ <span>Tools</span>
879
+ {toolsBadgeCount > 0 ? <span className="filter-badge">{toolsBadgeCount}</span> : null}
880
+ </button>
881
+ </div>
882
+
591
883
  <div
592
884
  className={["list-body", sideOpen ? "side-open" : "", previewEnabled ? "preview-open" : ""]
593
885
  .filter(Boolean)
@@ -651,16 +943,10 @@ export function RecordWorkspace<T>({
651
943
  key={key}
652
944
  className={selected ? "data-row selected" : "data-row"}
653
945
  onMouseEnter={(event) => {
654
- if (!previewEnabled) {
655
- showTooltip(item, event);
656
- }
946
+ showTooltip(item, event);
657
947
  focusRecord(item);
658
948
  }}
659
- onMouseMove={(event) => {
660
- if (!previewEnabled) {
661
- showTooltip(item, event);
662
- }
663
- }}
949
+ onMouseMove={(event) => showTooltip(item, event)}
664
950
  onMouseLeave={() => setTooltip(null)}
665
951
  onClick={() => focusRecord(item)}
666
952
  >
@@ -704,7 +990,7 @@ export function RecordWorkspace<T>({
704
990
  {previewItem ? (
705
991
  <DetailList lines={detailLines(previewItem)} />
706
992
  ) : (
707
- <p className="preview-empty">Hover or click a row to preview its details here.</p>
993
+ <p className="preview-empty">Select a row to preview its details here.</p>
708
994
  )}
709
995
  </div>
710
996
  </aside>
@@ -721,219 +1007,99 @@ export function RecordWorkspace<T>({
721
1007
  <ChevronIcon open={sideOpen} />
722
1008
  {sideOpen ? <span>Collapse</span> : null}
723
1009
  </button>
1010
+ {sideOpen ? renderToolsBody({ compactIcons: false }) : renderToolsBody({ compactIcons: true })}
1011
+ </aside>
1012
+ </div>
724
1013
 
725
- {sideOpen ? (
726
- <div className="side-rail-body">
727
- <div className="preview-check">
728
- <input
729
- type="checkbox"
730
- checked={previewEnabled}
731
- onChange={(event) => setPreviewEnabledValue(event.target.checked)}
732
- aria-label="Preview"
733
- />
734
- <PreviewIcon />
735
- <span aria-hidden="true">Preview</span>
736
- </div>
737
-
738
- {showAdd ? (
739
- <button type="button" className="side-rail-add" onClick={onAdd}>
740
- <PlusIcon />
741
- <span>{addLabel}</span>
742
- </button>
743
- ) : null}
744
-
745
- {showColumns ? (
746
- <div className="side-rail-columns">
747
- <button
748
- type="button"
749
- className="ghost side-rail-columns-toggle"
750
- aria-expanded={columnsOpen}
751
- onClick={() => setColumnsOpen((open) => !open)}
752
- >
753
- <ColumnsIcon />
754
- <span>Columns{hiddenColumnCount > 0 ? ` (${visibleColumns.length}/${columns.length})` : ""}</span>
1014
+ {isClient
1015
+ ? createPortal(
1016
+ <>
1017
+ <button
1018
+ type="button"
1019
+ className={toolsDrawerOpen ? "tools-drawer-backdrop open" : "tools-drawer-backdrop"}
1020
+ aria-label="Close tools"
1021
+ tabIndex={toolsDrawerOpen ? 0 : -1}
1022
+ onClick={() => setToolsDrawerOpen(false)}
1023
+ />
1024
+ <aside
1025
+ id="aksui-tools-drawer"
1026
+ className={toolsDrawerOpen ? "tools-drawer open" : "tools-drawer"}
1027
+ aria-label="Workspace tools"
1028
+ aria-hidden={!toolsDrawerOpen}
1029
+ >
1030
+ <div className="tools-drawer-head">
1031
+ <div>
1032
+ <p className="tools-drawer-kicker">Workspace</p>
1033
+ <h3>Tools</h3>
1034
+ </div>
1035
+ <button type="button" className="ghost tools-drawer-close" onClick={() => setToolsDrawerOpen(false)}>
1036
+ Close
755
1037
  </button>
756
- {columnsOpen ? (
757
- <div className="side-rail-columns-panel" role="group" aria-label="Visible columns">
758
- <div className="side-rail-columns-actions">
759
- <button type="button" className="ghost" onClick={selectAllColumns}>
760
- Select all
761
- </button>
762
- <button type="button" className="ghost" onClick={resetColumns}>
763
- Reset
764
- </button>
765
- </div>
766
- <div className="side-rail-columns-list">
767
- {columns.map((column) => {
768
- const checked = resolvedVisibleKeys.includes(column.key);
769
- const disableUncheck = checked && resolvedVisibleKeys.length <= 1;
770
- return (
771
- <label key={column.key} className="side-rail-column-option">
772
- <input
773
- type="checkbox"
774
- checked={checked}
775
- disabled={disableUncheck}
776
- onChange={(event) => toggleColumnVisible(column.key, event.target.checked)}
777
- />
778
- <span>{column.header}</span>
779
- </label>
780
- );
781
- })}
782
- </div>
783
- </div>
784
- ) : null}
785
1038
  </div>
786
- ) : null}
1039
+ <div className="tools-drawer-body">{renderToolsBody({ compactIcons: false })}</div>
1040
+ </aside>
1041
+ </>,
1042
+ document.body,
1043
+ )
1044
+ : null}
787
1045
 
788
- {showDownload ? (
789
- <div className="side-rail-download">
790
- <button
791
- type="button"
792
- className="ghost side-rail-download-toggle"
793
- aria-expanded={downloadOpen}
794
- disabled={downloadBusy}
795
- onClick={() => setDownloadOpen((open) => !open)}
796
- >
797
- <DownloadIcon />
798
- <span>{downloadBusy ? "Downloading…" : "Download"}</span>
799
- </button>
800
- {downloadOpen ? (
801
- <div className="side-rail-download-formats" role="group" aria-label="Download format">
802
- {EXPORT_FORMAT_OPTIONS.map((option) => (
803
- <button
804
- key={option.format}
805
- type="button"
806
- className="ghost side-rail-download-format"
807
- disabled={downloadBusy}
808
- onClick={() => requestDownload(option.format)}
809
- >
810
- {option.label}
811
- </button>
812
- ))}
813
- </div>
814
- ) : null}
815
- </div>
816
- ) : null}
1046
+ {form}
817
1047
 
818
- {showFilters ? (
819
- <div className="side-rail-filters">
820
- <div className="filter-panel-head">
821
- <h3>Filters</h3>
822
- <button
823
- type="button"
824
- className="ghost filter-clear"
825
- title="Reset filters"
826
- aria-label="Reset filters"
827
- disabled={activeFilterCount === 0}
828
- onClick={clearFilters}
829
- >
830
- <ResetIcon />
831
- <span>Reset</span>
832
- </button>
833
- </div>
834
- <div className="filter-panel-stack">
835
- {filterableColumns.map((column) => (
836
- <label key={column.key}>
837
- {column.header}
838
- {column.filterOptions ? (
839
- <Select
840
- value={filters[column.key] ?? ""}
841
- options={[{ value: "", label: column.filterPlaceholder ?? "All" }, ...column.filterOptions]}
842
- onChange={(value) => changeFilter(column.key, value)}
843
- placeholder={column.filterPlaceholder ?? "All"}
844
- />
845
- ) : (
846
- <input
847
- value={filters[column.key] ?? ""}
848
- onChange={(event) => changeFilter(column.key, event.target.value)}
849
- placeholder={`Filter ${column.header.toLowerCase()}`}
850
- />
851
- )}
852
- </label>
853
- ))}
854
- </div>
855
- </div>
856
- ) : null}
857
- </div>
858
- ) : (
859
- <div className="side-rail-icons">
860
- <label className="side-rail-icon preview-check-icon" title="Preview">
861
- <input
862
- type="checkbox"
863
- checked={previewEnabled}
864
- onChange={(event) => setPreviewEnabledValue(event.target.checked)}
865
- aria-label="Preview"
866
- />
867
- <PreviewIcon />
868
- </label>
869
- {showAdd ? (
870
- <button type="button" className="ghost side-rail-icon" title={addLabel} aria-label={addLabel} onClick={onAdd}>
871
- <PlusIcon />
872
- </button>
873
- ) : null}
874
- {showColumns ? (
1048
+ {isClient && actionsMenu && actionsMenuItem
1049
+ ? createPortal(
1050
+ <div
1051
+ ref={actionsMenuRef}
1052
+ className="row-action-menu row-action-menu-portal"
1053
+ role="menu"
1054
+ style={{ left: actionsMenu.x, top: actionsMenu.y }}
1055
+ >
1056
+ {onView ? (
875
1057
  <button
876
1058
  type="button"
877
- className="ghost side-rail-icon"
878
- title="Columns"
879
- aria-label="Columns"
1059
+ role="menuitem"
1060
+ className="ghost"
880
1061
  onClick={() => {
881
- setSideOpen(true);
882
- setColumnsOpen(true);
1062
+ setActionsMenu(null);
1063
+ onView(actionsMenuItem);
883
1064
  }}
884
1065
  >
885
- <ColumnsIcon />
886
- {hiddenColumnCount > 0 ? <span className="filter-badge">{hiddenColumnCount}</span> : null}
1066
+ View
887
1067
  </button>
888
1068
  ) : null}
889
- {showDownload ? (
1069
+ {onUpdate ? (
890
1070
  <button
891
1071
  type="button"
892
- className="ghost side-rail-icon"
893
- title="Download"
894
- aria-label="Download"
895
- aria-expanded={downloadOpen}
896
- disabled={downloadBusy}
1072
+ role="menuitem"
1073
+ className="ghost"
1074
+ disabled={!actionsUpdateAllowed}
897
1075
  onClick={() => {
898
- setSideOpen(true);
899
- setDownloadOpen(true);
1076
+ setActionsMenu(null);
1077
+ onUpdate(actionsMenuItem);
900
1078
  }}
901
1079
  >
902
- <DownloadIcon />
903
- </button>
904
- ) : null}
905
- {showFilters ? (
906
- <button
907
- type="button"
908
- className="ghost side-rail-icon"
909
- title="Show filters"
910
- aria-label="Show filters"
911
- onClick={() => setSideOpen(true)}
912
- >
913
- <FilterIcon />
914
- {activeFilterCount > 0 ? <span className="filter-badge">{activeFilterCount}</span> : null}
1080
+ Update
915
1081
  </button>
916
1082
  ) : null}
917
- {showFilters ? (
1083
+ {onDelete ? (
918
1084
  <button
919
1085
  type="button"
920
- className="ghost side-rail-icon"
921
- title="Reset filters"
922
- aria-label="Reset filters"
923
- disabled={activeFilterCount === 0}
924
- onClick={clearFilters}
1086
+ role="menuitem"
1087
+ className="ghost danger"
1088
+ disabled={!actionsDeleteAllowed}
1089
+ onClick={() => {
1090
+ setActionsMenu(null);
1091
+ onDelete(actionsMenuItem);
1092
+ }}
925
1093
  >
926
- <ResetIcon />
1094
+ Delete
927
1095
  </button>
928
1096
  ) : null}
929
- </div>
930
- )}
931
- </aside>
932
- </div>
933
-
934
- {form}
1097
+ </div>,
1098
+ document.body,
1099
+ )
1100
+ : null}
935
1101
 
936
- {isClient && tooltip && !previewEnabled
1102
+ {isClient && tooltip && canShowTooltip
937
1103
  ? createPortal(
938
1104
  <div className="record-tooltip" style={{ left: tooltip.x, top: tooltip.y }} role="tooltip">
939
1105
  <p className="record-tooltip-title">{title}</p>