@mshuja288/aksuicore 0.4.2 → 0.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mshuja288/aksuicore",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "description": "Shared UI components and templates for Ikssam applications",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -102,6 +102,48 @@ function defaultVisibleKeys<T>(columns: TableColumn<T>[]) {
102
102
  return columns.filter((column) => column.defaultVisible !== false).map((column) => column.key);
103
103
  }
104
104
 
105
+ /** Shared across all RecordWorkspace tables (preview + side rail). */
106
+ const WORKSPACE_CHROME_KEY = "aksui.workspace.chrome";
107
+
108
+ type WorkspaceChrome = {
109
+ previewEnabled: boolean;
110
+ sideOpen: boolean;
111
+ };
112
+
113
+ function readWorkspaceChrome(): WorkspaceChrome {
114
+ if (typeof window === "undefined") {
115
+ return { previewEnabled: false, sideOpen: false };
116
+ }
117
+ try {
118
+ const raw = window.localStorage.getItem(WORKSPACE_CHROME_KEY);
119
+ if (!raw) {
120
+ return { previewEnabled: false, sideOpen: false };
121
+ }
122
+ const parsed = JSON.parse(raw) as Partial<WorkspaceChrome>;
123
+ return {
124
+ previewEnabled: Boolean(parsed.previewEnabled),
125
+ sideOpen: Boolean(parsed.sideOpen),
126
+ };
127
+ } catch {
128
+ return { previewEnabled: false, sideOpen: false };
129
+ }
130
+ }
131
+
132
+ function writeWorkspaceChrome(chrome: WorkspaceChrome) {
133
+ if (typeof window === "undefined") {
134
+ return;
135
+ }
136
+ try {
137
+ window.localStorage.setItem(WORKSPACE_CHROME_KEY, JSON.stringify(chrome));
138
+ } catch {
139
+ /* ignore quota / private mode */
140
+ }
141
+ }
142
+
143
+ function filtersPrefsKey(columnPrefsKey: string | undefined) {
144
+ return columnPrefsKey ? `${columnPrefsKey}.filters` : undefined;
145
+ }
146
+
105
147
  function readStoredColumnKeys(prefsKey: string | undefined, fallback: string[], allowed: Set<string>) {
106
148
  if (!prefsKey || typeof window === "undefined") {
107
149
  return fallback;
@@ -133,6 +175,48 @@ function writeStoredColumnKeys(prefsKey: string | undefined, keys: string[]) {
133
175
  }
134
176
  }
135
177
 
178
+ function readStoredFilters(prefsKey: string | undefined, allowed: Set<string>): Record<string, string> {
179
+ if (!prefsKey || typeof window === "undefined") {
180
+ return {};
181
+ }
182
+ try {
183
+ const raw = window.localStorage.getItem(prefsKey);
184
+ if (!raw) {
185
+ return {};
186
+ }
187
+ const parsed = JSON.parse(raw) as unknown;
188
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
189
+ return {};
190
+ }
191
+ const next: Record<string, string> = {};
192
+ for (const [key, value] of Object.entries(parsed as Record<string, unknown>)) {
193
+ if (allowed.has(key) && typeof value === "string" && value.trim()) {
194
+ next[key] = value;
195
+ }
196
+ }
197
+ return next;
198
+ } catch {
199
+ return {};
200
+ }
201
+ }
202
+
203
+ function writeStoredFilters(prefsKey: string | undefined, filters: Record<string, string>) {
204
+ if (!prefsKey || typeof window === "undefined") {
205
+ return;
206
+ }
207
+ try {
208
+ const trimmed: Record<string, string> = {};
209
+ for (const [key, value] of Object.entries(filters)) {
210
+ if (value.trim()) {
211
+ trimmed[key] = value;
212
+ }
213
+ }
214
+ window.localStorage.setItem(prefsKey, JSON.stringify(trimmed));
215
+ } catch {
216
+ /* ignore quota / private mode */
217
+ }
218
+ }
219
+
136
220
  function compareColumnValues<T>(column: TableColumn<T>, left: T, right: T) {
137
221
  const leftRaw = column.display ? column.display(left) : column.value(left);
138
222
  const rightRaw = column.display ? column.display(right) : column.value(right);
@@ -330,7 +414,11 @@ export function RecordWorkspace<T>({
330
414
  importBusy?: boolean;
331
415
  /** Column checklist in the side rail. Default true. */
332
416
  showColumnPicker?: boolean;
333
- /** Persist visible column keys in localStorage under this key. */
417
+ /**
418
+ * Persist visible column keys in localStorage under this key.
419
+ * Also persists filter values at `${columnPrefsKey}.filters`.
420
+ * Preview + side-rail open/collapsed use a shared key across all tables (`aksui.workspace.chrome`).
421
+ */
334
422
  columnPrefsKey?: string;
335
423
  /** Controlled visible column keys. */
336
424
  visibleColumnKeys?: string[];
@@ -350,9 +438,12 @@ export function RecordWorkspace<T>({
350
438
  const [internalVisibleKeys, setInternalVisibleKeys] = useState<string[]>(() =>
351
439
  readStoredColumnKeys(columnPrefsKey, fallbackKeys, allowedKeys),
352
440
  );
353
- const [filters, setFilters] = useState<Record<string, string>>({});
441
+ const filterStorageKey = filtersPrefsKey(columnPrefsKey);
442
+ const [filters, setFilters] = useState<Record<string, string>>(() =>
443
+ readStoredFilters(filterStorageKey, allowedKeys),
444
+ );
354
445
  const [sort, setSort] = useState<SortState | null>(null);
355
- const [sideOpen, setSideOpen] = useState(false);
446
+ const [sideOpen, setSideOpen] = useState(() => readWorkspaceChrome().sideOpen);
356
447
  const [toolsDrawerOpen, setToolsDrawerOpen] = useState(false);
357
448
  const [downloadOpen, setDownloadOpen] = useState(false);
358
449
  const [bulkOpen, setBulkOpen] = useState(false);
@@ -361,7 +452,7 @@ export function RecordWorkspace<T>({
361
452
  const [bulkReport, setBulkReport] = useState<BulkImportResult | null>(null);
362
453
  const bulkFileInputRef = useRef<HTMLInputElement | null>(null);
363
454
  const [columnsOpen, setColumnsOpen] = useState(false);
364
- const [previewEnabled, setPreviewEnabled] = useState(false);
455
+ const [previewEnabled, setPreviewEnabled] = useState(() => readWorkspaceChrome().previewEnabled);
365
456
  const [previewKey, setPreviewKey] = useState<string | number | null>(null);
366
457
  const [tooltip, setTooltip] = useState<TooltipState | null>(null);
367
458
  const [pageSize, setPageSize] = useState("15");
@@ -383,6 +474,10 @@ export function RecordWorkspace<T>({
383
474
  setInternalVisibleKeys(readStoredColumnKeys(columnPrefsKey, fallbackKeys, allowedKeys));
384
475
  }, [allowedKeys, columnPrefsKey, fallbackKeys, isControlled]);
385
476
 
477
+ useEffect(() => {
478
+ setFilters(readStoredFilters(filtersPrefsKey(columnPrefsKey), allowedKeys));
479
+ }, [allowedKeys, columnPrefsKey]);
480
+
386
481
  const resolvedVisibleKeys = useMemo(() => {
387
482
  const source = isControlled ? visibleColumnKeys! : internalVisibleKeys;
388
483
  const filtered = source.filter((key) => allowedKeys.has(key));
@@ -565,15 +660,25 @@ export function RecordWorkspace<T>({
565
660
  const previewTitle = previewItem && visibleColumns[0] ? cellText(visibleColumns[0], previewItem) : null;
566
661
 
567
662
  function changeFilter(key: string, value: string) {
568
- setFilters((current) => ({ ...current, [key]: value }));
663
+ setFilters((current) => {
664
+ const next = { ...current, [key]: value };
665
+ writeStoredFilters(filterStorageKey, next);
666
+ return next;
667
+ });
569
668
  setPage(1);
570
669
  }
571
670
 
572
671
  function clearFilters() {
573
672
  setFilters({});
673
+ writeStoredFilters(filterStorageKey, {});
574
674
  setPage(1);
575
675
  }
576
676
 
677
+ function setSideOpenValue(open: boolean) {
678
+ setSideOpen(open);
679
+ writeWorkspaceChrome({ previewEnabled, sideOpen: open });
680
+ }
681
+
577
682
  function toggleSort(column: TableColumn<T>) {
578
683
  if (column.sortable === false) {
579
684
  return;
@@ -592,6 +697,7 @@ export function RecordWorkspace<T>({
592
697
 
593
698
  function setPreviewEnabledValue(enabled: boolean) {
594
699
  setPreviewEnabled(enabled);
700
+ writeWorkspaceChrome({ previewEnabled: enabled, sideOpen });
595
701
  setTooltip(null);
596
702
  if (!enabled) {
597
703
  setPreviewKey(null);
@@ -663,7 +769,7 @@ export function RecordWorkspace<T>({
663
769
  title="Columns"
664
770
  aria-label="Columns"
665
771
  onClick={() => {
666
- setSideOpen(true);
772
+ setSideOpenValue(true);
667
773
  setColumnsOpen(true);
668
774
  }}
669
775
  >
@@ -679,7 +785,7 @@ export function RecordWorkspace<T>({
679
785
  aria-label="Download"
680
786
  disabled={downloadBusy}
681
787
  onClick={() => {
682
- setSideOpen(true);
788
+ setSideOpenValue(true);
683
789
  setDownloadOpen(true);
684
790
  }}
685
791
  >
@@ -694,7 +800,7 @@ export function RecordWorkspace<T>({
694
800
  aria-label="Bulk entry"
695
801
  disabled={importBusy}
696
802
  onClick={() => {
697
- setSideOpen(true);
803
+ setSideOpenValue(true);
698
804
  setBulkOpen(true);
699
805
  }}
700
806
  >
@@ -707,7 +813,7 @@ export function RecordWorkspace<T>({
707
813
  className="ghost side-rail-icon"
708
814
  title="Show filters"
709
815
  aria-label="Show filters"
710
- onClick={() => setSideOpen(true)}
816
+ onClick={() => setSideOpenValue(true)}
711
817
  >
712
818
  <FilterIcon />
713
819
  {activeFilterCount > 0 ? <span className="filter-badge">{activeFilterCount}</span> : null}
@@ -1158,7 +1264,7 @@ export function RecordWorkspace<T>({
1158
1264
  className="ghost side-rail-toggle"
1159
1265
  aria-expanded={sideOpen}
1160
1266
  title={sideOpen ? "Collapse" : "Expand"}
1161
- onClick={() => setSideOpen((open) => !open)}
1267
+ onClick={() => setSideOpenValue(!sideOpen)}
1162
1268
  >
1163
1269
  <ChevronIcon open={sideOpen} />
1164
1270
  {sideOpen ? <span>Collapse</span> : null}
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
- import type { ReactNode } from "react";
3
+ import type { MouseEvent, ReactNode } from "react";
4
+ import { useEffect, useRef } from "react";
4
5
  import { Button } from "./Button";
5
6
 
6
7
  export function Dialog({
@@ -16,18 +17,42 @@ export function Dialog({
16
17
  children: ReactNode;
17
18
  wide?: boolean;
18
19
  }) {
20
+ const allowBackdropClose = useRef(false);
21
+
22
+ useEffect(() => {
23
+ if (!open) {
24
+ allowBackdropClose.current = false;
25
+ return;
26
+ }
27
+
28
+ // Ignore the opening gesture so View/Update clicks cannot close the dialog immediately.
29
+ allowBackdropClose.current = false;
30
+ const timer = window.setTimeout(() => {
31
+ allowBackdropClose.current = true;
32
+ }, 0);
33
+
34
+ return () => window.clearTimeout(timer);
35
+ }, [open]);
36
+
19
37
  if (!open) {
20
38
  return null;
21
39
  }
22
40
 
41
+ function onBackdropMouseDown(event: MouseEvent<HTMLDivElement>) {
42
+ if (event.target !== event.currentTarget || !allowBackdropClose.current) {
43
+ return;
44
+ }
45
+ onClose();
46
+ }
47
+
23
48
  return (
24
- <div className="dialog-backdrop" onClick={onClose}>
49
+ <div className="dialog-backdrop" onMouseDown={onBackdropMouseDown}>
25
50
  <div
26
51
  className={wide ? "dialog form-dialog" : "dialog"}
27
52
  role="dialog"
28
53
  aria-modal="true"
29
54
  aria-label={title}
30
- onClick={(event) => event.stopPropagation()}
55
+ onMouseDown={(event) => event.stopPropagation()}
31
56
  >
32
57
  <div className="dialog-head">
33
58
  <h2>{title}</h2>