@teambit/workspace 1.0.1079 → 1.0.1081

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 (77) hide show
  1. package/dist/{preview-1785260578779.js → preview-1785278532531.js} +2 -2
  2. package/dist/ui/workspace/use-workspace.js +179 -27
  3. package/dist/ui/workspace/use-workspace.js.map +1 -1
  4. package/dist/ui/workspace/workspace-overview/index.d.ts +1 -1
  5. package/dist/ui/workspace/workspace-overview/index.js +5 -5
  6. package/dist/ui/workspace/workspace-overview/index.js.map +1 -1
  7. package/dist/ui/workspace/workspace-overview/workspace-overview.js +13 -143
  8. package/dist/ui/workspace/workspace-overview/workspace-overview.js.map +1 -1
  9. package/dist/ui/workspace/workspace-skeleton.d.ts +8 -0
  10. package/dist/ui/workspace/workspace-skeleton.js +73 -0
  11. package/dist/ui/workspace/workspace-skeleton.js.map +1 -0
  12. package/dist/ui/workspace/workspace-skeleton.module.scss +102 -0
  13. package/dist/ui/workspace/workspace.js +23 -7
  14. package/dist/ui/workspace/workspace.js.map +1 -1
  15. package/package.json +65 -74
  16. package/ui/workspace/use-workspace.ts +185 -27
  17. package/ui/workspace/workspace-overview/index.ts +1 -1
  18. package/ui/workspace/workspace-overview/workspace-overview.tsx +9 -102
  19. package/ui/workspace/workspace-skeleton.module.scss +102 -0
  20. package/ui/workspace/workspace-skeleton.tsx +35 -0
  21. package/ui/workspace/workspace.tsx +16 -5
  22. package/dist/ui/workspace/workspace-overview/card-overlays.d.ts +0 -5
  23. package/dist/ui/workspace/workspace-overview/card-overlays.js +0 -133
  24. package/dist/ui/workspace/workspace-overview/card-overlays.js.map +0 -1
  25. package/dist/ui/workspace/workspace-overview/card-overlays.module.scss +0 -72
  26. package/dist/ui/workspace/workspace-overview/filter-utils.d.ts +0 -10
  27. package/dist/ui/workspace/workspace-overview/filter-utils.js +0 -40
  28. package/dist/ui/workspace/workspace-overview/filter-utils.js.map +0 -1
  29. package/dist/ui/workspace/workspace-overview/hope-component-card.d.ts +0 -15
  30. package/dist/ui/workspace/workspace-overview/hope-component-card.js +0 -186
  31. package/dist/ui/workspace/workspace-overview/hope-component-card.js.map +0 -1
  32. package/dist/ui/workspace/workspace-overview/hope-component-card.module.scss +0 -169
  33. package/dist/ui/workspace/workspace-overview/link-plugin.d.ts +0 -3
  34. package/dist/ui/workspace/workspace-overview/link-plugin.js +0 -14
  35. package/dist/ui/workspace/workspace-overview/link-plugin.js.map +0 -1
  36. package/dist/ui/workspace/workspace-overview/namespace-header.d.ts +0 -9
  37. package/dist/ui/workspace/workspace-overview/namespace-header.js +0 -107
  38. package/dist/ui/workspace/workspace-overview/namespace-header.js.map +0 -1
  39. package/dist/ui/workspace/workspace-overview/namespace-header.module.scss +0 -88
  40. package/dist/ui/workspace/workspace-overview/namespace-sort.d.ts +0 -8
  41. package/dist/ui/workspace/workspace-overview/namespace-sort.js +0 -47
  42. package/dist/ui/workspace/workspace-overview/namespace-sort.js.map +0 -1
  43. package/dist/ui/workspace/workspace-overview/scope-sort.d.ts +0 -1
  44. package/dist/ui/workspace/workspace-overview/scope-sort.js +0 -14
  45. package/dist/ui/workspace/workspace-overview/scope-sort.js.map +0 -1
  46. package/dist/ui/workspace/workspace-overview/use-query-param-with-default.d.ts +0 -6
  47. package/dist/ui/workspace/workspace-overview/use-query-param-with-default.js +0 -102
  48. package/dist/ui/workspace/workspace-overview/use-query-param-with-default.js.map +0 -1
  49. package/dist/ui/workspace/workspace-overview/use-workspace-aggregation.d.ts +0 -3
  50. package/dist/ui/workspace/workspace-overview/use-workspace-aggregation.js +0 -88
  51. package/dist/ui/workspace/workspace-overview/use-workspace-aggregation.js.map +0 -1
  52. package/dist/ui/workspace/workspace-overview/workspace-filter-panel.d.ts +0 -13
  53. package/dist/ui/workspace/workspace-overview/workspace-filter-panel.js +0 -110
  54. package/dist/ui/workspace/workspace-overview/workspace-filter-panel.js.map +0 -1
  55. package/dist/ui/workspace/workspace-overview/workspace-overview.module.scss +0 -91
  56. package/dist/ui/workspace/workspace-overview/workspace-overview.sort.d.ts +0 -1
  57. package/dist/ui/workspace/workspace-overview/workspace-overview.sort.js +0 -30
  58. package/dist/ui/workspace/workspace-overview/workspace-overview.sort.js.map +0 -1
  59. package/dist/ui/workspace/workspace-overview/workspace-overview.types.d.ts +0 -28
  60. package/dist/ui/workspace/workspace-overview/workspace-overview.types.js +0 -3
  61. package/dist/ui/workspace/workspace-overview/workspace-overview.types.js.map +0 -1
  62. package/ui/workspace/workspace-overview/card-overlays.module.scss +0 -72
  63. package/ui/workspace/workspace-overview/card-overlays.tsx +0 -66
  64. package/ui/workspace/workspace-overview/filter-utils.ts +0 -42
  65. package/ui/workspace/workspace-overview/hope-component-card.module.scss +0 -169
  66. package/ui/workspace/workspace-overview/hope-component-card.tsx +0 -152
  67. package/ui/workspace/workspace-overview/link-plugin.ts +0 -5
  68. package/ui/workspace/workspace-overview/namespace-header.module.scss +0 -88
  69. package/ui/workspace/workspace-overview/namespace-header.tsx +0 -72
  70. package/ui/workspace/workspace-overview/namespace-sort.ts +0 -44
  71. package/ui/workspace/workspace-overview/scope-sort.ts +0 -9
  72. package/ui/workspace/workspace-overview/use-query-param-with-default.ts +0 -115
  73. package/ui/workspace/workspace-overview/use-workspace-aggregation.ts +0 -79
  74. package/ui/workspace/workspace-overview/workspace-filter-panel.tsx +0 -109
  75. package/ui/workspace/workspace-overview/workspace-overview.module.scss +0 -91
  76. package/ui/workspace/workspace-overview/workspace-overview.sort.ts +0 -29
  77. package/ui/workspace/workspace-overview/workspace-overview.types.ts +0 -30
