@igstack/app-catalog-frontend-core 0.10.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/__tests__/integration/harness/given.d.ts +1 -0
- package/dist/esm/__tests__/integration/skewWarning.integration.test.d.ts +0 -0
- package/dist/esm/__tests__/integration/tools/AppDetailTools.d.ts +5 -0
- package/dist/esm/__tests__/integration/tools/CatalogTools.d.ts +15 -1
- package/dist/esm/__tests__/modules/appCatalog/utils/resolveHelpers.test.d.ts +1 -0
- package/dist/esm/modules/appCatalog/hooks/useSessionSyncedState.d.ts +40 -0
- package/dist/esm/modules/appCatalog/hooks/useSessionSyncedState.js +40 -0
- package/dist/esm/modules/appCatalog/hooks/useSessionSyncedState.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/components/AccessPrerequisiteChain.d.ts +17 -0
- package/dist/esm/modules/appCatalog/ui/components/AccessPrerequisiteChain.js +61 -0
- package/dist/esm/modules/appCatalog/ui/components/AccessPrerequisiteChain.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/components/AccessRequestSection.js +9 -2
- package/dist/esm/modules/appCatalog/ui/components/AccessRequestSection.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/OnboardingCard.js +1 -1
- package/dist/esm/modules/appCatalog/ui/components/OnboardingCard.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.d.ts +9 -1
- package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js +12 -3
- package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/context/AppCatalogFiltersContext.js +3 -2
- package/dist/esm/modules/appCatalog/ui/context/AppCatalogFiltersContext.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.d.ts +5 -0
- package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js +56 -10
- package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/launcher/LauncherDetailPanel.d.ts +12 -0
- package/dist/esm/modules/appCatalog/ui/launcher/LauncherDetailPanel.js +48 -0
- package/dist/esm/modules/appCatalog/ui/launcher/LauncherDetailPanel.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/launcher/LauncherHome.d.ts +19 -0
- package/dist/esm/modules/appCatalog/ui/launcher/LauncherHome.js +355 -0
- package/dist/esm/modules/appCatalog/ui/launcher/LauncherHome.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/launcher/ResourceIcon.d.ts +13 -0
- package/dist/esm/modules/appCatalog/ui/launcher/ResourceIcon.js +45 -0
- package/dist/esm/modules/appCatalog/ui/launcher/ResourceIcon.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/launcher/monogram.d.ts +4 -0
- package/dist/esm/modules/appCatalog/ui/launcher/monogram.js +41 -0
- package/dist/esm/modules/appCatalog/ui/launcher/monogram.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js +40 -6
- package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js.map +1 -1
- package/dist/esm/modules/appCatalog/utils/markdownToPlainText.d.ts +1 -1
- package/dist/esm/modules/appCatalog/utils/markdownToPlainText.js.map +1 -1
- package/dist/esm/modules/appCatalog/utils/resolveHelpers.d.ts +10 -0
- package/dist/esm/modules/appCatalog/utils/resolveHelpers.js +17 -0
- package/dist/esm/modules/appCatalog/utils/resolveHelpers.js.map +1 -1
- package/dist/esm/routeTree.gen.d.ts +1 -1
- package/dist/esm/routes/_layout/app.$slug.d.ts +1 -1
- package/dist/esm/routes/_layout/app._slug.js +4 -3
- package/dist/esm/routes/_layout/app._slug.js.map +1 -1
- package/dist/esm/routes/_layout/catalog.apps.index.js +2 -1
- package/dist/esm/routes/_layout/catalog.apps.index.js.map +1 -1
- package/dist/esm/routes/_layout/index.js +2 -1
- package/dist/esm/routes/_layout/index.js.map +1 -1
- package/dist/esm/routes/_layout/service-desks.js +2 -1
- package/dist/esm/routes/_layout/service-desks.js.map +1 -1
- package/dist/esm/ui/components/header/Header.js +1 -1
- package/dist/esm/ui/components/header/Header.js.map +1 -1
- package/dist/index.css +73 -66
- package/package.json +4 -4
- package/src/__tests__/integration/appCatalog.integration.test.ts +106 -0
- package/src/__tests__/integration/appDeepLink.integration.test.ts +50 -24
- package/src/__tests__/integration/crossRefLinkListView.integration.test.ts +8 -8
- package/src/__tests__/integration/harness/given.tsx +9 -1
- package/src/__tests__/integration/replacementLink.integration.test.ts +6 -2
- package/src/__tests__/integration/searchDeprecatedFallback.integration.test.ts +21 -16
- package/src/__tests__/integration/setup/testSetup.ts +1 -0
- package/src/__tests__/integration/skewWarning.integration.test.ts +56 -0
- package/src/__tests__/integration/tools/AppDetailTools.ts +13 -0
- package/src/__tests__/integration/tools/CatalogTools.ts +60 -11
- package/src/__tests__/modules/appCatalog/utils/markdownToPlainText.test.ts +5 -5
- package/src/__tests__/modules/appCatalog/utils/resolveHelpers.test.ts +79 -0
- package/src/index.css +73 -66
- package/src/modules/appCatalog/hooks/useSessionSyncedState.ts +83 -0
- package/src/modules/appCatalog/ui/components/AccessPrerequisiteChain.tsx +89 -0
- package/src/modules/appCatalog/ui/components/AccessRequestSection.tsx +31 -2
- package/src/modules/appCatalog/ui/components/OnboardingCard.tsx +1 -1
- package/src/modules/appCatalog/ui/components/SubResourcesSection.tsx +22 -1
- package/src/modules/appCatalog/ui/context/AppCatalogFiltersContext.tsx +11 -5
- package/src/modules/appCatalog/ui/grid/AppCatalogGrid.tsx +68 -10
- package/src/modules/appCatalog/ui/launcher/LauncherDetailPanel.tsx +55 -0
- package/src/modules/appCatalog/ui/launcher/LauncherHome.tsx +497 -0
- package/src/modules/appCatalog/ui/launcher/ResourceIcon.tsx +54 -0
- package/src/modules/appCatalog/ui/launcher/monogram.ts +41 -0
- package/src/modules/appCatalog/ui/pages/AppCatalogPage.tsx +75 -13
- package/src/modules/appCatalog/utils/markdownToPlainText.ts +1 -1
- package/src/modules/appCatalog/utils/resolveHelpers.ts +31 -0
- package/src/routes/_layout/app.$slug.tsx +7 -4
- package/src/routes/_layout/catalog.apps.index.tsx +4 -1
- package/src/routes/_layout/index.tsx +5 -1
- package/src/routes/_layout/service-desks.tsx +4 -1
- package/src/ui/components/header/Header.tsx +3 -1
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Search, ArrowUpRight } from "lucide-react";
|
|
3
|
+
import { useState, useEffect, useMemo, useRef } from "react";
|
|
4
|
+
import { cn } from "../../../../lib/utils.js";
|
|
5
|
+
import { useAppClickHistory } from "../../hooks/useAppClickHistory.js";
|
|
6
|
+
import { markdownToPlainText } from "../../utils/markdownToPlainText.js";
|
|
7
|
+
import { ResourceIcon } from "./ResourceIcon.js";
|
|
8
|
+
import { searchResources } from "../../utils/searchApps.js";
|
|
9
|
+
const typeLabel = (t) => {
|
|
10
|
+
if (!t || t === "application") return "App";
|
|
11
|
+
return t.charAt(0).toUpperCase() + t.slice(1);
|
|
12
|
+
};
|
|
13
|
+
function LaunchButton({
|
|
14
|
+
app,
|
|
15
|
+
onLaunch,
|
|
16
|
+
className
|
|
17
|
+
}) {
|
|
18
|
+
if (!app.appUrl) return null;
|
|
19
|
+
return /* @__PURE__ */ jsx(
|
|
20
|
+
"a",
|
|
21
|
+
{
|
|
22
|
+
href: app.appUrl,
|
|
23
|
+
target: "_blank",
|
|
24
|
+
rel: "noopener noreferrer",
|
|
25
|
+
title: `Open ${app.displayName} in a new tab`,
|
|
26
|
+
"aria-label": `Open ${app.displayName} in a new tab`,
|
|
27
|
+
onClick: (e) => {
|
|
28
|
+
e.stopPropagation();
|
|
29
|
+
onLaunch(app);
|
|
30
|
+
},
|
|
31
|
+
className: cn(
|
|
32
|
+
"grid place-items-center rounded-full border border-border bg-card text-muted-foreground",
|
|
33
|
+
"hover:bg-primary hover:text-primary-foreground hover:border-primary transition-colors",
|
|
34
|
+
className
|
|
35
|
+
),
|
|
36
|
+
children: /* @__PURE__ */ jsx(ArrowUpRight, { className: "size-3.5" })
|
|
37
|
+
}
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
function SectionHead({
|
|
41
|
+
title,
|
|
42
|
+
count,
|
|
43
|
+
action
|
|
44
|
+
}) {
|
|
45
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2.5 mb-4", children: [
|
|
46
|
+
/* @__PURE__ */ jsx("h2", { className: "font-serif font-semibold text-[15px] tracking-tight m-0", children: title }),
|
|
47
|
+
count && /* @__PURE__ */ jsx("span", { className: "text-[13px] text-muted-foreground bg-muted rounded-full px-2.5 py-0.5", children: count }),
|
|
48
|
+
action && /* @__PURE__ */ jsx("div", { className: "ml-auto", children: action })
|
|
49
|
+
] });
|
|
50
|
+
}
|
|
51
|
+
function ResourceRow({
|
|
52
|
+
app,
|
|
53
|
+
onAppClick,
|
|
54
|
+
onLaunch,
|
|
55
|
+
showNew = false
|
|
56
|
+
}) {
|
|
57
|
+
return /* @__PURE__ */ jsxs(
|
|
58
|
+
"button",
|
|
59
|
+
{
|
|
60
|
+
type: "button",
|
|
61
|
+
onClick: () => onAppClick(app),
|
|
62
|
+
title: `View ${app.displayName}`,
|
|
63
|
+
className: cn(
|
|
64
|
+
"group flex items-center gap-3.5 w-full text-left",
|
|
65
|
+
"bg-card border border-border rounded-[var(--radius)] px-3.5 py-3",
|
|
66
|
+
"hover:border-ring hover:shadow-sm transition-all"
|
|
67
|
+
),
|
|
68
|
+
children: [
|
|
69
|
+
/* @__PURE__ */ jsx(ResourceIcon, { app, size: 38 }),
|
|
70
|
+
/* @__PURE__ */ jsxs("span", { className: "flex-1 min-w-0", children: [
|
|
71
|
+
/* @__PURE__ */ jsx("span", { className: "block text-[14.5px] font-semibold truncate", children: app.displayName }),
|
|
72
|
+
app.description && /* @__PURE__ */ jsx("span", { className: "block text-[13px] text-muted-foreground truncate", children: markdownToPlainText(app.description) })
|
|
73
|
+
] }),
|
|
74
|
+
showNew && /* @__PURE__ */ jsx("span", { className: "text-[11px] font-bold text-[#65a443] bg-[#65a443]/12 rounded-full px-2.5 py-0.5", children: "New" }),
|
|
75
|
+
/* @__PURE__ */ jsx("span", { className: "text-[12px] text-muted-foreground bg-muted rounded-full px-2.5 py-0.5 whitespace-nowrap", children: typeLabel(app.type) }),
|
|
76
|
+
/* @__PURE__ */ jsx(LaunchButton, { app, onLaunch, className: "size-8" })
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
function SearchResultsList({
|
|
82
|
+
apps,
|
|
83
|
+
searchValue,
|
|
84
|
+
onAppClick,
|
|
85
|
+
onLaunch,
|
|
86
|
+
onClear
|
|
87
|
+
}) {
|
|
88
|
+
const [focusedIndex, setFocusedIndex] = useState(-1);
|
|
89
|
+
const results = useMemo(
|
|
90
|
+
() => searchResources(apps, searchValue),
|
|
91
|
+
[apps, searchValue]
|
|
92
|
+
);
|
|
93
|
+
const didDeprecatedFallback = useMemo(
|
|
94
|
+
() => results.length > 0 && results.every((r) => Boolean(r.deprecated)),
|
|
95
|
+
[results]
|
|
96
|
+
);
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
setFocusedIndex(-1);
|
|
99
|
+
}, [searchValue]);
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
const onKey = (e) => {
|
|
102
|
+
if (results.length === 0) return;
|
|
103
|
+
if (e.key === "ArrowDown") {
|
|
104
|
+
e.preventDefault();
|
|
105
|
+
setFocusedIndex((i) => Math.min(i + 1, results.length - 1));
|
|
106
|
+
} else if (e.key === "ArrowUp") {
|
|
107
|
+
e.preventDefault();
|
|
108
|
+
setFocusedIndex((i) => Math.max(i - 1, 0));
|
|
109
|
+
} else if (e.key === "Enter") {
|
|
110
|
+
if (focusedIndex >= 0 && results[focusedIndex]) {
|
|
111
|
+
onAppClick(results[focusedIndex]);
|
|
112
|
+
}
|
|
113
|
+
} else if (e.key === "Escape") {
|
|
114
|
+
onClear();
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
document.addEventListener("keydown", onKey);
|
|
118
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
119
|
+
}, [results, focusedIndex, onAppClick, onClear]);
|
|
120
|
+
return /* @__PURE__ */ jsxs("div", { className: "max-w-[620px] mx-auto mt-3", children: [
|
|
121
|
+
/* @__PURE__ */ jsx("div", { className: "text-[13px] text-muted-foreground mb-2 px-1", children: results.length === 0 ? `No results for "${searchValue}"` : `${results.length} result${results.length === 1 ? "" : "s"}` }),
|
|
122
|
+
didDeprecatedFallback && /* @__PURE__ */ jsxs(
|
|
123
|
+
"div",
|
|
124
|
+
{
|
|
125
|
+
role: "status",
|
|
126
|
+
className: "text-[13px] text-muted-foreground mb-2 px-1",
|
|
127
|
+
children: [
|
|
128
|
+
'No active apps for "',
|
|
129
|
+
searchValue,
|
|
130
|
+
'" — showing deprecated matches.'
|
|
131
|
+
]
|
|
132
|
+
}
|
|
133
|
+
),
|
|
134
|
+
/* @__PURE__ */ jsx(
|
|
135
|
+
"div",
|
|
136
|
+
{
|
|
137
|
+
className: "flex flex-col gap-1.5",
|
|
138
|
+
role: "listbox",
|
|
139
|
+
"aria-label": "Search results",
|
|
140
|
+
children: results.map((app, i) => /* @__PURE__ */ jsxs(
|
|
141
|
+
"button",
|
|
142
|
+
{
|
|
143
|
+
type: "button",
|
|
144
|
+
role: "option",
|
|
145
|
+
title: `View ${app.displayName}`,
|
|
146
|
+
"aria-selected": focusedIndex === i,
|
|
147
|
+
onMouseEnter: () => setFocusedIndex(i),
|
|
148
|
+
onClick: () => onAppClick(app),
|
|
149
|
+
className: cn(
|
|
150
|
+
"group flex items-center gap-3.5 w-full text-left",
|
|
151
|
+
"bg-card border rounded-[var(--radius)] px-3.5 py-2.5",
|
|
152
|
+
"transition-all",
|
|
153
|
+
focusedIndex === i ? "border-ring shadow-sm bg-muted/30" : "border-border hover:border-ring hover:shadow-sm"
|
|
154
|
+
),
|
|
155
|
+
children: [
|
|
156
|
+
/* @__PURE__ */ jsx(ResourceIcon, { app, size: 40 }),
|
|
157
|
+
/* @__PURE__ */ jsxs("span", { className: "flex-1 min-w-0", children: [
|
|
158
|
+
/* @__PURE__ */ jsxs("span", { className: "block text-[14px] font-semibold truncate", children: [
|
|
159
|
+
app.displayName,
|
|
160
|
+
app.abbreviation && app.abbreviation !== app.displayName && /* @__PURE__ */ jsxs("span", { className: "ml-1.5 text-[12px] font-normal text-muted-foreground", children: [
|
|
161
|
+
"(",
|
|
162
|
+
app.abbreviation,
|
|
163
|
+
")"
|
|
164
|
+
] })
|
|
165
|
+
] }),
|
|
166
|
+
app.description && /* @__PURE__ */ jsx("span", { className: "block text-[12.5px] text-muted-foreground truncate", children: markdownToPlainText(app.description) })
|
|
167
|
+
] }),
|
|
168
|
+
/* @__PURE__ */ jsx("span", { className: "text-[11.5px] text-muted-foreground bg-muted rounded-full px-2.5 py-0.5 whitespace-nowrap shrink-0", children: typeLabel(app.type) }),
|
|
169
|
+
/* @__PURE__ */ jsx(
|
|
170
|
+
LaunchButton,
|
|
171
|
+
{
|
|
172
|
+
app,
|
|
173
|
+
onLaunch,
|
|
174
|
+
className: "size-7 shrink-0 opacity-0 group-hover:opacity-100"
|
|
175
|
+
}
|
|
176
|
+
)
|
|
177
|
+
]
|
|
178
|
+
},
|
|
179
|
+
app.slug
|
|
180
|
+
))
|
|
181
|
+
}
|
|
182
|
+
),
|
|
183
|
+
results.length > 0 && /* @__PURE__ */ jsx("p", { className: "text-[11.5px] text-muted-foreground/60 text-center mt-3", children: "↑↓ navigate · ↵ view access · esc clear" })
|
|
184
|
+
] });
|
|
185
|
+
}
|
|
186
|
+
function LauncherHome({
|
|
187
|
+
apps,
|
|
188
|
+
allResources,
|
|
189
|
+
searchValue,
|
|
190
|
+
onSearchChange,
|
|
191
|
+
onAppClick,
|
|
192
|
+
onLaunch,
|
|
193
|
+
totalCount
|
|
194
|
+
}) {
|
|
195
|
+
const { getTopApps } = useAppClickHistory();
|
|
196
|
+
const [topSlugs, setTopSlugs] = useState([]);
|
|
197
|
+
useEffect(() => {
|
|
198
|
+
void getTopApps(5).then(setTopSlugs);
|
|
199
|
+
}, [getTopApps]);
|
|
200
|
+
const bySlug = useMemo(() => {
|
|
201
|
+
const m = /* @__PURE__ */ new Map();
|
|
202
|
+
for (const a of apps) m.set(a.slug, a);
|
|
203
|
+
return m;
|
|
204
|
+
}, [apps]);
|
|
205
|
+
const frequent = useMemo(
|
|
206
|
+
() => topSlugs.map((s) => bySlug.get(s)).filter((a) => Boolean(a)).slice(0, 5),
|
|
207
|
+
[topSlugs, bySlug]
|
|
208
|
+
);
|
|
209
|
+
const fresh = useMemo(() => {
|
|
210
|
+
const weekAgo = Date.now() - 7 * 24 * 60 * 60 * 1e3;
|
|
211
|
+
return apps.filter((a) => {
|
|
212
|
+
var _a;
|
|
213
|
+
const t = (_a = a.freshness) == null ? void 0 : _a.lastCheckedAt;
|
|
214
|
+
return t ? new Date(t).getTime() >= weekAgo : false;
|
|
215
|
+
}).sort((a, b) => {
|
|
216
|
+
var _a, _b;
|
|
217
|
+
const ta = new Date(((_a = a.freshness) == null ? void 0 : _a.lastCheckedAt) ?? 0).getTime();
|
|
218
|
+
const tb = new Date(((_b = b.freshness) == null ? void 0 : _b.lastCheckedAt) ?? 0).getTime();
|
|
219
|
+
return tb - ta;
|
|
220
|
+
}).slice(0, 6);
|
|
221
|
+
}, [apps]);
|
|
222
|
+
const browse = useMemo(() => {
|
|
223
|
+
const promoted = new Set(frequent.map((a) => a.slug));
|
|
224
|
+
return [...apps].filter((a) => !promoted.has(a.slug)).sort((a, b) => a.displayName.localeCompare(b.displayName));
|
|
225
|
+
}, [apps, frequent]);
|
|
226
|
+
const browseLeft = browse.filter((_, i) => i % 2 === 0);
|
|
227
|
+
const browseRight = browse.filter((_, i) => i % 2 === 1);
|
|
228
|
+
const isSearching = searchValue.trim() !== "";
|
|
229
|
+
const searchRef = useRef(null);
|
|
230
|
+
useEffect(() => {
|
|
231
|
+
const onKey = (e) => {
|
|
232
|
+
var _a;
|
|
233
|
+
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
|
|
234
|
+
e.preventDefault();
|
|
235
|
+
(_a = searchRef.current) == null ? void 0 : _a.focus();
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
document.addEventListener("keydown", onKey);
|
|
239
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
240
|
+
}, []);
|
|
241
|
+
return /* @__PURE__ */ jsxs("div", { className: "mx-auto w-full max-w-[1000px] pb-24", children: [
|
|
242
|
+
/* @__PURE__ */ jsxs("div", { className: "text-center pt-10 pb-1.5", children: [
|
|
243
|
+
/* @__PURE__ */ jsx("h1", { className: "font-serif font-semibold text-[30px] tracking-tight m-0 mb-1.5", children: "What do you need to get into?" }),
|
|
244
|
+
/* @__PURE__ */ jsx("p", { className: "text-muted-foreground m-0 mb-6 text-[15px]", children: "Find any tool, see how to request access, or jump straight in." }),
|
|
245
|
+
/* @__PURE__ */ jsx("div", { className: "max-w-[620px] mx-auto", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 bg-card border-[1.5px] border-input rounded-full px-5 h-14 shadow-sm focus-within:border-primary focus-within:ring-4 focus-within:ring-primary/15 transition-colors", children: [
|
|
246
|
+
/* @__PURE__ */ jsx(Search, { className: "size-5 shrink-0 text-muted-foreground" }),
|
|
247
|
+
/* @__PURE__ */ jsx(
|
|
248
|
+
"input",
|
|
249
|
+
{
|
|
250
|
+
type: "text",
|
|
251
|
+
value: searchValue,
|
|
252
|
+
onChange: (e) => onSearchChange(e.target.value),
|
|
253
|
+
placeholder: "Search apps, databases, cloud accounts, account IDs…",
|
|
254
|
+
autoComplete: "off",
|
|
255
|
+
autoFocus: true,
|
|
256
|
+
ref: searchRef,
|
|
257
|
+
"aria-label": "Search apps",
|
|
258
|
+
className: "flex-1 bg-transparent border-0 outline-none text-[16px] text-foreground placeholder:text-muted-foreground"
|
|
259
|
+
}
|
|
260
|
+
),
|
|
261
|
+
!isSearching && /* @__PURE__ */ jsx("kbd", { className: "hidden sm:inline-flex items-center gap-0.5 text-[11px] text-muted-foreground/60 border border-muted-foreground/20 rounded px-1.5 py-0.5 font-mono shrink-0", children: typeof navigator !== "undefined" && navigator.platform.includes("Mac") ? "⌘K" : "Ctrl+K" })
|
|
262
|
+
] }) })
|
|
263
|
+
] }),
|
|
264
|
+
isSearching && /* @__PURE__ */ jsx(
|
|
265
|
+
SearchResultsList,
|
|
266
|
+
{
|
|
267
|
+
apps: allResources ?? apps,
|
|
268
|
+
searchValue,
|
|
269
|
+
onAppClick,
|
|
270
|
+
onLaunch,
|
|
271
|
+
onClear: () => onSearchChange("")
|
|
272
|
+
}
|
|
273
|
+
),
|
|
274
|
+
!isSearching && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
275
|
+
frequent.length > 0 && /* @__PURE__ */ jsxs("section", { className: "mt-9", children: [
|
|
276
|
+
/* @__PURE__ */ jsx(SectionHead, { title: "Your apps", count: "frequent" }),
|
|
277
|
+
/* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 gap-3.5", children: frequent.map((app) => /* @__PURE__ */ jsxs(
|
|
278
|
+
"button",
|
|
279
|
+
{
|
|
280
|
+
type: "button",
|
|
281
|
+
onClick: () => onAppClick(app),
|
|
282
|
+
title: `View ${app.displayName}`,
|
|
283
|
+
className: "group relative bg-card border border-border rounded-[var(--radius)] px-3.5 pt-[18px] pb-4 text-center shadow-sm hover:-translate-y-0.5 hover:shadow-md transition-all",
|
|
284
|
+
children: [
|
|
285
|
+
/* @__PURE__ */ jsx(
|
|
286
|
+
LaunchButton,
|
|
287
|
+
{
|
|
288
|
+
app,
|
|
289
|
+
onLaunch,
|
|
290
|
+
className: "absolute top-2.5 right-2.5 size-[26px] opacity-0 group-hover:opacity-100"
|
|
291
|
+
}
|
|
292
|
+
),
|
|
293
|
+
/* @__PURE__ */ jsx(
|
|
294
|
+
ResourceIcon,
|
|
295
|
+
{
|
|
296
|
+
app,
|
|
297
|
+
size: 46,
|
|
298
|
+
className: "mx-auto mb-3"
|
|
299
|
+
}
|
|
300
|
+
),
|
|
301
|
+
/* @__PURE__ */ jsx("span", { className: "block text-[13.5px] font-bold truncate", children: app.displayName })
|
|
302
|
+
]
|
|
303
|
+
},
|
|
304
|
+
app.slug
|
|
305
|
+
)) })
|
|
306
|
+
] }),
|
|
307
|
+
fresh.length > 0 && /* @__PURE__ */ jsxs("section", { className: "mt-9", children: [
|
|
308
|
+
/* @__PURE__ */ jsx(
|
|
309
|
+
SectionHead,
|
|
310
|
+
{
|
|
311
|
+
title: "New this week",
|
|
312
|
+
count: `${fresh.length} added`
|
|
313
|
+
}
|
|
314
|
+
),
|
|
315
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-col gap-2", children: fresh.map((app) => /* @__PURE__ */ jsx(
|
|
316
|
+
ResourceRow,
|
|
317
|
+
{
|
|
318
|
+
app,
|
|
319
|
+
onAppClick,
|
|
320
|
+
onLaunch,
|
|
321
|
+
showNew: true
|
|
322
|
+
},
|
|
323
|
+
app.slug
|
|
324
|
+
)) })
|
|
325
|
+
] }),
|
|
326
|
+
/* @__PURE__ */ jsxs("section", { className: "mt-9", children: [
|
|
327
|
+
/* @__PURE__ */ jsx(SectionHead, { title: "Browse all", count: `${totalCount} resources` }),
|
|
328
|
+
/* @__PURE__ */ jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-2", children: [
|
|
329
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-col gap-2", children: browseLeft.map((app) => /* @__PURE__ */ jsx(
|
|
330
|
+
ResourceRow,
|
|
331
|
+
{
|
|
332
|
+
app,
|
|
333
|
+
onAppClick,
|
|
334
|
+
onLaunch
|
|
335
|
+
},
|
|
336
|
+
app.slug
|
|
337
|
+
)) }),
|
|
338
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-col gap-2", children: browseRight.map((app) => /* @__PURE__ */ jsx(
|
|
339
|
+
ResourceRow,
|
|
340
|
+
{
|
|
341
|
+
app,
|
|
342
|
+
onAppClick,
|
|
343
|
+
onLaunch
|
|
344
|
+
},
|
|
345
|
+
app.slug
|
|
346
|
+
)) })
|
|
347
|
+
] })
|
|
348
|
+
] })
|
|
349
|
+
] })
|
|
350
|
+
] });
|
|
351
|
+
}
|
|
352
|
+
export {
|
|
353
|
+
LauncherHome
|
|
354
|
+
};
|
|
355
|
+
//# sourceMappingURL=LauncherHome.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LauncherHome.js","sources":["../../../../../../src/modules/appCatalog/ui/launcher/LauncherHome.tsx"],"sourcesContent":["import type { Resource } from '@igstack/app-catalog-backend-core'\nimport { ArrowUpRight, Search } from 'lucide-react'\nimport { useEffect, useMemo, useRef, useState } from 'react'\nimport { cn } from '~/lib/utils'\nimport { useAppClickHistory } from '../../hooks/useAppClickHistory'\nimport { markdownToPlainText } from '../../utils/markdownToPlainText'\nimport { ResourceIcon } from './ResourceIcon'\nimport { searchResources } from '../../utils/searchApps'\n\n/**\n * Adaptive-home discovery spine (issue #38, increment 1) — matches the\n * option-a-launcher prototype:\n * Your apps (frequent) → New this week → Browse all\n * Rendered by AppCatalogPage when there is no active search and no open detail.\n * The hero search box drives the same `searchValue` used elsewhere; typing hands\n * off to the results view (increment 2 refines the morph).\n */\n\nconst typeLabel = (t?: string): string => {\n if (!t || t === 'application') return 'App'\n return t.charAt(0).toUpperCase() + t.slice(1)\n}\n\nexport interface LauncherHomeProps {\n /** All root (non-child) resources, already filtered for deprecated per settings. */\n apps: Resource[]\n /**\n * All resources INCLUDING children/sub-resources. Passed to search so a query\n * matching a sub-resource (e.g. an AWS account, an alias/account ID) surfaces\n * its parent — preserving the existing cross-sub-resource search behavior.\n * Falls back to `apps` when omitted.\n */\n allResources?: Resource[]\n searchValue: string\n onSearchChange: (v: string) => void\n onAppClick: (app: Resource) => void\n onLaunch: (app: Resource) => void\n /** Total resource count for the \"Browse all\" label. */\n totalCount: number\n}\n\nfunction LaunchButton({\n app,\n onLaunch,\n className,\n}: {\n app: Resource\n onLaunch: (app: Resource) => void\n className?: string\n}) {\n if (!app.appUrl) return null\n return (\n <a\n href={app.appUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n title={`Open ${app.displayName} in a new tab`}\n aria-label={`Open ${app.displayName} in a new tab`}\n onClick={(e) => {\n e.stopPropagation()\n onLaunch(app)\n }}\n className={cn(\n 'grid place-items-center rounded-full border border-border bg-card text-muted-foreground',\n 'hover:bg-primary hover:text-primary-foreground hover:border-primary transition-colors',\n className,\n )}\n >\n <ArrowUpRight className=\"size-3.5\" />\n </a>\n )\n}\n\nfunction SectionHead({\n title,\n count,\n action,\n}: {\n title: string\n count?: string\n action?: React.ReactNode\n}) {\n return (\n <div className=\"flex items-center gap-2.5 mb-4\">\n <h2 className=\"font-serif font-semibold text-[15px] tracking-tight m-0\">\n {title}\n </h2>\n {count && (\n <span className=\"text-[13px] text-muted-foreground bg-muted rounded-full px-2.5 py-0.5\">\n {count}\n </span>\n )}\n {action && <div className=\"ml-auto\">{action}</div>}\n </div>\n )\n}\n\n/** Small row used by \"New this week\" and \"Browse all\". */\nfunction ResourceRow({\n app,\n onAppClick,\n onLaunch,\n showNew = false,\n}: {\n app: Resource\n onAppClick: (app: Resource) => void\n onLaunch: (app: Resource) => void\n showNew?: boolean\n}) {\n return (\n <button\n type=\"button\"\n onClick={() => onAppClick(app)}\n title={`View ${app.displayName}`}\n className={cn(\n 'group flex items-center gap-3.5 w-full text-left',\n 'bg-card border border-border rounded-[var(--radius)] px-3.5 py-3',\n 'hover:border-ring hover:shadow-sm transition-all',\n )}\n >\n <ResourceIcon app={app} size={38} />\n <span className=\"flex-1 min-w-0\">\n <span className=\"block text-[14.5px] font-semibold truncate\">\n {app.displayName}\n </span>\n {app.description && (\n <span className=\"block text-[13px] text-muted-foreground truncate\">\n {markdownToPlainText(app.description)}\n </span>\n )}\n </span>\n {showNew && (\n <span className=\"text-[11px] font-bold text-[#65a443] bg-[#65a443]/12 rounded-full px-2.5 py-0.5\">\n New\n </span>\n )}\n <span className=\"text-[12px] text-muted-foreground bg-muted rounded-full px-2.5 py-0.5 whitespace-nowrap\">\n {typeLabel(app.type)}\n </span>\n <LaunchButton app={app} onLaunch={onLaunch} className=\"size-8\" />\n </button>\n )\n}\n\n/**\n * Keyboard-navigable search results list — increment 2 of the launcher redesign.\n * Appears below the hero search box when the user types, replacing the\n * discovery spine. Pressing ↑↓ navigates, ↵ opens detail, Esc clears.\n */\nfunction SearchResultsList({\n apps,\n searchValue,\n onAppClick,\n onLaunch,\n onClear,\n}: {\n apps: Resource[]\n searchValue: string\n onAppClick: (app: Resource) => void\n onLaunch: (app: Resource) => void\n onClear: () => void\n}) {\n const [focusedIndex, setFocusedIndex] = useState(-1)\n\n const results = useMemo(\n () => searchResources(apps, searchValue),\n [apps, searchValue],\n )\n\n // #11 deprecated fallback: when EVERY match is a deprecated app (i.e. there\n // are no active matches for the query, only deprecated ones), surface a\n // notice so the user understands why they're seeing archived tools. The\n // deprecated apps are already in `results` because the launcher feeds search\n // the full resource set; this just labels the situation.\n const didDeprecatedFallback = useMemo(\n () => results.length > 0 && results.every((r) => Boolean(r.deprecated)),\n [results],\n )\n\n // Reset focus when results change\n useEffect(() => {\n setFocusedIndex(-1)\n }, [searchValue])\n\n useEffect(() => {\n const onKey = (e: KeyboardEvent) => {\n if (results.length === 0) return\n if (e.key === 'ArrowDown') {\n e.preventDefault()\n setFocusedIndex((i) => Math.min(i + 1, results.length - 1))\n } else if (e.key === 'ArrowUp') {\n e.preventDefault()\n setFocusedIndex((i) => Math.max(i - 1, 0))\n } else if (e.key === 'Enter') {\n if (focusedIndex >= 0 && results[focusedIndex]) {\n onAppClick(results[focusedIndex])\n }\n } else if (e.key === 'Escape') {\n onClear()\n }\n }\n document.addEventListener('keydown', onKey)\n return () => document.removeEventListener('keydown', onKey)\n }, [results, focusedIndex, onAppClick, onClear])\n\n return (\n <div className=\"max-w-[620px] mx-auto mt-3\">\n {/* Meta line */}\n <div className=\"text-[13px] text-muted-foreground mb-2 px-1\">\n {results.length === 0\n ? `No results for \"${searchValue}\"`\n : `${results.length} result${results.length === 1 ? '' : 's'}`}\n </div>\n\n {/* #11: deprecated-only fallback notice */}\n {didDeprecatedFallback && (\n <div\n role=\"status\"\n className=\"text-[13px] text-muted-foreground mb-2 px-1\"\n >\n No active apps for "{searchValue}" — showing deprecated\n matches.\n </div>\n )}\n\n {/* Results */}\n <div\n className=\"flex flex-col gap-1.5\"\n role=\"listbox\"\n aria-label=\"Search results\"\n >\n {results.map((app, i) => (\n <button\n key={app.slug}\n type=\"button\"\n role=\"option\"\n title={`View ${app.displayName}`}\n aria-selected={focusedIndex === i}\n onMouseEnter={() => setFocusedIndex(i)}\n onClick={() => onAppClick(app)}\n className={cn(\n 'group flex items-center gap-3.5 w-full text-left',\n 'bg-card border rounded-[var(--radius)] px-3.5 py-2.5',\n 'transition-all',\n focusedIndex === i\n ? 'border-ring shadow-sm bg-muted/30'\n : 'border-border hover:border-ring hover:shadow-sm',\n )}\n >\n <ResourceIcon app={app} size={40} />\n <span className=\"flex-1 min-w-0\">\n <span className=\"block text-[14px] font-semibold truncate\">\n {app.displayName}\n {app.abbreviation && app.abbreviation !== app.displayName && (\n <span className=\"ml-1.5 text-[12px] font-normal text-muted-foreground\">\n ({app.abbreviation})\n </span>\n )}\n </span>\n {app.description && (\n <span className=\"block text-[12.5px] text-muted-foreground truncate\">\n {markdownToPlainText(app.description)}\n </span>\n )}\n </span>\n <span className=\"text-[11.5px] text-muted-foreground bg-muted rounded-full px-2.5 py-0.5 whitespace-nowrap shrink-0\">\n {typeLabel(app.type)}\n </span>\n <LaunchButton\n app={app}\n onLaunch={onLaunch}\n className=\"size-7 shrink-0 opacity-0 group-hover:opacity-100\"\n />\n </button>\n ))}\n </div>\n\n {/* Keyboard hint */}\n {results.length > 0 && (\n <p className=\"text-[11.5px] text-muted-foreground/60 text-center mt-3\">\n ↑↓ navigate · ↵ view access · esc clear\n </p>\n )}\n </div>\n )\n}\n\nexport function LauncherHome({\n apps,\n allResources,\n searchValue,\n onSearchChange,\n onAppClick,\n onLaunch,\n totalCount,\n}: LauncherHomeProps) {\n const { getTopApps } = useAppClickHistory()\n const [topSlugs, setTopSlugs] = useState<string[]>([])\n\n useEffect(() => {\n void getTopApps(5).then(setTopSlugs)\n }, [getTopApps])\n\n const bySlug = useMemo(() => {\n const m = new Map<string, Resource>()\n for (const a of apps) m.set(a.slug, a)\n return m\n }, [apps])\n\n // Your apps: frequently-clicked (personalized). Empty on a cold new machine.\n const frequent = useMemo(\n () =>\n topSlugs\n .map((s) => bySlug.get(s))\n .filter((a): a is Resource => Boolean(a))\n .slice(0, 5),\n [topSlugs, bySlug],\n )\n\n // New this week: apps whose sources were checked in the last 7 days, as a\n // proxy for \"recently added/updated\" (frontend Resource has no createdAt yet;\n // see #38). Falls back to empty (section hidden) when no freshness data.\n const fresh = useMemo(() => {\n const weekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000\n return apps\n .filter((a) => {\n const t = a.freshness?.lastCheckedAt\n return t ? new Date(t).getTime() >= weekAgo : false\n })\n .sort((a, b) => {\n const ta = new Date(a.freshness?.lastCheckedAt ?? 0).getTime()\n const tb = new Date(b.freshness?.lastCheckedAt ?? 0).getTime()\n return tb - ta\n })\n .slice(0, 6)\n }, [apps])\n\n // Browse all: everything not already promoted to \"Your apps\", alpha by name.\n const browse = useMemo(() => {\n const promoted = new Set(frequent.map((a) => a.slug))\n return [...apps]\n .filter((a) => !promoted.has(a.slug))\n .sort((a, b) => a.displayName.localeCompare(b.displayName))\n }, [apps, frequent])\n\n const browseLeft = browse.filter((_, i) => i % 2 === 0)\n const browseRight = browse.filter((_, i) => i % 2 === 1)\n\n const isSearching = searchValue.trim() !== ''\n const searchRef = useRef<HTMLInputElement>(null)\n\n // ⌘K / Ctrl+K focuses the hero search box from anywhere on the page.\n useEffect(() => {\n const onKey = (e: KeyboardEvent) => {\n if ((e.metaKey || e.ctrlKey) && e.key === 'k') {\n e.preventDefault()\n searchRef.current?.focus()\n }\n }\n document.addEventListener('keydown', onKey)\n return () => document.removeEventListener('keydown', onKey)\n }, [])\n\n return (\n <div className=\"mx-auto w-full max-w-[1000px] pb-24\">\n {/* hero */}\n <div className=\"text-center pt-10 pb-1.5\">\n <h1 className=\"font-serif font-semibold text-[30px] tracking-tight m-0 mb-1.5\">\n What do you need to get into?\n </h1>\n <p className=\"text-muted-foreground m-0 mb-6 text-[15px]\">\n Find any tool, see how to request access, or jump straight in.\n </p>\n <div className=\"max-w-[620px] mx-auto\">\n <div className=\"flex items-center gap-3 bg-card border-[1.5px] border-input rounded-full px-5 h-14 shadow-sm focus-within:border-primary focus-within:ring-4 focus-within:ring-primary/15 transition-colors\">\n <Search className=\"size-5 shrink-0 text-muted-foreground\" />\n <input\n type=\"text\"\n value={searchValue}\n onChange={(e) => onSearchChange(e.target.value)}\n placeholder=\"Search apps, databases, cloud accounts, account IDs…\"\n autoComplete=\"off\"\n autoFocus\n ref={searchRef}\n aria-label=\"Search apps\"\n className=\"flex-1 bg-transparent border-0 outline-none text-[16px] text-foreground placeholder:text-muted-foreground\"\n />\n {!isSearching && (\n <kbd className=\"hidden sm:inline-flex items-center gap-0.5 text-[11px] text-muted-foreground/60 border border-muted-foreground/20 rounded px-1.5 py-0.5 font-mono shrink-0\">\n {typeof navigator !== 'undefined' &&\n navigator.platform.includes('Mac')\n ? '⌘K'\n : 'Ctrl+K'}\n </kbd>\n )}\n </div>\n </div>\n </div>\n\n {/* Search morph: when typing, show compact results list instead of the discovery spine */}\n {isSearching && (\n <SearchResultsList\n apps={allResources ?? apps}\n searchValue={searchValue}\n onAppClick={onAppClick}\n onLaunch={onLaunch}\n onClear={() => onSearchChange('')}\n />\n )}\n\n {/* Discovery spine: only shown when not searching */}\n {!isSearching && (\n <>\n {/* Your apps */}\n {frequent.length > 0 && (\n <section className=\"mt-9\">\n <SectionHead title=\"Your apps\" count=\"frequent\" />\n <div className=\"grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 gap-3.5\">\n {frequent.map((app) => (\n <button\n type=\"button\"\n key={app.slug}\n onClick={() => onAppClick(app)}\n title={`View ${app.displayName}`}\n className=\"group relative bg-card border border-border rounded-[var(--radius)] px-3.5 pt-[18px] pb-4 text-center shadow-sm hover:-translate-y-0.5 hover:shadow-md transition-all\"\n >\n <LaunchButton\n app={app}\n onLaunch={onLaunch}\n className=\"absolute top-2.5 right-2.5 size-[26px] opacity-0 group-hover:opacity-100\"\n />\n <ResourceIcon\n app={app}\n size={46}\n className=\"mx-auto mb-3\"\n />\n <span className=\"block text-[13.5px] font-bold truncate\">\n {app.displayName}\n </span>\n </button>\n ))}\n </div>\n </section>\n )}\n\n {/* New this week */}\n {fresh.length > 0 && (\n <section className=\"mt-9\">\n <SectionHead\n title=\"New this week\"\n count={`${fresh.length} added`}\n />\n <div className=\"flex flex-col gap-2\">\n {fresh.map((app) => (\n <ResourceRow\n key={app.slug}\n app={app}\n onAppClick={onAppClick}\n onLaunch={onLaunch}\n showNew\n />\n ))}\n </div>\n </section>\n )}\n\n {/* Browse all */}\n <section className=\"mt-9\">\n <SectionHead title=\"Browse all\" count={`${totalCount} resources`} />\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-2\">\n <div className=\"flex flex-col gap-2\">\n {browseLeft.map((app) => (\n <ResourceRow\n key={app.slug}\n app={app}\n onAppClick={onAppClick}\n onLaunch={onLaunch}\n />\n ))}\n </div>\n <div className=\"flex flex-col gap-2\">\n {browseRight.map((app) => (\n <ResourceRow\n key={app.slug}\n app={app}\n onAppClick={onAppClick}\n onLaunch={onLaunch}\n />\n ))}\n </div>\n </div>\n </section>\n </>\n )}\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Resource } from '@igstack/app-catalog-backend-core';
|
|
2
|
+
/**
|
|
3
|
+
* Resource icon used across the launcher UI.
|
|
4
|
+
* - If the app has an `iconName`, render the served image (`/api/icons/<name>`).
|
|
5
|
+
* - Otherwise fall back to a flat crayon-colored monogram chip (prototype look),
|
|
6
|
+
* NOT a generic glyph, so every resource still has a recognizable "scent".
|
|
7
|
+
*/
|
|
8
|
+
export declare function ResourceIcon({ app, size, className, rounded, }: {
|
|
9
|
+
app: Pick<Resource, 'iconName' | 'displayName' | 'abbreviation' | 'slug'>;
|
|
10
|
+
size?: number;
|
|
11
|
+
className?: string;
|
|
12
|
+
rounded?: string;
|
|
13
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { cn } from "../../../../lib/utils.js";
|
|
4
|
+
import { monogramInitials, monogramColor } from "./monogram.js";
|
|
5
|
+
function ResourceIcon({
|
|
6
|
+
app,
|
|
7
|
+
size = 40,
|
|
8
|
+
className,
|
|
9
|
+
rounded = "rounded-[12px]"
|
|
10
|
+
}) {
|
|
11
|
+
const [imageError, setImageError] = useState(false);
|
|
12
|
+
const dim = { width: size, height: size };
|
|
13
|
+
if (app.iconName && !imageError) {
|
|
14
|
+
return /* @__PURE__ */ jsx(
|
|
15
|
+
"img",
|
|
16
|
+
{
|
|
17
|
+
src: `/api/icons/${app.iconName}`,
|
|
18
|
+
alt: "",
|
|
19
|
+
"aria-hidden": "true",
|
|
20
|
+
style: dim,
|
|
21
|
+
className: cn(rounded, "shrink-0 object-contain", className),
|
|
22
|
+
onError: () => setImageError(true)
|
|
23
|
+
}
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
const initials = monogramInitials(app.displayName, app.abbreviation);
|
|
27
|
+
const bg = monogramColor(app.slug || app.displayName);
|
|
28
|
+
return /* @__PURE__ */ jsx(
|
|
29
|
+
"span",
|
|
30
|
+
{
|
|
31
|
+
"aria-hidden": "true",
|
|
32
|
+
style: { ...dim, background: bg, fontSize: Math.round(size * 0.36) },
|
|
33
|
+
className: cn(
|
|
34
|
+
rounded,
|
|
35
|
+
"shrink-0 grid place-items-center font-semibold text-white font-serif tracking-tight select-none",
|
|
36
|
+
className
|
|
37
|
+
),
|
|
38
|
+
children: initials
|
|
39
|
+
}
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
export {
|
|
43
|
+
ResourceIcon
|
|
44
|
+
};
|
|
45
|
+
//# sourceMappingURL=ResourceIcon.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ResourceIcon.js","sources":["../../../../../../src/modules/appCatalog/ui/launcher/ResourceIcon.tsx"],"sourcesContent":["import { useState } from 'react'\nimport type { Resource } from '@igstack/app-catalog-backend-core'\nimport { cn } from '~/lib/utils'\nimport { monogramColor, monogramInitials } from './monogram'\n\n/**\n * Resource icon used across the launcher UI.\n * - If the app has an `iconName`, render the served image (`/api/icons/<name>`).\n * - Otherwise fall back to a flat crayon-colored monogram chip (prototype look),\n * NOT a generic glyph, so every resource still has a recognizable \"scent\".\n */\nexport function ResourceIcon({\n app,\n size = 40,\n className,\n rounded = 'rounded-[12px]',\n}: {\n app: Pick<Resource, 'iconName' | 'displayName' | 'abbreviation' | 'slug'>\n size?: number\n className?: string\n rounded?: string\n}) {\n const [imageError, setImageError] = useState(false)\n const dim = { width: size, height: size }\n\n if (app.iconName && !imageError) {\n return (\n <img\n src={`/api/icons/${app.iconName}`}\n alt=\"\"\n aria-hidden=\"true\"\n style={dim}\n className={cn(rounded, 'shrink-0 object-contain', className)}\n onError={() => setImageError(true)}\n />\n )\n }\n\n const initials = monogramInitials(app.displayName, app.abbreviation)\n const bg = monogramColor(app.slug || app.displayName)\n return (\n <span\n aria-hidden=\"true\"\n style={{ ...dim, background: bg, fontSize: Math.round(size * 0.36) }}\n className={cn(\n rounded,\n 'shrink-0 grid place-items-center font-semibold text-white font-serif tracking-tight select-none',\n className,\n )}\n >\n {initials}\n </span>\n )\n}\n"],"names":[],"mappings":";;;;AAWO,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA,UAAU;AACZ,GAKG;AACD,QAAM,CAAC,YAAY,aAAa,IAAI,SAAS,KAAK;AAClD,QAAM,MAAM,EAAE,OAAO,MAAM,QAAQ,KAAA;AAEnC,MAAI,IAAI,YAAY,CAAC,YAAY;AAC/B,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,KAAK,cAAc,IAAI,QAAQ;AAAA,QAC/B,KAAI;AAAA,QACJ,eAAY;AAAA,QACZ,OAAO;AAAA,QACP,WAAW,GAAG,SAAS,2BAA2B,SAAS;AAAA,QAC3D,SAAS,MAAM,cAAc,IAAI;AAAA,MAAA;AAAA,IAAA;AAAA,EAGvC;AAEA,QAAM,WAAW,iBAAiB,IAAI,aAAa,IAAI,YAAY;AACnE,QAAM,KAAK,cAAc,IAAI,QAAQ,IAAI,WAAW;AACpD,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,eAAY;AAAA,MACZ,OAAO,EAAE,GAAG,KAAK,YAAY,IAAI,UAAU,KAAK,MAAM,OAAO,IAAI,EAAA;AAAA,MACjE,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,MAAA;AAAA,MAGD,UAAA;AAAA,IAAA;AAAA,EAAA;AAGP;"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Stable hash → palette index, so a given slug always maps to one color. */
|
|
2
|
+
export declare function monogramColor(key: string): string;
|
|
3
|
+
/** Up-to-2-char initials from a display name / abbreviation. */
|
|
4
|
+
export declare function monogramInitials(name: string, abbreviation?: string): string;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const CRAYON_PALETTE = [
|
|
2
|
+
"#dc6827",
|
|
3
|
+
// orange
|
|
4
|
+
"#eca832",
|
|
5
|
+
// amber
|
|
6
|
+
"#e4724a",
|
|
7
|
+
// coral
|
|
8
|
+
"#f07a84",
|
|
9
|
+
// pink
|
|
10
|
+
"#3782c2",
|
|
11
|
+
// blue
|
|
12
|
+
"#388aca",
|
|
13
|
+
// blue-2
|
|
14
|
+
"#65a443",
|
|
15
|
+
// green
|
|
16
|
+
"#2c2622"
|
|
17
|
+
// ink
|
|
18
|
+
];
|
|
19
|
+
function monogramColor(key) {
|
|
20
|
+
let hash = 0;
|
|
21
|
+
for (let i = 0; i < key.length; i++) {
|
|
22
|
+
hash = hash * 31 + key.charCodeAt(i) | 0;
|
|
23
|
+
}
|
|
24
|
+
const idx = Math.abs(hash) % CRAYON_PALETTE.length;
|
|
25
|
+
return CRAYON_PALETTE[idx];
|
|
26
|
+
}
|
|
27
|
+
function monogramInitials(name, abbreviation) {
|
|
28
|
+
const base = (abbreviation || name).trim();
|
|
29
|
+
if (!base) return "?";
|
|
30
|
+
if (abbreviation) return abbreviation.slice(0, 2).toUpperCase();
|
|
31
|
+
const words = base.replace(/[^A-Za-z0-9 ]/g, "").split(/\s+/).filter(Boolean);
|
|
32
|
+
if (words.length >= 2) {
|
|
33
|
+
return (words[0][0] + words[1][0]).toUpperCase();
|
|
34
|
+
}
|
|
35
|
+
return base.slice(0, 2).toUpperCase();
|
|
36
|
+
}
|
|
37
|
+
export {
|
|
38
|
+
monogramColor,
|
|
39
|
+
monogramInitials
|
|
40
|
+
};
|
|
41
|
+
//# sourceMappingURL=monogram.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"monogram.js","sources":["../../../../../../src/modules/appCatalog/ui/launcher/monogram.ts"],"sourcesContent":["// Deterministic crayon-palette monogram color for an app that has no icon.\n// Mirrors the prototype's flat brand-color chips (option-a-launcher.html):\n// a small warm palette drawn from the logo, picked stably by slug so the same\n// app always gets the same color.\n\nconst CRAYON_PALETTE = [\n '#dc6827', // orange\n '#eca832', // amber\n '#e4724a', // coral\n '#f07a84', // pink\n '#3782c2', // blue\n '#388aca', // blue-2\n '#65a443', // green\n '#2c2622', // ink\n]\n\n/** Stable hash → palette index, so a given slug always maps to one color. */\nexport function monogramColor(key: string): string {\n let hash = 0\n for (let i = 0; i < key.length; i++) {\n hash = (hash * 31 + key.charCodeAt(i)) | 0\n }\n const idx = Math.abs(hash) % CRAYON_PALETTE.length\n return CRAYON_PALETTE[idx] as string\n}\n\n/** Up-to-2-char initials from a display name / abbreviation. */\nexport function monogramInitials(name: string, abbreviation?: string): string {\n const base = (abbreviation || name).trim()\n if (!base) return '?'\n // If an abbreviation is given, use its first 2 chars verbatim (e.g. \"K8\", \"EH\").\n if (abbreviation) return abbreviation.slice(0, 2).toUpperCase()\n const words = base\n .replace(/[^A-Za-z0-9 ]/g, '')\n .split(/\\s+/)\n .filter(Boolean)\n if (words.length >= 2) {\n return (words[0]![0]! + words[1]![0]!).toUpperCase()\n }\n return base.slice(0, 2).toUpperCase()\n}\n"],"names":[],"mappings":"AAKA,MAAM,iBAAiB;AAAA,EACrB;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AACF;AAGO,SAAS,cAAc,KAAqB;AACjD,MAAI,OAAO;AACX,WAAS,IAAI,GAAG,IAAI,IAAI,QAAQ,KAAK;AACnC,WAAQ,OAAO,KAAK,IAAI,WAAW,CAAC,IAAK;AAAA,EAC3C;AACA,QAAM,MAAM,KAAK,IAAI,IAAI,IAAI,eAAe;AAC5C,SAAO,eAAe,GAAG;AAC3B;AAGO,SAAS,iBAAiB,MAAc,cAA+B;AAC5E,QAAM,QAAQ,gBAAgB,MAAM,KAAA;AACpC,MAAI,CAAC,KAAM,QAAO;AAElB,MAAI,aAAc,QAAO,aAAa,MAAM,GAAG,CAAC,EAAE,YAAA;AAClD,QAAM,QAAQ,KACX,QAAQ,kBAAkB,EAAE,EAC5B,MAAM,KAAK,EACX,OAAO,OAAO;AACjB,MAAI,MAAM,UAAU,GAAG;AACrB,YAAQ,MAAM,CAAC,EAAG,CAAC,IAAK,MAAM,CAAC,EAAG,CAAC,GAAI,YAAA;AAAA,EACzC;AACA,SAAO,KAAK,MAAM,GAAG,CAAC,EAAE,YAAA;AAC1B;"}
|
|
@@ -12,6 +12,8 @@ import { OnboardingCard } from "../components/OnboardingCard.js";
|
|
|
12
12
|
import { useAppCatalogFilters } from "../context/AppCatalogFiltersContext.js";
|
|
13
13
|
import { FilterBar } from "../filters/FilterBar.js";
|
|
14
14
|
import { AppCatalogGrid } from "../grid/AppCatalogGrid.js";
|
|
15
|
+
import { LauncherHome } from "../launcher/LauncherHome.js";
|
|
16
|
+
import { LauncherDetailPanel } from "../launcher/LauncherDetailPanel.js";
|
|
15
17
|
function AppCatalogPage({
|
|
16
18
|
selectedSlug
|
|
17
19
|
} = {}) {
|
|
@@ -88,19 +90,17 @@ function AppCatalogPage({
|
|
|
88
90
|
topAppSlugs,
|
|
89
91
|
searchValue: deferredSearchValue
|
|
90
92
|
});
|
|
93
|
+
const legacyGridActive = filterState.recentMode || Object.keys(filterState.tagFilters).length > 0;
|
|
91
94
|
useEffect(() => {
|
|
92
|
-
if (filteredApps.length === 1 && filteredApps[0] && !selectedAppSlug) {
|
|
95
|
+
if (legacyGridActive && filteredApps.length === 1 && filteredApps[0] && !selectedAppSlug) {
|
|
93
96
|
void navigate({
|
|
94
97
|
to: "/app/$slug",
|
|
95
98
|
params: { slug: filteredApps[0].slug },
|
|
96
|
-
search: (prev) =>
|
|
97
|
-
...prev,
|
|
98
|
-
q: searchValue === "" ? void 0 : searchValue
|
|
99
|
-
}),
|
|
99
|
+
search: (prev) => prev,
|
|
100
100
|
replace: true
|
|
101
101
|
});
|
|
102
102
|
}
|
|
103
|
-
}, [filteredApps, selectedAppSlug, navigate
|
|
103
|
+
}, [legacyGridActive, filteredApps, selectedAppSlug, navigate]);
|
|
104
104
|
useEffect(() => {
|
|
105
105
|
if (!selectedAppSlug || resources.length === 0) return;
|
|
106
106
|
const canonical = resources.some((r) => r.slug === selectedAppSlug);
|
|
@@ -125,6 +125,14 @@ function AppCatalogPage({
|
|
|
125
125
|
search: (prev) => prev
|
|
126
126
|
});
|
|
127
127
|
};
|
|
128
|
+
const handleLaunch = (app) => {
|
|
129
|
+
if (app.appUrl) window.open(app.appUrl, "_blank", "noopener,noreferrer");
|
|
130
|
+
};
|
|
131
|
+
const showLauncherHome = !filterState.recentMode && Object.keys(filterState.tagFilters).length === 0;
|
|
132
|
+
const launcherSelectedApp = useMemo(
|
|
133
|
+
() => selectedAppSlug ? resources.find((r) => r.slug === selectedAppSlug) ?? null : null,
|
|
134
|
+
[selectedAppSlug, resources]
|
|
135
|
+
);
|
|
128
136
|
const handleClearFilters = () => {
|
|
129
137
|
setSearchValue("");
|
|
130
138
|
actions.clearAllFilters();
|
|
@@ -141,6 +149,32 @@ function AppCatalogPage({
|
|
|
141
149
|
return /* @__PURE__ */ jsx("div", { className: "py-6 text-muted-foreground", children: "Loading…" });
|
|
142
150
|
}
|
|
143
151
|
const groupingDefinition = tagsDefinitions[0];
|
|
152
|
+
if (showLauncherHome) {
|
|
153
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex-1 min-h-0 overflow-y-auto", children: [
|
|
154
|
+
/* @__PURE__ */ jsx(
|
|
155
|
+
LauncherHome,
|
|
156
|
+
{
|
|
157
|
+
apps: rootResources.filter(
|
|
158
|
+
(a) => filterState.showDeprecated || !a.deprecated
|
|
159
|
+
),
|
|
160
|
+
allResources: resources,
|
|
161
|
+
searchValue,
|
|
162
|
+
onSearchChange: setSearchValue,
|
|
163
|
+
onAppClick: handleAppClick,
|
|
164
|
+
onLaunch: handleLaunch,
|
|
165
|
+
totalCount: totalAppsCount
|
|
166
|
+
}
|
|
167
|
+
),
|
|
168
|
+
launcherSelectedApp && /* @__PURE__ */ jsx(
|
|
169
|
+
LauncherDetailPanel,
|
|
170
|
+
{
|
|
171
|
+
app: launcherSelectedApp,
|
|
172
|
+
onClose: () => void navigate({ to: "/" }),
|
|
173
|
+
onAppClick: handleAppClick
|
|
174
|
+
}
|
|
175
|
+
)
|
|
176
|
+
] });
|
|
177
|
+
}
|
|
144
178
|
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col flex-1 min-h-0", children: [
|
|
145
179
|
/* @__PURE__ */ jsx("div", { className: "shrink-0", children: /* @__PURE__ */ jsx(OnboardingCard, {}) }),
|
|
146
180
|
/* @__PURE__ */ jsx("div", { className: "shrink-0", children: /* @__PURE__ */ jsx(
|