@@ -1,169 +0,0 @@
1
- .card {
2
- position: relative;
3
- border-radius: 10px;
4
- overflow: hidden;
5
- background: var(--surface-color);
6
- border: 1px solid var(--border-medium-color);
7
- cursor: pointer;
8
- transition:
9
- box-shadow 0.15s ease,
10
- border-color 0.15s ease;
11
-
12
- &:hover {
13
- box-shadow: 0 4px 14px rgba(20, 0, 104, 0.06);
14
- }
15
- }
16
-
17
- .cardBuilding {
18
- composes: card;
19
-
20
- &:hover {
21
- box-shadow: none;
22
- }
23
- }
24
-
25
- .linkWrapper {
26
- text-decoration: none;
27
- color: inherit;
28
- display: block;
29
- }
30
-
31
- /* ---- Preview ---- */
32
-
33
- .preview {
34
- height: 180px;
35
- position: relative;
36
- overflow: hidden;
37
- background: var(--surface-color);
38
- border-bottom: 1px solid var(--border-medium-color);
39
- }
40
-
41
- .previewQueued {
42
- composes: preview;
43
- background: var(--surface01-color);
44
- }
45
-
46
- .previewInner {
47
- position: absolute;
48
- inset: 0;
49
- overflow: hidden;
50
- }
51
-
52
- /* ---- Env badge ---- */
53
-
54
- .envBadge {
55
- position: absolute;
56
- bottom: 8px;
57
- right: 8px;
58
- background: var(--surface-color);
59
- padding: 4px;
60
- border-radius: 8px;
61
- box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
62
- display: flex;
63
- align-items: center;
64
- justify-content: center;
65
- width: 24px;
66
- height: 24px;
67
- z-index: 3;
68
- }
69
-
70
- .envIcon {
71
- height: 14px;
72
- display: block;
73
- }
74
-
75
- /* ---- Status corner ---- */
76
-
77
- .statusCorner {
78
- position: absolute;
79
- top: 10px;
80
- right: 10px;
81
- z-index: 2;
82
- }
83
-
84
- /* ---- Footer ---- */
85
-
86
- .footer {
87
- padding: 8px 12px;
88
- display: flex;
89
- align-items: center;
90
- gap: 8px;
91
- }
92
-
93
- .name {
94
- flex: 1;
95
- min-width: 0;
96
- font-size: 12.5px;
97
- font-weight: 500;
98
- font-family: 'JetBrains Mono', ui-monospace, Menlo, monospace;
99
- color: var(--on-background-color);
100
- overflow: hidden;
101
- text-overflow: ellipsis;
102
- white-space: nowrap;
103
- letter-spacing: -0.01em;
104
- }
105
-
106
- .nameQueued {
107
- composes: name;
108
- color: var(--on-background-medium-color);
109
- }
110
-
111
- .hash {
112
- font-size: 10.5px;
113
- padding: 1px 6px;
114
- border-radius: 4px;
115
- background: var(--surface01-color);
116
- font-family: 'JetBrains Mono', ui-monospace, Menlo, monospace;
117
- color: var(--on-background-low-color);
118
- flex-shrink: 0;
119
- }
120
-
121
- .scopeBadge {
122
- width: 20px;
123
- height: 20px;
124
- border-radius: 5px;
125
- display: flex;
126
- align-items: center;
127
- justify-content: center;
128
- flex-shrink: 0;
129
- overflow: hidden;
130
- }
131
-
132
- .scopeBadgeIcon {
133
- width: 12px;
134
- height: 12px;
135
- object-fit: contain;
136
- filter: brightness(0) invert(1);
137
- }
138
-
139
- .scopeBadgeInitial {
140
- font-family: 'JetBrains Mono', ui-monospace, Menlo, monospace;
141
- font-size: 10px;
142
- font-weight: 700;
143
- color: white;
144
- line-height: 1;
145
- }
146
-
147
- .buildingLabel {
148
- font-size: 10px;
149
- font-weight: 600;
150
- letter-spacing: 0.06em;
151
- text-transform: uppercase;
152
- flex-shrink: 0;
153
- }
154
-
155
- /* ---- Load preview button ---- */
156
-
157
- .loadPreview {
158
- position: absolute;
159
- right: 4px !important;
160
- left: unset !important;
161
- z-index: 4;
162
- background-color: var(--surface01-color) !important;
163
- color: var(--on-surface-color) !important;
164
- border: 1px solid var(--border-medium-color);
165
-
166
- &:hover {
167
- background-color: var(--surface-hover-color) !important;
168
- }
169
- }
@@ -1,152 +0,0 @@
1
- import React, { useEffect, useRef, useState } from 'react';
2
- import { Link } from '@teambit/base-react.navigation.link';
3
- import { PreviewPlaceholder } from '@teambit/preview.ui.preview-placeholder';
4
- import { Tooltip } from '@teambit/design.ui.tooltip';
5
- import { LoadPreview } from '@teambit/workspace.ui.load-preview';
6
- import { ComponentID } from '@teambit/component-id';
7
- import type { ComponentModel } from '@teambit/component';
8
- import type { ComponentDescriptor } from '@teambit/component-descriptor';
9
- import type { ScopeID } from '@teambit/scopes.scope-id';
10
- import { getComponentStatus } from './filter-utils';
11
- import { ChangedPill, BuildSpinner, BuildingPreview } from './card-overlays';
12
- import styles from './hope-component-card.module.scss';
13
-
14
- export type HopeComponentCardProps = {
15
- component: ComponentModel;
16
- componentDescriptor: ComponentDescriptor;
17
- scope?: { id: ScopeID; icon?: string; backgroundIconColor?: string };
18
- showPreview?: boolean;
19
- };
20
-
21
- export function HopeComponentCard({
22
- component,
23
- componentDescriptor,
24
- scope,
25
- showPreview: showPreviewProp,
26
- }: HopeComponentCardProps) {
27
- const [shouldShowPreview, setShouldShowPreview] = useState(Boolean(showPreviewProp));
28
- const prevServerUrlRef = useRef(component.server?.url);
29
-
30
- useEffect(() => {
31
- if (prevServerUrlRef.current !== component.server?.url && shouldShowPreview) {
32
- setShouldShowPreview(false);
33
- setTimeout(() => setShouldShowPreview(true), 50);
34
- }
35
- prevServerUrlRef.current = component.server?.url;
36
- }, [component.server?.url]);
37
-
38
- useEffect(() => {
39
- setShouldShowPreview(Boolean(showPreviewProp));
40
- }, [showPreviewProp]);
41
-
42
- const item = { component } as any;
43
- const status = getComponentStatus(item);
44
- const accent = 'var(--bit-accent-color, #6c5ce7)';
45
-
46
- const isBuilding = status === 'building';
47
- const isQueued = status === 'queued';
48
- const isChanged = status === 'changed';
49
-
50
- const href = `${component.id.fullName}?scope=${component.id.scope}`;
51
-
52
- const loadPreviewVisible = component.compositions.length > 0 && !isBuilding && !shouldShowPreview;
53
-
54
- const showPreviewClick = (e: React.MouseEvent) => {
55
- e.stopPropagation();
56
- e.preventDefault();
57
- setShouldShowPreview(true);
58
- };
59
-
60
- const envAspect = componentDescriptor.get<any>('teambit.envs/envs');
61
- const env = envAspect?.data || envAspect;
62
- const envComponentId = env?.id ? ComponentID.fromString(env.id) : undefined;
63
-
64
- const cardClass = isBuilding ? styles.cardBuilding : styles.card;
65
- const buildingBorderStyle = isBuilding
66
- ? {
67
- borderColor: accent,
68
- boxShadow: `0 0 0 3px color-mix(in srgb, var(--bit-accent-color, #6c5ce7) 10%, transparent)`,
69
- }
70
- : undefined;
71
-
72
- const nameLabel = component.id.namespace ? `${component.id.namespace}/${component.id.name}` : component.id.name;
73
-
74
- const shortHash = component.id.version?.slice(0, 7);
75
-
76
- const scopeInitial = component.id.scope?.split('.').pop()?.charAt(0).toUpperCase();
77
-
78
- return (
79
- <div className={cardClass} style={buildingBorderStyle}>
80
- {loadPreviewVisible && <LoadPreview className={styles.loadPreview} onClick={showPreviewClick} />}
81
-
82
- <Link href={href} className={styles.linkWrapper}>
83
- <div className={styles.preview}>
84
- <div className={styles.previewInner}>
85
- <CardPreview
86
- component={component}
87
- componentDescriptor={componentDescriptor}
88
- status={status}
89
- shouldShowPreview={shouldShowPreview}
90
- />
91
- </div>
92
-
93
- {!isQueued && env?.icon && (
94
- <div className={styles.envBadge}>
95
- <Tooltip delay={300} content={envComponentId?.name}>
96
- <img src={env.icon} className={styles.envIcon} alt="" />
97
- </Tooltip>
98
- </div>
99
- )}
100
-
101
- {(isChanged || isBuilding) && (
102
- <div className={styles.statusCorner}>
103
- {isChanged && <ChangedPill />}
104
- {isBuilding && <BuildSpinner />}
105
- </div>
106
- )}
107
- </div>
108
-
109
- <div className={styles.footer}>
110
- <Tooltip delay={300} content={component.id.scope}>
111
- <div className={styles.scopeBadge} style={{ background: scope?.backgroundIconColor || accent }}>
112
- {scope?.icon ? (
113
- <img src={scope.icon} className={styles.scopeBadgeIcon} alt="" />
114
- ) : (
115
- <span className={styles.scopeBadgeInitial}>{scopeInitial}</span>
116
- )}
117
- </div>
118
- </Tooltip>
119
- <span className={styles.name}>{nameLabel}</span>
120
- {!isBuilding && !isQueued && shortHash && <span className={styles.hash}>{shortHash}</span>}
121
- {isBuilding && (
122
- <span className={styles.buildingLabel} style={{ color: accent }}>
123
- BUILDING
124
- </span>
125
- )}
126
- </div>
127
- </Link>
128
- </div>
129
- );
130
- }
131
-
132
- function CardPreview({
133
- component,
134
- componentDescriptor,
135
- status,
136
- shouldShowPreview,
137
- }: {
138
- component: ComponentModel;
139
- componentDescriptor: ComponentDescriptor;
140
- status: string;
141
- shouldShowPreview: boolean;
142
- }) {
143
- if (status === 'building') return <BuildingPreview />;
144
-
145
- return (
146
- <PreviewPlaceholder
147
- component={component}
148
- componentDescriptor={componentDescriptor}
149
- shouldShowPreview={shouldShowPreview}
150
- />
151
- );
152
- }
@@ -1,5 +0,0 @@
1
- export class LinkPlugin {
2
- link(id) {
3
- return `${id.fullName}?scope=${id.scope}`;
4
- }
5
- }
@@ -1,88 +0,0 @@
1
- .header {
2
- display: flex;
3
- align-items: center;
4
- gap: 10px;
5
- }
6
-
7
- .dot {
8
- width: 10px;
9
- height: 10px;
10
- border-radius: 3px;
11
- flex-shrink: 0;
12
- }
13
-
14
- .name {
15
- font-size: 20px;
16
- font-weight: 600;
17
- letter-spacing: 0.02em;
18
- color: var(--on-background-color);
19
- flex-shrink: 0;
20
- }
21
-
22
- .count {
23
- font-size: 12px;
24
- color: var(--on-background-low-color);
25
- flex-shrink: 0;
26
- }
27
-
28
- .buildingPill {
29
- display: inline-flex;
30
- align-items: center;
31
- gap: 5px;
32
- font-size: 10.5px;
33
- font-weight: 500;
34
- padding: 1px 8px;
35
- border-radius: 999px;
36
- margin-left: 4px;
37
- flex-shrink: 0;
38
- }
39
-
40
- .buildingDot {
41
- width: 5px;
42
- height: 5px;
43
- border-radius: 50%;
44
- animation: pulse 1.4s ease-in-out infinite;
45
- }
46
-
47
- .scopeIconBadge {
48
- width: 24px;
49
- height: 24px;
50
- border-radius: 6px;
51
- display: flex;
52
- align-items: center;
53
- justify-content: center;
54
- flex-shrink: 0;
55
- overflow: hidden;
56
- }
57
-
58
- .scopeIconImg {
59
- width: 14px;
60
- height: 14px;
61
- object-fit: contain;
62
- filter: brightness(0) invert(1);
63
- }
64
-
65
- .scopeInitial {
66
- font-family: 'JetBrains Mono', ui-monospace, Menlo, monospace;
67
- font-size: 11px;
68
- font-weight: 700;
69
- color: white;
70
- line-height: 1;
71
- }
72
-
73
- .divider {
74
- flex: 1;
75
- height: 1px;
76
- background: var(--border-medium-color);
77
- margin-left: 4px;
78
- }
79
-
80
- @keyframes pulse {
81
- 0%,
82
- 100% {
83
- opacity: 1;
84
- }
85
- 50% {
86
- opacity: 0.55;
87
- }
88
- }
@@ -1,72 +0,0 @@
1
- import React from 'react';
2
- import { getComponentStatus } from './filter-utils';
3
- import type { WorkspaceItem } from './workspace-overview.types';
4
- import styles from './namespace-header.module.scss';
5
-
6
- export interface NamespaceHeaderProps {
7
- namespace: string;
8
- items: WorkspaceItem[];
9
- scopeIcon?: string;
10
- scopeIconColor?: string;
11
- }
12
-
13
- export function NamespaceHeader({ namespace, items, scopeIcon, scopeIconColor }: NamespaceHeaderProps) {
14
- const accent = 'var(--bit-accent-color, #6c5ce7)';
15
- const tint = 'color-mix(in srgb, var(--bit-accent-color, #6c5ce7) 12%, transparent)';
16
-
17
- let buildingCount = 0;
18
- let readyCount = 0;
19
- for (const item of items) {
20
- const s = getComponentStatus(item);
21
- if (s === 'building') buildingCount++;
22
- if (s === 'built' || s === 'changed') readyCount++;
23
- }
24
-
25
- return (
26
- <header className={styles.header}>
27
- <HeaderIcon scopeIcon={scopeIcon} scopeIconColor={scopeIconColor} namespace={namespace} accent={accent} />
28
- <span className={styles.name}>{namespace}</span>
29
- <span className={styles.count}>
30
- {readyCount}/{items.length}
31
- </span>
32
- {buildingCount > 0 && (
33
- <span className={styles.buildingPill} style={{ color: accent, background: tint }}>
34
- <span className={styles.buildingDot} style={{ background: accent }} />
35
- building
36
- </span>
37
- )}
38
- <div className={styles.divider} />
39
- </header>
40
- );
41
- }
42
-
43
- function HeaderIcon({
44
- scopeIcon,
45
- scopeIconColor,
46
- namespace,
47
- accent,
48
- }: {
49
- scopeIcon?: string;
50
- scopeIconColor?: string;
51
- namespace: string;
52
- accent: string;
53
- }) {
54
- if (scopeIcon) {
55
- return (
56
- <div className={styles.scopeIconBadge} style={{ background: scopeIconColor || accent }}>
57
- <img src={scopeIcon} className={styles.scopeIconImg} alt="" />
58
- </div>
59
- );
60
- }
61
-
62
- if (scopeIconColor) {
63
- const initial = namespace.split(/[./]/).pop()?.charAt(0).toUpperCase() || '?';
64
- return (
65
- <div className={styles.scopeIconBadge} style={{ background: scopeIconColor }}>
66
- <span className={styles.scopeInitial}>{initial}</span>
67
- </div>
68
- );
69
- }
70
-
71
- return <span className={styles.dot} style={{ background: accent }} />;
72
- }
@@ -1,44 +0,0 @@
1
- import type { WorkspaceItem } from './workspace-overview.types';
2
-
3
- export const PRIORITY_HIGH = ['ui', 'pages'] as string[];
4
- export const PRIORITY_MED = ['design'] as string[];
5
- export const PRIORITY_LOW = ['entities', 'provider', 'hooks', 'icons'] as string[];
6
-
7
- export function getRootNamespace(ns: string): string {
8
- if (!ns) return '';
9
- return ns.split('/')[0]!;
10
- }
11
-
12
- export function namespacePriority(ns: string): number {
13
- const root = getRootNamespace(ns);
14
-
15
- // internal namespaces (starting with _) go last
16
- if (root.startsWith('_')) return 4;
17
-
18
- if (PRIORITY_HIGH.includes(root)) return 0;
19
- if (PRIORITY_MED.includes(root)) return 1;
20
- if (PRIORITY_LOW.includes(root)) return 3;
21
- return 2;
22
- }
23
-
24
- export function sortNamespacesAdvanced(list: string[]): string[] {
25
- return [...list].sort((a, b) => {
26
- const pa = namespacePriority(a);
27
- const pb = namespacePriority(b);
28
- if (pa !== pb) return pa - pb;
29
- return a.localeCompare(b);
30
- });
31
- }
32
-
33
- export function sortItemsByNamespace(items: WorkspaceItem[]): WorkspaceItem[] {
34
- return [...items].sort((a, b) => {
35
- const na = a.component.id.namespace || '/';
36
- const nb = b.component.id.namespace || '/';
37
-
38
- const pa = namespacePriority(na);
39
- const pb = namespacePriority(nb);
40
- if (pa !== pb) return pa - pb;
41
-
42
- return a.component.id.name.localeCompare(b.component.id.name);
43
- });
44
- }
@@ -1,9 +0,0 @@
1
- export function sortScopes(scopes: string[]) {
2
- const designPrefix = 'design';
3
-
4
- const nonDesign = scopes.filter((s) => !s.includes(designPrefix)).sort((a, b) => a.localeCompare(b));
5
-
6
- const design = scopes.filter((s) => s.includes(designPrefix)).sort((a, b) => a.localeCompare(b));
7
-
8
- return [...nonDesign, ...design];
9
- }
@@ -1,115 +0,0 @@
1
- import { useCallback, useMemo } from 'react';
2
- import { useSearchParams } from 'react-router-dom';
3
-
4
- const STORAGE_KEY_PREFIX = 'workspace-overview:';
5
-
6
- export interface QueryParamOptions {
7
- /** persist value to localStorage across sessions. Default: true */
8
- persist?: boolean;
9
- }
10
-
11
- export function useQueryParamWithDefault<T extends string>(
12
- paramName: string,
13
- fallback: T,
14
- options: QueryParamOptions = {}
15
- ): [T, (value: T | null) => void] {
16
- const { persist = true } = options;
17
- const [searchParams, setSearchParams] = useSearchParams();
18
- const storageKey = STORAGE_KEY_PREFIX + paramName;
19
-
20
- const value = useMemo((): T => {
21
- const urlValue = searchParams.get(paramName);
22
- if (urlValue) return urlValue as T;
23
-
24
- if (persist) {
25
- const stored = safeGetItem(storageKey);
26
- if (stored) return stored as T;
27
- }
28
-
29
- return fallback;
30
- }, [searchParams, paramName, storageKey, fallback, persist]);
31
-
32
- const setValue = useCallback(
33
- (newValue: T | null) => {
34
- const newParams = new URLSearchParams(searchParams);
35
-
36
- if (newValue === null) {
37
- newParams.delete(paramName);
38
- if (persist) safeRemoveItem(storageKey);
39
- } else {
40
- newParams.set(paramName, newValue);
41
- if (persist) safeSetItem(storageKey, newValue);
42
- }
43
-
44
- setSearchParams(newParams);
45
- },
46
- [searchParams, setSearchParams, paramName, storageKey, persist]
47
- );
48
-
49
- return [value, setValue];
50
- }
51
-
52
- export function useListParamWithDefault(
53
- paramName: string,
54
- options: QueryParamOptions = {}
55
- ): [string[], (values: string[]) => void] {
56
- const { persist = false } = options;
57
- const [searchParams, setSearchParams] = useSearchParams();
58
- const storageKey = STORAGE_KEY_PREFIX + paramName;
59
-
60
- const values = useMemo((): string[] => {
61
- const urlValue = searchParams.get(paramName);
62
- if (urlValue) return urlValue.split(',').filter(Boolean);
63
-
64
- if (persist) {
65
- const stored = safeGetItem(storageKey);
66
- if (stored) return stored.split(',').filter(Boolean);
67
- }
68
-
69
- return [];
70
- }, [searchParams, paramName, storageKey, persist]);
71
-
72
- const setValues = useCallback(
73
- (newValues: string[]) => {
74
- const newParams = new URLSearchParams(searchParams);
75
- const joined = newValues.filter(Boolean).join(',');
76
-
77
- if (joined) {
78
- newParams.set(paramName, joined);
79
- if (persist) safeSetItem(storageKey, joined);
80
- } else {
81
- newParams.delete(paramName);
82
- if (persist) safeRemoveItem(storageKey);
83
- }
84
-
85
- setSearchParams(newParams);
86
- },
87
- [searchParams, setSearchParams, paramName, storageKey, persist]
88
- );
89
-
90
- return [values, setValues];
91
- }
92
-
93
- function safeGetItem(key: string): string | null {
94
- try {
95
- return localStorage.getItem(key);
96
- } catch {
97
- return null;
98
- }
99
- }
100
-
101
- function safeSetItem(key: string, value: string): void {
102
- try {
103
- localStorage.setItem(key, value);
104
- } catch {
105
- // ignore storage errors
106
- }
107
- }
108
-
109
- function safeRemoveItem(key: string): void {
110
- try {
111
- localStorage.removeItem(key);
112
- } catch {
113
- // ignore storage errors
114
- }
115
- }