@igstack/app-catalog-frontend-core 0.1.1-alpha-20260304050203 → 0.2.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/api/infra/trpc.d.ts +0 -21
- package/dist/esm/modules/appCatalog/ui/filters/FilterBar.js +11 -23
- package/dist/esm/modules/appCatalog/ui/filters/FilterBar.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.d.ts +1 -5
- package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js +56 -146
- package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js +1 -20
- package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js.map +1 -1
- package/package.json +3 -3
- package/dist/esm/ui/input-group.js +0 -125
- package/dist/esm/ui/input-group.js.map +0 -1
|
@@ -228,7 +228,6 @@ declare const TRPCProvider: import('react').FunctionComponent<{
|
|
|
228
228
|
links: PrismaJson.AppLink[] | null;
|
|
229
229
|
iconName: string | null;
|
|
230
230
|
screenshotIds: string[];
|
|
231
|
-
sources: string[];
|
|
232
231
|
}[];
|
|
233
232
|
meta: object;
|
|
234
233
|
}>;
|
|
@@ -253,7 +252,6 @@ declare const TRPCProvider: import('react').FunctionComponent<{
|
|
|
253
252
|
links: PrismaJson.AppLink[] | null;
|
|
254
253
|
iconName: string | null;
|
|
255
254
|
screenshotIds: string[];
|
|
256
|
-
sources: string[];
|
|
257
255
|
} | null;
|
|
258
256
|
meta: object;
|
|
259
257
|
}>;
|
|
@@ -278,7 +276,6 @@ declare const TRPCProvider: import('react').FunctionComponent<{
|
|
|
278
276
|
links: PrismaJson.AppLink[] | null;
|
|
279
277
|
iconName: string | null;
|
|
280
278
|
screenshotIds: string[];
|
|
281
|
-
sources: string[];
|
|
282
279
|
} | null;
|
|
283
280
|
meta: object;
|
|
284
281
|
}>;
|
|
@@ -338,7 +335,6 @@ declare const TRPCProvider: import('react').FunctionComponent<{
|
|
|
338
335
|
links: PrismaJson.AppLink[] | null;
|
|
339
336
|
iconName: string | null;
|
|
340
337
|
screenshotIds: string[];
|
|
341
|
-
sources: string[];
|
|
342
338
|
};
|
|
343
339
|
meta: object;
|
|
344
340
|
}>;
|
|
@@ -399,7 +395,6 @@ declare const TRPCProvider: import('react').FunctionComponent<{
|
|
|
399
395
|
links: PrismaJson.AppLink[] | null;
|
|
400
396
|
iconName: string | null;
|
|
401
397
|
screenshotIds: string[];
|
|
402
|
-
sources: string[];
|
|
403
398
|
};
|
|
404
399
|
meta: object;
|
|
405
400
|
}>;
|
|
@@ -425,7 +420,6 @@ declare const TRPCProvider: import('react').FunctionComponent<{
|
|
|
425
420
|
links: PrismaJson.AppLink[] | null;
|
|
426
421
|
iconName: string | null;
|
|
427
422
|
screenshotIds: string[];
|
|
428
|
-
sources: string[];
|
|
429
423
|
};
|
|
430
424
|
meta: object;
|
|
431
425
|
}>;
|
|
@@ -450,7 +444,6 @@ declare const TRPCProvider: import('react').FunctionComponent<{
|
|
|
450
444
|
links: PrismaJson.AppLink[] | null;
|
|
451
445
|
iconName: string | null;
|
|
452
446
|
screenshotIds: string[];
|
|
453
|
-
sources: string[];
|
|
454
447
|
};
|
|
455
448
|
meta: object;
|
|
456
449
|
}>;
|
|
@@ -783,7 +776,6 @@ declare const TRPCProvider: import('react').FunctionComponent<{
|
|
|
783
776
|
links: PrismaJson.AppLink[] | null;
|
|
784
777
|
iconName: string | null;
|
|
785
778
|
screenshotIds: string[];
|
|
786
|
-
sources: string[];
|
|
787
779
|
}[];
|
|
788
780
|
meta: object;
|
|
789
781
|
}>;
|
|
@@ -808,7 +800,6 @@ declare const TRPCProvider: import('react').FunctionComponent<{
|
|
|
808
800
|
links: PrismaJson.AppLink[] | null;
|
|
809
801
|
iconName: string | null;
|
|
810
802
|
screenshotIds: string[];
|
|
811
|
-
sources: string[];
|
|
812
803
|
} | null;
|
|
813
804
|
meta: object;
|
|
814
805
|
}>;
|
|
@@ -833,7 +824,6 @@ declare const TRPCProvider: import('react').FunctionComponent<{
|
|
|
833
824
|
links: PrismaJson.AppLink[] | null;
|
|
834
825
|
iconName: string | null;
|
|
835
826
|
screenshotIds: string[];
|
|
836
|
-
sources: string[];
|
|
837
827
|
} | null;
|
|
838
828
|
meta: object;
|
|
839
829
|
}>;
|
|
@@ -893,7 +883,6 @@ declare const TRPCProvider: import('react').FunctionComponent<{
|
|
|
893
883
|
links: PrismaJson.AppLink[] | null;
|
|
894
884
|
iconName: string | null;
|
|
895
885
|
screenshotIds: string[];
|
|
896
|
-
sources: string[];
|
|
897
886
|
};
|
|
898
887
|
meta: object;
|
|
899
888
|
}>;
|
|
@@ -954,7 +943,6 @@ declare const TRPCProvider: import('react').FunctionComponent<{
|
|
|
954
943
|
links: PrismaJson.AppLink[] | null;
|
|
955
944
|
iconName: string | null;
|
|
956
945
|
screenshotIds: string[];
|
|
957
|
-
sources: string[];
|
|
958
946
|
};
|
|
959
947
|
meta: object;
|
|
960
948
|
}>;
|
|
@@ -980,7 +968,6 @@ declare const TRPCProvider: import('react').FunctionComponent<{
|
|
|
980
968
|
links: PrismaJson.AppLink[] | null;
|
|
981
969
|
iconName: string | null;
|
|
982
970
|
screenshotIds: string[];
|
|
983
|
-
sources: string[];
|
|
984
971
|
};
|
|
985
972
|
meta: object;
|
|
986
973
|
}>;
|
|
@@ -1005,7 +992,6 @@ declare const TRPCProvider: import('react').FunctionComponent<{
|
|
|
1005
992
|
links: PrismaJson.AppLink[] | null;
|
|
1006
993
|
iconName: string | null;
|
|
1007
994
|
screenshotIds: string[];
|
|
1008
|
-
sources: string[];
|
|
1009
995
|
};
|
|
1010
996
|
meta: object;
|
|
1011
997
|
}>;
|
|
@@ -1337,7 +1323,6 @@ declare const TRPCProvider: import('react').FunctionComponent<{
|
|
|
1337
1323
|
links: PrismaJson.AppLink[] | null;
|
|
1338
1324
|
iconName: string | null;
|
|
1339
1325
|
screenshotIds: string[];
|
|
1340
|
-
sources: string[];
|
|
1341
1326
|
}[];
|
|
1342
1327
|
meta: object;
|
|
1343
1328
|
}>;
|
|
@@ -1362,7 +1347,6 @@ declare const TRPCProvider: import('react').FunctionComponent<{
|
|
|
1362
1347
|
links: PrismaJson.AppLink[] | null;
|
|
1363
1348
|
iconName: string | null;
|
|
1364
1349
|
screenshotIds: string[];
|
|
1365
|
-
sources: string[];
|
|
1366
1350
|
} | null;
|
|
1367
1351
|
meta: object;
|
|
1368
1352
|
}>;
|
|
@@ -1387,7 +1371,6 @@ declare const TRPCProvider: import('react').FunctionComponent<{
|
|
|
1387
1371
|
links: PrismaJson.AppLink[] | null;
|
|
1388
1372
|
iconName: string | null;
|
|
1389
1373
|
screenshotIds: string[];
|
|
1390
|
-
sources: string[];
|
|
1391
1374
|
} | null;
|
|
1392
1375
|
meta: object;
|
|
1393
1376
|
}>;
|
|
@@ -1447,7 +1430,6 @@ declare const TRPCProvider: import('react').FunctionComponent<{
|
|
|
1447
1430
|
links: PrismaJson.AppLink[] | null;
|
|
1448
1431
|
iconName: string | null;
|
|
1449
1432
|
screenshotIds: string[];
|
|
1450
|
-
sources: string[];
|
|
1451
1433
|
};
|
|
1452
1434
|
meta: object;
|
|
1453
1435
|
}>;
|
|
@@ -1508,7 +1490,6 @@ declare const TRPCProvider: import('react').FunctionComponent<{
|
|
|
1508
1490
|
links: PrismaJson.AppLink[] | null;
|
|
1509
1491
|
iconName: string | null;
|
|
1510
1492
|
screenshotIds: string[];
|
|
1511
|
-
sources: string[];
|
|
1512
1493
|
};
|
|
1513
1494
|
meta: object;
|
|
1514
1495
|
}>;
|
|
@@ -1534,7 +1515,6 @@ declare const TRPCProvider: import('react').FunctionComponent<{
|
|
|
1534
1515
|
links: PrismaJson.AppLink[] | null;
|
|
1535
1516
|
iconName: string | null;
|
|
1536
1517
|
screenshotIds: string[];
|
|
1537
|
-
sources: string[];
|
|
1538
1518
|
};
|
|
1539
1519
|
meta: object;
|
|
1540
1520
|
}>;
|
|
@@ -1559,7 +1539,6 @@ declare const TRPCProvider: import('react').FunctionComponent<{
|
|
|
1559
1539
|
links: PrismaJson.AppLink[] | null;
|
|
1560
1540
|
iconName: string | null;
|
|
1561
1541
|
screenshotIds: string[];
|
|
1562
|
-
sources: string[];
|
|
1563
1542
|
};
|
|
1564
1543
|
meta: object;
|
|
1565
1544
|
}>;
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
-
import { X } from "lucide-react";
|
|
3
2
|
import { useMemo } from "react";
|
|
4
3
|
import { Button } from "../../../../ui/button.js";
|
|
5
4
|
import { Checkbox } from "../../../../ui/checkbox.js";
|
|
6
|
-
import {
|
|
5
|
+
import { Input } from "../../../../ui/input.js";
|
|
7
6
|
import { Label } from "../../../../ui/label.js";
|
|
8
7
|
import { useAppCatalogFilters } from "../context/AppCatalogFiltersContext.js";
|
|
9
8
|
import { FilterCombobox } from "./FilterCombobox.js";
|
|
@@ -41,27 +40,16 @@ function FilterBar({
|
|
|
41
40
|
data.availableTagsByPrefix
|
|
42
41
|
]);
|
|
43
42
|
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 mb-4", children: [
|
|
44
|
-
/* @__PURE__ */
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
),
|
|
55
|
-
state.searchValue && /* @__PURE__ */ jsx(InputGroupAddon, { align: "inline-end", children: /* @__PURE__ */ jsx(
|
|
56
|
-
InputGroupButton,
|
|
57
|
-
{
|
|
58
|
-
size: "icon-xs",
|
|
59
|
-
onClick: () => actions.setSearchValue(""),
|
|
60
|
-
"aria-label": "Clear search",
|
|
61
|
-
children: /* @__PURE__ */ jsx(X, {})
|
|
62
|
-
}
|
|
63
|
-
) })
|
|
64
|
-
] }),
|
|
43
|
+
/* @__PURE__ */ jsx(
|
|
44
|
+
Input,
|
|
45
|
+
{
|
|
46
|
+
value: state.searchValue,
|
|
47
|
+
onChange: (e) => actions.setSearchValue(e.target.value),
|
|
48
|
+
placeholder: "Search apps by name, description, or tags…",
|
|
49
|
+
"aria-label": "Search apps",
|
|
50
|
+
className: "max-w-sm"
|
|
51
|
+
}
|
|
52
|
+
),
|
|
65
53
|
/* @__PURE__ */ jsx("div", { className: "h-8 w-px bg-border" }),
|
|
66
54
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center rounded-md border", children: [
|
|
67
55
|
/* @__PURE__ */ jsxs(
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FilterBar.js","sources":["../../../../../../src/modules/appCatalog/ui/filters/FilterBar.tsx"],"sourcesContent":["import type { AppForCatalog } from '@igstack/app-catalog-backend-core'\nimport {
|
|
1
|
+
{"version":3,"file":"FilterBar.js","sources":["../../../../../../src/modules/appCatalog/ui/filters/FilterBar.tsx"],"sourcesContent":["import type { AppForCatalog } from '@igstack/app-catalog-backend-core'\nimport { useMemo } from 'react'\nimport { Button } from '~/ui/button'\nimport { Checkbox } from '~/ui/checkbox'\nimport { Input } from '~/ui/input'\nimport { Label } from '~/ui/label'\nimport { useAppCatalogFilters } from '../context/AppCatalogFiltersContext'\nimport { FilterCombobox } from './FilterCombobox'\n\ninterface FilterBarProps {\n /** Total number of apps (respecting deprecated filter) */\n totalCount: number\n /** Number of apps in \"My Recent\" */\n recentCount: number\n /** Number of deprecated apps (total) */\n deprecatedCount: number\n /** All apps for counting filter options */\n apps: Array<AppForCatalog>\n}\n\n/**\n * Horizontal filter bar with All/My Recent toggle, dynamic tag filter comboboxes, and search.\n * Filters are mutually exclusive: Recent clears tag filters, tag filters clear Recent.\n * All discovery controls are grouped together.\n */\nexport function FilterBar({\n totalCount,\n recentCount,\n deprecatedCount,\n apps,\n}: FilterBarProps) {\n const { state, data, actions } = useAppCatalogFilters()\n\n // Check if \"Show All\" mode is truly active (no filters at all)\n const isShowAllActive =\n !state.recentMode && Object.keys(state.tagFilters).length === 0\n\n // Calculate counts for each filter option (respecting showDeprecated)\n const filterOptionCounts = useMemo(() => {\n const counts: Record<string, Record<string, number>> = {}\n\n // Filter apps by deprecated setting first\n const baseApps = state.showDeprecated\n ? apps\n : apps.filter((app) => !app.deprecated)\n\n state.filterableTagPrefixes.forEach((prefix) => {\n const prefixCounts: Record<string, number> = {}\n const options = data.availableTagsByPrefix[prefix] || []\n\n options.forEach((option) => {\n const fullTag = `${prefix}:${option.value}`\n const count = baseApps.filter((app) =>\n app.tags?.some((tag) => tag.toLowerCase() === fullTag.toLowerCase()),\n ).length\n prefixCounts[option.value] = count\n })\n\n counts[prefix] = prefixCounts\n })\n\n return counts\n }, [\n apps,\n state.showDeprecated,\n state.filterableTagPrefixes,\n data.availableTagsByPrefix,\n ])\n\n return (\n <div className=\"flex items-center gap-3 mb-4\">\n {/* Search input */}\n <Input\n value={state.searchValue}\n onChange={(e) => actions.setSearchValue(e.target.value)}\n placeholder=\"Search apps by name, description, or tags…\"\n aria-label=\"Search apps\"\n className=\"max-w-sm\"\n />\n\n {/* Vertical divider */}\n <div className=\"h-8 w-px bg-border\" />\n\n {/* Show All / My Recent toggle group */}\n <div className=\"flex items-center rounded-md border\">\n <Button\n variant={isShowAllActive ? 'default' : 'ghost'}\n size=\"sm\"\n onClick={() => actions.setRecentMode(false)}\n className=\"rounded-r-none border-r\"\n >\n Show All ({totalCount})\n </Button>\n <Button\n variant={state.recentMode ? 'default' : 'ghost'}\n size=\"sm\"\n onClick={() => actions.setRecentMode(true)}\n className=\"rounded-l-none\"\n disabled={recentCount === 0}\n >\n My Recent ({recentCount})\n </Button>\n </div>\n\n {/* Vertical divider */}\n {state.filterableTagPrefixes.length > 0 && (\n <div className=\"h-8 w-px bg-border\" />\n )}\n\n {/* Dynamic tag filter comboboxes */}\n {state.filterableTagPrefixes.map((prefix) => {\n const options = data.availableTagsByPrefix[prefix] || []\n const value = state.tagFilters[prefix]\n const counts = filterOptionCounts[prefix] || {}\n\n // Create \"Filter By <Name>\" label\n const displayName =\n prefix.charAt(0).toUpperCase() + prefix.slice(1).replace(/-/g, ' ')\n const label = `Filter By ${displayName}`\n\n return (\n <FilterCombobox\n key={prefix}\n prefix={prefix}\n label={label}\n options={options}\n value={value}\n counts={counts}\n onValueChange={(newValue) => actions.setTagFilter(prefix, newValue)}\n />\n )\n })}\n\n {/* Vertical divider before deprecated checkbox */}\n <div className=\"h-8 w-px bg-border\" />\n\n {/* Show Deprecated Apps checkbox */}\n <div className=\"flex items-center gap-2\">\n <Checkbox\n id=\"show-deprecated\"\n checked={state.showDeprecated}\n onCheckedChange={(checked) =>\n actions.setShowDeprecated(checked === true)\n }\n />\n <Label\n htmlFor=\"show-deprecated\"\n className=\"text-sm font-normal cursor-pointer\"\n >\n Show Deprecated Apps ({deprecatedCount})\n </Label>\n </div>\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -6,9 +6,5 @@ export interface AppCatalogGridProps {
|
|
|
6
6
|
onAppClick?: (app: AppForCatalog) => void;
|
|
7
7
|
/** Whether search is active (affects group sorting) */
|
|
8
8
|
hasSearch?: boolean;
|
|
9
|
-
/** Total count of apps before filtering */
|
|
10
|
-
totalAppsCount?: number;
|
|
11
|
-
/** Callback to clear all filters and search */
|
|
12
|
-
onClearFilters?: () => void;
|
|
13
9
|
}
|
|
14
|
-
export declare function AppCatalogGrid({ apps, selectedAppSlug, groupingDefinition, onAppClick, hasSearch,
|
|
10
|
+
export declare function AppCatalogGrid({ apps, selectedAppSlug, groupingDefinition, onAppClick, hasSearch, }: AppCatalogGridProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -2,7 +2,6 @@ import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
|
2
2
|
import { useReactTable, getCoreRowModel, flexRender } from "@tanstack/react-table";
|
|
3
3
|
import { X, AppWindow, ExternalLink } from "lucide-react";
|
|
4
4
|
import React__default, { useState, useEffect } from "react";
|
|
5
|
-
import { useHotkeys } from "react-hotkeys-hook";
|
|
6
5
|
import { cn } from "../../../../lib/utils.js";
|
|
7
6
|
import { Badge } from "../../../../ui/badge.js";
|
|
8
7
|
import { Button } from "../../../../ui/button.js";
|
|
@@ -71,37 +70,13 @@ function AppScreenshot({ app }) {
|
|
|
71
70
|
}
|
|
72
71
|
function AppDetails({
|
|
73
72
|
app,
|
|
74
|
-
onAppClick
|
|
75
|
-
onClosePanel
|
|
73
|
+
onAppClick
|
|
76
74
|
}) {
|
|
77
75
|
var _a;
|
|
78
76
|
const [isGalleryOpen, setIsGalleryOpen] = React__default.useState(false);
|
|
79
77
|
const [galleryInitialIndex, setGalleryInitialIndex] = React__default.useState(0);
|
|
80
78
|
const { approvalMethods, apps } = useAppCatalogContext();
|
|
81
79
|
const { recordClick } = useAppClickHistory();
|
|
82
|
-
useHotkeys(
|
|
83
|
-
"enter",
|
|
84
|
-
() => {
|
|
85
|
-
var _a2;
|
|
86
|
-
const tag = (_a2 = document.activeElement) == null ? void 0 : _a2.tagName;
|
|
87
|
-
if (tag === "BUTTON" || tag === "A" || tag === "INPUT" || tag === "SELECT" || tag === "TEXTAREA")
|
|
88
|
-
return;
|
|
89
|
-
if (app.screenshotIds && app.screenshotIds.length > 0) {
|
|
90
|
-
setGalleryInitialIndex(0);
|
|
91
|
-
setIsGalleryOpen(true);
|
|
92
|
-
}
|
|
93
|
-
},
|
|
94
|
-
{ enabled: !isGalleryOpen },
|
|
95
|
-
[app, isGalleryOpen]
|
|
96
|
-
);
|
|
97
|
-
useHotkeys(
|
|
98
|
-
"escape",
|
|
99
|
-
() => {
|
|
100
|
-
onClosePanel();
|
|
101
|
-
},
|
|
102
|
-
{ enabled: !isGalleryOpen },
|
|
103
|
-
[isGalleryOpen, onClosePanel]
|
|
104
|
-
);
|
|
105
80
|
const handleScreenshotClick = (index) => {
|
|
106
81
|
setGalleryInitialIndex(index);
|
|
107
82
|
setIsGalleryOpen(true);
|
|
@@ -112,10 +87,10 @@ function AppDetails({
|
|
|
112
87
|
}) : null;
|
|
113
88
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
114
89
|
/* @__PURE__ */ jsxs("div", { className: "flex h-full flex-col p-6", children: [
|
|
115
|
-
/* @__PURE__ */
|
|
90
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-4 border-b pb-6", children: [
|
|
116
91
|
/* @__PURE__ */ jsx(AppIcon, { app, className: "size-16" }),
|
|
117
|
-
/* @__PURE__ */ jsxs("div", {
|
|
118
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2
|
|
92
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
93
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
119
94
|
/* @__PURE__ */ jsx("h2", { className: "text-2xl font-semibold", children: app.displayName }),
|
|
120
95
|
app.deprecated && (() => {
|
|
121
96
|
const deprecationType = app.deprecated.type || "deprecated";
|
|
@@ -135,15 +110,15 @@ function AppDetails({
|
|
|
135
110
|
target: "_blank",
|
|
136
111
|
rel: "noopener noreferrer",
|
|
137
112
|
onClick: () => recordClick(app.slug),
|
|
138
|
-
className: "mt-1 inline-flex items-center gap-1
|
|
113
|
+
className: "mt-1 inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-primary",
|
|
139
114
|
children: [
|
|
140
115
|
app.appUrl.replaceAll(/https?:\/\//g, ""),
|
|
141
|
-
/* @__PURE__ */ jsx(ExternalLink, { className: "size-3
|
|
116
|
+
/* @__PURE__ */ jsx(ExternalLink, { className: "size-3" })
|
|
142
117
|
]
|
|
143
118
|
}
|
|
144
119
|
)
|
|
145
120
|
] })
|
|
146
|
-
] })
|
|
121
|
+
] }),
|
|
147
122
|
app.deprecated && (() => {
|
|
148
123
|
const deprecationType = app.deprecated.type || "deprecated";
|
|
149
124
|
const isDiscouraged = deprecationType === "discouraged";
|
|
@@ -203,23 +178,6 @@ function AppDetails({
|
|
|
203
178
|
)
|
|
204
179
|
] }),
|
|
205
180
|
/* @__PURE__ */ jsx(AccessRequestSection, { app, approvalMethods }),
|
|
206
|
-
app.links && app.links.length > 0 && /* @__PURE__ */ jsxs("div", { className: "mt-4", children: [
|
|
207
|
-
/* @__PURE__ */ jsx("h3", { className: "mb-1 text-xs font-medium text-muted-foreground", children: "Links" }),
|
|
208
|
-
/* @__PURE__ */ jsx("div", { className: "space-y-0.5", children: app.links.map((link) => /* @__PURE__ */ jsxs(
|
|
209
|
-
"a",
|
|
210
|
-
{
|
|
211
|
-
href: link.url,
|
|
212
|
-
target: "_blank",
|
|
213
|
-
rel: "noopener noreferrer",
|
|
214
|
-
className: "flex items-center gap-1 text-xs text-muted-foreground hover:text-primary truncate",
|
|
215
|
-
children: [
|
|
216
|
-
/* @__PURE__ */ jsx(ExternalLink, { className: "size-3 shrink-0" }),
|
|
217
|
-
link.title || link.url.replaceAll(/https?:\/\//g, "")
|
|
218
|
-
]
|
|
219
|
-
},
|
|
220
|
-
link.url
|
|
221
|
-
)) })
|
|
222
|
-
] }),
|
|
223
181
|
app.tags && app.tags.length > 0 && /* @__PURE__ */ jsxs("div", { className: "mt-6", children: [
|
|
224
182
|
/* @__PURE__ */ jsx("h3", { className: "mb-2 text-sm font-medium", children: "Tags" }),
|
|
225
183
|
/* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2", children: app.tags.map((tag) => /* @__PURE__ */ jsx(Badge, { variant: "secondary", className: "text-xs", children: tag }, tag)) })
|
|
@@ -227,22 +185,6 @@ function AppDetails({
|
|
|
227
185
|
app.teams && app.teams.length > 0 && /* @__PURE__ */ jsxs("div", { className: "mt-6", children: [
|
|
228
186
|
/* @__PURE__ */ jsx("h3", { className: "mb-2 text-sm font-medium", children: "Teams" }),
|
|
229
187
|
/* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2", children: app.teams.map((team) => /* @__PURE__ */ jsx(Badge, { variant: "outline", className: "text-xs", children: team }, team)) })
|
|
230
|
-
] }),
|
|
231
|
-
app.sources && app.sources.length > 0 && /* @__PURE__ */ jsxs("div", { className: "mt-6", children: [
|
|
232
|
-
/* @__PURE__ */ jsx("h3", { className: "mb-2 text-sm font-medium", children: "Sources" }),
|
|
233
|
-
/* @__PURE__ */ jsx("ol", { className: "list-decimal list-inside space-y-1", children: app.sources.map((source, index) => /* @__PURE__ */ jsx("li", { className: "text-xs text-muted-foreground", children: /* @__PURE__ */ jsxs(
|
|
234
|
-
"a",
|
|
235
|
-
{
|
|
236
|
-
href: source,
|
|
237
|
-
target: "_blank",
|
|
238
|
-
rel: "noopener noreferrer",
|
|
239
|
-
className: "hover:text-primary inline-flex items-center gap-1",
|
|
240
|
-
children: [
|
|
241
|
-
source.replaceAll(/https?:\/\//g, ""),
|
|
242
|
-
/* @__PURE__ */ jsx(ExternalLink, { className: "size-3 shrink-0" })
|
|
243
|
-
]
|
|
244
|
-
}
|
|
245
|
-
) }, index)) })
|
|
246
188
|
] })
|
|
247
189
|
] }),
|
|
248
190
|
/* @__PURE__ */ jsx(
|
|
@@ -311,9 +253,7 @@ function AppCatalogGrid({
|
|
|
311
253
|
selectedAppSlug,
|
|
312
254
|
groupingDefinition,
|
|
313
255
|
onAppClick,
|
|
314
|
-
hasSearch = false
|
|
315
|
-
totalAppsCount,
|
|
316
|
-
onClearFilters
|
|
256
|
+
hasSearch = false
|
|
317
257
|
}) {
|
|
318
258
|
const selectedApp = selectedAppSlug ? apps.find((a) => a.slug === selectedAppSlug) : null;
|
|
319
259
|
const groupedApps = groupApps(apps, groupingDefinition, hasSearch);
|
|
@@ -410,78 +350,55 @@ function AppCatalogGrid({
|
|
|
410
350
|
header.id
|
|
411
351
|
);
|
|
412
352
|
}) }, headerGroup.id)) }),
|
|
413
|
-
/* @__PURE__ */
|
|
414
|
-
|
|
415
|
-
/* @__PURE__ */ jsx(TableRow, { className: "bg-muted/50 hover:bg-muted/50", children: /* @__PURE__ */ jsx(
|
|
416
|
-
TableCell,
|
|
417
|
-
{
|
|
418
|
-
colSpan: columns.length,
|
|
419
|
-
className: "px-4 py-6 sticky top-[49px] bg-muted/90 backdrop-blur z-10",
|
|
420
|
-
children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: "font-bold text-lg tracking-widest uppercase leading-loose text-muted-foreground", children: group.groupName }) })
|
|
421
|
-
}
|
|
422
|
-
) }),
|
|
423
|
-
group.apps.map((app) => {
|
|
424
|
-
const row = table.getRowModel().rows.find((r) => r.id === app.id);
|
|
425
|
-
if (!row) return null;
|
|
426
|
-
return /* @__PURE__ */ jsx(
|
|
427
|
-
TableRow,
|
|
428
|
-
{
|
|
429
|
-
ref: (el) => {
|
|
430
|
-
if (el && row.original.slug) {
|
|
431
|
-
rowRefs.current.set(row.original.slug, el);
|
|
432
|
-
} else if (row.original.slug) {
|
|
433
|
-
rowRefs.current.delete(row.original.slug);
|
|
434
|
-
}
|
|
435
|
-
},
|
|
436
|
-
onClick: () => handleAppClick(row.original),
|
|
437
|
-
className: cn(
|
|
438
|
-
"border-b cursor-pointer transition-colors",
|
|
439
|
-
(selectedApp == null ? void 0 : selectedApp.id) === row.original.id ? "bg-blue-100 dark:bg-blue-950 hover:bg-blue-200 dark:hover:bg-blue-900" : "hover:bg-muted/30"
|
|
440
|
-
),
|
|
441
|
-
children: row.getVisibleCells().map((cell) => {
|
|
442
|
-
var _a;
|
|
443
|
-
return /* @__PURE__ */ jsx(
|
|
444
|
-
TableCell,
|
|
445
|
-
{
|
|
446
|
-
className: cn(
|
|
447
|
-
"px-4 py-4",
|
|
448
|
-
(_a = cell.column.columnDef.meta) == null ? void 0 : _a.className
|
|
449
|
-
),
|
|
450
|
-
children: flexRender(
|
|
451
|
-
cell.column.columnDef.cell,
|
|
452
|
-
cell.getContext()
|
|
453
|
-
)
|
|
454
|
-
},
|
|
455
|
-
cell.id
|
|
456
|
-
);
|
|
457
|
-
})
|
|
458
|
-
},
|
|
459
|
-
row.id
|
|
460
|
-
);
|
|
461
|
-
})
|
|
462
|
-
] }, group.groupName)),
|
|
463
|
-
totalAppsCount && totalAppsCount > apps.length && onClearFilters && /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(
|
|
353
|
+
/* @__PURE__ */ jsx(TableBody, { children: groupedApps.map((group) => /* @__PURE__ */ jsxs(React__default.Fragment, { children: [
|
|
354
|
+
/* @__PURE__ */ jsx(TableRow, { className: "bg-muted/50 hover:bg-muted/50", children: /* @__PURE__ */ jsx(
|
|
464
355
|
TableCell,
|
|
465
356
|
{
|
|
466
357
|
colSpan: columns.length,
|
|
467
|
-
className: "px-4 py-
|
|
468
|
-
children: /* @__PURE__ */
|
|
469
|
-
Button,
|
|
470
|
-
{
|
|
471
|
-
variant: "outline",
|
|
472
|
-
onClick: onClearFilters,
|
|
473
|
-
className: "gap-2",
|
|
474
|
-
children: [
|
|
475
|
-
/* @__PURE__ */ jsx(X, { className: "h-4 w-4" }),
|
|
476
|
-
"Clear filters to show all apps (",
|
|
477
|
-
totalAppsCount,
|
|
478
|
-
")"
|
|
479
|
-
]
|
|
480
|
-
}
|
|
481
|
-
)
|
|
358
|
+
className: "px-4 py-6 sticky top-[49px] bg-muted/90 backdrop-blur z-10",
|
|
359
|
+
children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: "font-bold text-lg tracking-widest uppercase leading-loose text-muted-foreground", children: group.groupName }) })
|
|
482
360
|
}
|
|
483
|
-
) })
|
|
484
|
-
|
|
361
|
+
) }),
|
|
362
|
+
group.apps.map((app) => {
|
|
363
|
+
const row = table.getRowModel().rows.find((r) => r.id === app.id);
|
|
364
|
+
if (!row) return null;
|
|
365
|
+
return /* @__PURE__ */ jsx(
|
|
366
|
+
TableRow,
|
|
367
|
+
{
|
|
368
|
+
ref: (el) => {
|
|
369
|
+
if (el && row.original.slug) {
|
|
370
|
+
rowRefs.current.set(row.original.slug, el);
|
|
371
|
+
} else if (row.original.slug) {
|
|
372
|
+
rowRefs.current.delete(row.original.slug);
|
|
373
|
+
}
|
|
374
|
+
},
|
|
375
|
+
onClick: () => handleAppClick(row.original),
|
|
376
|
+
className: cn(
|
|
377
|
+
"border-b cursor-pointer transition-colors",
|
|
378
|
+
(selectedApp == null ? void 0 : selectedApp.id) === row.original.id ? "bg-blue-100 dark:bg-blue-950 hover:bg-blue-200 dark:hover:bg-blue-900" : "hover:bg-muted/30"
|
|
379
|
+
),
|
|
380
|
+
children: row.getVisibleCells().map((cell) => {
|
|
381
|
+
var _a;
|
|
382
|
+
return /* @__PURE__ */ jsx(
|
|
383
|
+
TableCell,
|
|
384
|
+
{
|
|
385
|
+
className: cn(
|
|
386
|
+
"px-4 py-4",
|
|
387
|
+
(_a = cell.column.columnDef.meta) == null ? void 0 : _a.className
|
|
388
|
+
),
|
|
389
|
+
children: flexRender(
|
|
390
|
+
cell.column.columnDef.cell,
|
|
391
|
+
cell.getContext()
|
|
392
|
+
)
|
|
393
|
+
},
|
|
394
|
+
cell.id
|
|
395
|
+
);
|
|
396
|
+
})
|
|
397
|
+
},
|
|
398
|
+
row.id
|
|
399
|
+
);
|
|
400
|
+
})
|
|
401
|
+
] }, group.groupName)) })
|
|
485
402
|
] }) })
|
|
486
403
|
}
|
|
487
404
|
),
|
|
@@ -499,20 +416,13 @@ function AppCatalogGrid({
|
|
|
499
416
|
{
|
|
500
417
|
variant: "ghost",
|
|
501
418
|
size: "icon",
|
|
502
|
-
className: "absolute top-
|
|
419
|
+
className: "absolute top-2 right-2 z-10",
|
|
503
420
|
onClick: handleClosePanel,
|
|
504
421
|
"aria-label": "Close details panel",
|
|
505
|
-
children: /* @__PURE__ */ jsx(X, { className: "h-
|
|
422
|
+
children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" })
|
|
506
423
|
}
|
|
507
424
|
),
|
|
508
|
-
/* @__PURE__ */ jsx(
|
|
509
|
-
AppDetails,
|
|
510
|
-
{
|
|
511
|
-
app: selectedApp,
|
|
512
|
-
onAppClick,
|
|
513
|
-
onClosePanel: handleClosePanel
|
|
514
|
-
}
|
|
515
|
-
)
|
|
425
|
+
/* @__PURE__ */ jsx(AppDetails, { app: selectedApp, onAppClick })
|
|
516
426
|
] }) : null })
|
|
517
427
|
}
|
|
518
428
|
)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AppCatalogGrid.js","sources":["../../../../../../src/modules/appCatalog/ui/grid/AppCatalogGrid.tsx"],"sourcesContent":["import type {\n AppForCatalog,\n GroupingTagDefinition,\n} from '@igstack/app-catalog-backend-core'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport {\n flexRender,\n getCoreRowModel,\n useReactTable,\n} from '@tanstack/react-table'\nimport { AppWindow, ExternalLink, X } from 'lucide-react'\nimport React, { useEffect, useState } from 'react'\nimport { useHotkeys } from 'react-hotkeys-hook'\n\nimport { cn } from '~/lib/utils'\nimport type {} from '~/types/table'\nimport { Badge } from '~/ui/badge'\nimport { Button } from '~/ui/button'\nimport {\n ResizableHandle,\n ResizablePanel,\n ResizablePanelGroup,\n} from '~/ui/resizable'\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '~/ui/table'\nimport { AccessRequestSection } from '../components/AccessRequestSection'\nimport { ScreenshotGallery } from '../components/ScreenshotGallery'\nimport { useAppCatalogContext } from '../../context/AppCatalogContext'\nimport { useAppClickHistory } from '../../hooks/useAppClickHistory'\nimport { useKeyboardNavigation } from '../hooks/useKeyboardNavigation'\n\nexport interface AppCatalogGridProps {\n apps: Array<AppForCatalog>\n selectedAppSlug?: string\n groupingDefinition?: GroupingTagDefinition\n onAppClick?: (app: AppForCatalog) => void\n /** Whether search is active (affects group sorting) */\n hasSearch?: boolean\n /** Total count of apps before filtering */\n totalAppsCount?: number\n /** Callback to clear all filters and search */\n onClearFilters?: () => void\n}\n\nfunction getIconUrl(iconName: string): string {\n return `/api/icons/${iconName}`\n}\n\nfunction AppIcon({\n app,\n className,\n}: {\n app: AppForCatalog\n className?: string\n}) {\n const [imageError, setImageError] = React.useState(false)\n\n // Use iconName from backend if available\n if (app.iconName && !imageError) {\n return (\n <div className={cn('size-12 shrink-0', className)}>\n <img\n src={getIconUrl(app.iconName)}\n alt={`${app.displayName} icon`}\n className=\"size-12 rounded-lg object-contain\"\n onError={() => setImageError(true)}\n />\n </div>\n )\n }\n\n // Fallback icon\n return (\n <div\n className={cn(\n 'flex items-center justify-center rounded-lg bg-primary/10 text-primary size-12 shrink-0',\n className,\n )}\n >\n <AppWindow className=\"size-6\" />\n </div>\n )\n}\n\nfunction AppScreenshot({ app }: { app: AppForCatalog }) {\n const [imageError, setImageError] = React.useState(false)\n const [isLoadingImage, setIsLoadingImage] = React.useState(true)\n\n // Check if app has screenshots\n const screenshotId = app.screenshotIds?.[0]\n if (!screenshotId) {\n return (\n <div className=\"w-full bg-muted/50 rounded-lg overflow-hidden flex items-center justify-center min-h-64\">\n <div className=\"w-full h-64 bg-muted/30 flex items-center justify-center text-muted-foreground text-sm\">\n No screenshot available\n </div>\n </div>\n )\n }\n\n const screenshotImageUrl = `/api/screenshots/${screenshotId}?size=512`\n\n return (\n <div className=\"w-full flex justify-center\">\n <div className=\"rounded-lg overflow-hidden inline-flex items-center justify-center min-h-64\">\n {!imageError ? (\n <img\n src={screenshotImageUrl}\n alt={`${app.displayName} screenshot`}\n className=\"h-64 object-contain\"\n onError={() => {\n setImageError(true)\n setIsLoadingImage(false)\n }}\n onLoad={() => setIsLoadingImage(false)}\n />\n ) : null}\n {(imageError || isLoadingImage) && (\n <div className=\"w-full h-64 bg-muted/30 flex items-center justify-center text-muted-foreground text-sm\">\n {isLoadingImage\n ? 'Loading screenshot...'\n : 'No screenshot available'}\n </div>\n )}\n </div>\n </div>\n )\n}\n\nfunction AppDetails({\n app,\n onAppClick,\n onClosePanel,\n}: {\n app: AppForCatalog\n onAppClick?: (app: AppForCatalog) => void\n onClosePanel: () => void\n}) {\n const [isGalleryOpen, setIsGalleryOpen] = React.useState(false)\n const [galleryInitialIndex, setGalleryInitialIndex] = React.useState(0)\n const { approvalMethods, apps } = useAppCatalogContext()\n const { recordClick } = useAppClickHistory()\n\n // Enter: open screenshot gallery\n useHotkeys(\n 'enter',\n () => {\n const tag = document.activeElement?.tagName\n if (\n tag === 'BUTTON' ||\n tag === 'A' ||\n tag === 'INPUT' ||\n tag === 'SELECT' ||\n tag === 'TEXTAREA'\n )\n return\n\n if (app.screenshotIds && app.screenshotIds.length > 0) {\n setGalleryInitialIndex(0)\n setIsGalleryOpen(true)\n }\n },\n { enabled: !isGalleryOpen },\n [app, isGalleryOpen],\n )\n\n // Esc: close the details panel (only when gallery is NOT open)\n useHotkeys(\n 'escape',\n () => {\n onClosePanel()\n },\n { enabled: !isGalleryOpen },\n [isGalleryOpen, onClosePanel],\n )\n\n const handleScreenshotClick = (index: number) => {\n setGalleryInitialIndex(index)\n setIsGalleryOpen(true)\n }\n\n // Find replacement app if deprecated\n const replacementApp = app.deprecated?.replacementSlug\n ? apps.find((a) => a.slug === app.deprecated?.replacementSlug)\n : null\n\n return (\n <>\n <div className=\"flex h-full flex-col p-6\">\n {/* Icon and Title */}\n <div className=\"border-b pb-6\">\n <div className=\"flex items-center gap-3\">\n <AppIcon app={app} className=\"size-16\" />\n <div className=\"-mx-3\">\n <div className=\"flex items-center gap-2 px-3\">\n <h2 className=\"text-2xl font-semibold\">{app.displayName}</h2>\n {app.deprecated &&\n (() => {\n const deprecationType = app.deprecated.type || 'deprecated'\n return (\n <Badge\n variant={\n deprecationType === 'discouraged'\n ? 'secondary'\n : 'destructive'\n }\n >\n {deprecationType === 'discouraged'\n ? 'Discouraged'\n : 'Deprecated'}\n </Badge>\n )\n })()}\n </div>\n {app.appUrl && (\n <a\n href={app.appUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n onClick={() => recordClick(app.slug)}\n className=\"mt-1 inline-flex items-center gap-1 rounded-md px-3 py-1 text-sm text-blue-600 hover:bg-accent/30 hover:underline dark:text-blue-400 transition-all group\"\n >\n {app.appUrl.replaceAll(/https?:\\/\\//g, '')}\n <ExternalLink className=\"size-3.5 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity\" />\n </a>\n )}\n </div>\n </div>\n </div>\n\n {/* Deprecation/Discouraged Warning */}\n {app.deprecated &&\n (() => {\n const deprecationType = app.deprecated.type || 'deprecated'\n const isDiscouraged = deprecationType === 'discouraged'\n return (\n <div\n className={\n isDiscouraged\n ? 'mt-6 p-4 border border-yellow-500/50 rounded-lg bg-yellow-50 dark:bg-yellow-950/20'\n : 'mt-6 p-4 border border-destructive/50 rounded-lg bg-destructive/10'\n }\n >\n <h3\n className={\n isDiscouraged\n ? 'text-sm font-semibold text-yellow-700 dark:text-yellow-500 mb-2'\n : 'text-sm font-semibold text-destructive mb-2'\n }\n >\n {isDiscouraged\n ? 'Usage discouraged'\n : 'This application is deprecated'}\n </h3>\n <p className=\"text-sm text-muted-foreground mb-3\">\n {app.deprecated.comment}\n </p>\n {replacementApp && (\n <button\n onClick={() => onAppClick?.(replacementApp)}\n className=\"text-sm font-medium text-primary hover:underline inline-flex items-center gap-1\"\n >\n View replacement: {replacementApp.displayName}\n <ExternalLink className=\"size-3\" />\n </button>\n )}\n </div>\n )\n })()}\n\n {/* Description */}\n {app.description && (\n <div className=\"mt-6\">\n <h3 className=\"mb-2 text-sm font-medium\">Description</h3>\n <p className=\"text-sm text-muted-foreground\">{app.description}</p>\n </div>\n )}\n\n {/* Screenshots - Clickable preview */}\n {app.screenshotIds && app.screenshotIds.length > 0 && (\n <div className=\"mt-6\">\n <h3 className=\"mb-2 text-sm font-medium\">\n Screenshots ({app.screenshotIds.length})\n </h3>\n <div\n className=\"cursor-pointer hover:opacity-80 transition-opacity\"\n onClick={() => handleScreenshotClick(0)}\n >\n <AppScreenshot app={app} />\n {app.screenshotIds.length > 1 && (\n <p className=\"text-xs text-muted-foreground mt-2 text-center\">\n Click to view all {app.screenshotIds.length} screenshots\n </p>\n )}\n </div>\n </div>\n )}\n\n {/* Access Request Section */}\n <AccessRequestSection app={app} approvalMethods={approvalMethods} />\n\n {/* Links */}\n {app.links && app.links.length > 0 && (\n <div className=\"mt-4\">\n <h3 className=\"mb-1 text-xs font-medium text-muted-foreground\">\n Links\n </h3>\n <div className=\"space-y-0.5\">\n {app.links.map((link) => (\n <a\n key={link.url}\n href={link.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"flex items-center gap-1 text-xs text-muted-foreground hover:text-primary truncate\"\n >\n <ExternalLink className=\"size-3 shrink-0\" />\n {link.title || link.url.replaceAll(/https?:\\/\\//g, '')}\n </a>\n ))}\n </div>\n </div>\n )}\n\n {/* Tags */}\n {app.tags && app.tags.length > 0 && (\n <div className=\"mt-6\">\n <h3 className=\"mb-2 text-sm font-medium\">Tags</h3>\n <div className=\"flex flex-wrap gap-2\">\n {app.tags.map((tag) => (\n <Badge key={tag} variant=\"secondary\" className=\"text-xs\">\n {tag}\n </Badge>\n ))}\n </div>\n </div>\n )}\n\n {/* Teams */}\n {app.teams && app.teams.length > 0 && (\n <div className=\"mt-6\">\n <h3 className=\"mb-2 text-sm font-medium\">Teams</h3>\n <div className=\"flex flex-wrap gap-2\">\n {app.teams.map((team) => (\n <Badge key={team} variant=\"outline\" className=\"text-xs\">\n {team}\n </Badge>\n ))}\n </div>\n </div>\n )}\n\n {/* Sources */}\n {app.sources && app.sources.length > 0 && (\n <div className=\"mt-6\">\n <h3 className=\"mb-2 text-sm font-medium\">Sources</h3>\n <ol className=\"list-decimal list-inside space-y-1\">\n {app.sources.map((source, index) => (\n <li key={index} className=\"text-xs text-muted-foreground\">\n <a\n href={source}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"hover:text-primary inline-flex items-center gap-1\"\n >\n {source.replaceAll(/https?:\\/\\//g, '')}\n <ExternalLink className=\"size-3 shrink-0\" />\n </a>\n </li>\n ))}\n </ol>\n </div>\n )}\n </div>\n\n {/* Screenshot Gallery Dialog */}\n <ScreenshotGallery\n app={app}\n screenshotIds={app.screenshotIds || []}\n open={isGalleryOpen}\n onOpenChange={setIsGalleryOpen}\n initialIndex={galleryInitialIndex}\n title={`${app.displayName} - Screenshots`}\n />\n </>\n )\n}\n\ninterface GroupedApps {\n groupName: string\n apps: Array<AppForCatalog>\n}\n\nfunction groupApps(\n apps: Array<AppForCatalog>,\n groupingDef?: GroupingTagDefinition,\n hasSearch?: boolean,\n): Array<GroupedApps> {\n if (!groupingDef) {\n const sortedApps = [...apps].sort((a, b) =>\n a.displayName.localeCompare(b.displayName),\n )\n return [{ groupName: 'All Apps', apps: sortedApps }]\n }\n\n const grouped = new Map<string, Array<AppForCatalog>>()\n const ungrouped: Array<AppForCatalog> = []\n\n for (const app of apps) {\n const matchingTag = app.tags?.find((tag) =>\n tag.startsWith(`${groupingDef.prefix}:`),\n )\n\n if (matchingTag) {\n const value = matchingTag.split(':')[1]\n if (value) {\n const tagValue = groupingDef.values.find((v) => v.value === value)\n const displayName = tagValue?.displayName || value\n\n if (!grouped.has(displayName)) {\n grouped.set(displayName, [])\n }\n grouped.get(displayName)!.push(app)\n } else {\n ungrouped.push(app)\n }\n } else {\n ungrouped.push(app)\n }\n }\n\n const result: Array<GroupedApps> = []\n for (const [groupName, appsInGroup] of grouped) {\n // Sort apps alphabetically within each group\n const sortedGroupApps = appsInGroup.sort((a, b) =>\n a.displayName.localeCompare(b.displayName),\n )\n result.push({ groupName, apps: sortedGroupApps })\n }\n\n if (ungrouped.length > 0) {\n // Sort ungrouped apps alphabetically\n const sortedUngrouped = ungrouped.sort((a, b) =>\n a.displayName.localeCompare(b.displayName),\n )\n result.push({ groupName: 'Other', apps: sortedUngrouped })\n }\n\n // Sort groups: when no search, sort by app count descending\n // When search is active, keep the order based on app relevance\n if (!hasSearch) {\n result.sort((a, b) => b.apps.length - a.apps.length)\n }\n\n return result\n}\n\nexport function AppCatalogGrid({\n apps,\n selectedAppSlug,\n groupingDefinition,\n onAppClick,\n hasSearch = false,\n totalAppsCount,\n onClearFilters,\n}: AppCatalogGridProps) {\n const selectedApp = selectedAppSlug\n ? apps.find((a) => a.slug === selectedAppSlug)\n : null\n\n const groupedApps = groupApps(apps, groupingDefinition, hasSearch)\n\n // Flatten grouped apps to get display order for keyboard navigation\n const appsInDisplayOrder = React.useMemo(\n () => groupedApps.flatMap((group) => group.apps),\n [groupedApps],\n )\n\n // Use keyboard navigation hook with apps in display order\n const { rowRefs } = useKeyboardNavigation({\n apps: appsInDisplayOrder,\n selectedAppSlug,\n onAppClick,\n })\n\n // Define columns\n const columns = React.useMemo<Array<ColumnDef<AppForCatalog>>>(\n () => [\n {\n id: 'application',\n header: 'Application',\n cell: ({ row }) => (\n <div className=\"flex items-center gap-3\">\n <AppIcon app={row.original} className=\"size-6\" />\n <div className=\"flex items-center gap-2\">\n <span className=\"font-medium\">\n {row.original.displayName || 'Unnamed App'}\n </span>\n {row.original.deprecated &&\n (() => {\n const deprecationType =\n row.original.deprecated.type || 'deprecated'\n return (\n <span className=\"text-[0.7rem] text-muted-foreground\">\n (\n {deprecationType === 'discouraged'\n ? 'Discouraged'\n : 'Deprecated'}\n )\n </span>\n )\n })()}\n </div>\n </div>\n ),\n meta: {\n className: 'w-[300px]',\n },\n },\n {\n id: 'description',\n header: 'Description',\n cell: ({ row }) => (\n <span className=\"text-sm text-muted-foreground line-clamp-2\">\n {row.original.description || '—'}\n </span>\n ),\n },\n ],\n [],\n )\n\n // Create a single table instance with all apps\n const table = useReactTable({\n data: apps,\n columns,\n getCoreRowModel: getCoreRowModel(),\n getRowId: (row) => row.id,\n })\n\n // Panel visibility state - default to closed\n const [isPanelOpen, setIsPanelOpen] = useState(false)\n\n // Open panel when app is selected\n useEffect(() => {\n if (selectedApp) {\n setIsPanelOpen(true)\n }\n }, [selectedApp])\n\n // Auto-scroll to selected app (only on initial load)\n const hasScrolledRef = React.useRef(false)\n React.useEffect(() => {\n // Only scroll once on initial load if there's a selection\n if (selectedAppSlug && !hasScrolledRef.current) {\n const rowElement = rowRefs.current.get(selectedAppSlug)\n if (rowElement) {\n rowElement.scrollIntoView({ behavior: 'smooth', block: 'center' })\n }\n hasScrolledRef.current = true\n }\n }, [selectedAppSlug, rowRefs])\n\n const handleAppClick = (app: AppForCatalog) => {\n onAppClick?.(app)\n }\n\n const handleClosePanel = () => {\n setIsPanelOpen(false)\n }\n\n return (\n <ResizablePanelGroup orientation=\"horizontal\" className=\"h-full w-full\">\n {/* Left Panel - Table */}\n <ResizablePanel\n defaultSize={isPanelOpen ? 60 : 100}\n minSize={30}\n className=\"overflow-hidden\"\n >\n <div className=\"h-full overflow-y-auto pr-2 pb-6 [scrollbar-gutter:stable]\">\n <Table>\n <TableHeader className=\"sticky top-0 border-b bg-background z-10\">\n {table.getHeaderGroups().map((headerGroup) => (\n <TableRow key={headerGroup.id}>\n {headerGroup.headers.map((header) => (\n <TableHead\n key={header.id}\n className={cn(\n 'px-4 py-3 text-left font-medium text-sm',\n header.column.columnDef.meta?.className,\n )}\n >\n {header.isPlaceholder\n ? null\n : flexRender(\n header.column.columnDef.header,\n header.getContext(),\n )}\n </TableHead>\n ))}\n </TableRow>\n ))}\n </TableHeader>\n\n <TableBody>\n {groupedApps.map((group) => (\n <React.Fragment key={group.groupName}>\n {/* Group Header Row */}\n <TableRow className=\"bg-muted/50 hover:bg-muted/50\">\n <TableCell\n colSpan={columns.length}\n className=\"px-4 py-6 sticky top-[49px] bg-muted/90 backdrop-blur z-10\"\n >\n <div className=\"flex items-center justify-center\">\n <span className=\"font-bold text-lg tracking-widest uppercase leading-loose text-muted-foreground\">\n {group.groupName}\n </span>\n </div>\n </TableCell>\n </TableRow>\n\n {/* Group Apps */}\n {group.apps.map((app) => {\n const row = table\n .getRowModel()\n .rows.find((r) => r.id === app.id)\n if (!row) return null\n\n return (\n <TableRow\n key={row.id}\n ref={(el) => {\n if (el && row.original.slug) {\n rowRefs.current.set(row.original.slug, el)\n } else if (row.original.slug) {\n rowRefs.current.delete(row.original.slug)\n }\n }}\n onClick={() => handleAppClick(row.original)}\n className={cn(\n 'border-b cursor-pointer transition-colors',\n selectedApp?.id === row.original.id\n ? 'bg-blue-100 dark:bg-blue-950 hover:bg-blue-200 dark:hover:bg-blue-900'\n : 'hover:bg-muted/30',\n )}\n >\n {row.getVisibleCells().map((cell) => (\n <TableCell\n key={cell.id}\n className={cn(\n 'px-4 py-4',\n cell.column.columnDef.meta?.className,\n )}\n >\n {flexRender(\n cell.column.columnDef.cell,\n cell.getContext(),\n )}\n </TableCell>\n ))}\n </TableRow>\n )\n })}\n </React.Fragment>\n ))}\n\n {/* Clear Filters Row */}\n {totalAppsCount &&\n totalAppsCount > apps.length &&\n onClearFilters && (\n <TableRow>\n <TableCell\n colSpan={columns.length}\n className=\"px-4 py-8 text-center\"\n >\n <Button\n variant=\"outline\"\n onClick={onClearFilters}\n className=\"gap-2\"\n >\n <X className=\"h-4 w-4\" />\n Clear filters to show all apps ({totalAppsCount})\n </Button>\n </TableCell>\n </TableRow>\n )}\n </TableBody>\n </Table>\n </div>\n </ResizablePanel>\n\n {/* Right Panel - Details (only render when panel is open) */}\n {isPanelOpen && (\n <>\n {/* Resizable Handle */}\n <ResizableHandle withHandle />\n\n <ResizablePanel\n defaultSize={40}\n minSize={25}\n className=\"overflow-hidden\"\n >\n <div className=\"h-full overflow-y-auto border-l bg-background pl-4\">\n {selectedApp ? (\n <div className=\"relative\">\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"absolute top-4 right-4 z-10 hover:bg-accent\"\n onClick={handleClosePanel}\n aria-label=\"Close details panel\"\n >\n <X className=\"h-5 w-5\" />\n </Button>\n <AppDetails\n app={selectedApp}\n onAppClick={onAppClick}\n onClosePanel={handleClosePanel}\n />\n </div>\n ) : null}\n </div>\n </ResizablePanel>\n </>\n )}\n </ResizablePanelGroup>\n )\n}\n"],"names":["React","_a"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AppCatalogGrid.js","sources":["../../../../../../src/modules/appCatalog/ui/grid/AppCatalogGrid.tsx"],"sourcesContent":["import type {\n AppForCatalog,\n GroupingTagDefinition,\n} from '@igstack/app-catalog-backend-core'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport {\n flexRender,\n getCoreRowModel,\n useReactTable,\n} from '@tanstack/react-table'\nimport { AppWindow, ExternalLink, X } from 'lucide-react'\nimport React, { useEffect, useState } from 'react'\n\nimport { cn } from '~/lib/utils'\nimport type {} from '~/types/table'\nimport { Badge } from '~/ui/badge'\nimport { Button } from '~/ui/button'\nimport {\n ResizableHandle,\n ResizablePanel,\n ResizablePanelGroup,\n} from '~/ui/resizable'\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '~/ui/table'\nimport { AccessRequestSection } from '../components/AccessRequestSection'\nimport { ScreenshotGallery } from '../components/ScreenshotGallery'\nimport { useAppCatalogContext } from '../../context/AppCatalogContext'\nimport { useAppClickHistory } from '../../hooks/useAppClickHistory'\nimport { useKeyboardNavigation } from '../hooks/useKeyboardNavigation'\n\nexport interface AppCatalogGridProps {\n apps: Array<AppForCatalog>\n selectedAppSlug?: string\n groupingDefinition?: GroupingTagDefinition\n onAppClick?: (app: AppForCatalog) => void\n /** Whether search is active (affects group sorting) */\n hasSearch?: boolean\n}\n\nfunction getIconUrl(iconName: string): string {\n return `/api/icons/${iconName}`\n}\n\nfunction AppIcon({\n app,\n className,\n}: {\n app: AppForCatalog\n className?: string\n}) {\n const [imageError, setImageError] = React.useState(false)\n\n // Use iconName from backend if available\n if (app.iconName && !imageError) {\n return (\n <div className={cn('size-12 shrink-0', className)}>\n <img\n src={getIconUrl(app.iconName)}\n alt={`${app.displayName} icon`}\n className=\"size-12 rounded-lg object-contain\"\n onError={() => setImageError(true)}\n />\n </div>\n )\n }\n\n // Fallback icon\n return (\n <div\n className={cn(\n 'flex items-center justify-center rounded-lg bg-primary/10 text-primary size-12 shrink-0',\n className,\n )}\n >\n <AppWindow className=\"size-6\" />\n </div>\n )\n}\n\nfunction AppScreenshot({ app }: { app: AppForCatalog }) {\n const [imageError, setImageError] = React.useState(false)\n const [isLoadingImage, setIsLoadingImage] = React.useState(true)\n\n // Check if app has screenshots\n const screenshotId = app.screenshotIds?.[0]\n if (!screenshotId) {\n return (\n <div className=\"w-full bg-muted/50 rounded-lg overflow-hidden flex items-center justify-center min-h-64\">\n <div className=\"w-full h-64 bg-muted/30 flex items-center justify-center text-muted-foreground text-sm\">\n No screenshot available\n </div>\n </div>\n )\n }\n\n const screenshotImageUrl = `/api/screenshots/${screenshotId}?size=512`\n\n return (\n <div className=\"w-full flex justify-center\">\n <div className=\"rounded-lg overflow-hidden inline-flex items-center justify-center min-h-64\">\n {!imageError ? (\n <img\n src={screenshotImageUrl}\n alt={`${app.displayName} screenshot`}\n className=\"h-64 object-contain\"\n onError={() => {\n setImageError(true)\n setIsLoadingImage(false)\n }}\n onLoad={() => setIsLoadingImage(false)}\n />\n ) : null}\n {(imageError || isLoadingImage) && (\n <div className=\"w-full h-64 bg-muted/30 flex items-center justify-center text-muted-foreground text-sm\">\n {isLoadingImage\n ? 'Loading screenshot...'\n : 'No screenshot available'}\n </div>\n )}\n </div>\n </div>\n )\n}\n\nfunction AppDetails({\n app,\n onAppClick,\n}: {\n app: AppForCatalog\n onAppClick?: (app: AppForCatalog) => void\n}) {\n const [isGalleryOpen, setIsGalleryOpen] = React.useState(false)\n const [galleryInitialIndex, setGalleryInitialIndex] = React.useState(0)\n const { approvalMethods, apps } = useAppCatalogContext()\n const { recordClick } = useAppClickHistory()\n\n const handleScreenshotClick = (index: number) => {\n setGalleryInitialIndex(index)\n setIsGalleryOpen(true)\n }\n\n // Find replacement app if deprecated\n const replacementApp = app.deprecated?.replacementSlug\n ? apps.find((a) => a.slug === app.deprecated?.replacementSlug)\n : null\n\n return (\n <>\n <div className=\"flex h-full flex-col p-6\">\n {/* Icon and Title */}\n <div className=\"flex items-center gap-4 border-b pb-6\">\n <AppIcon app={app} className=\"size-16\" />\n <div>\n <div className=\"flex items-center gap-2\">\n <h2 className=\"text-2xl font-semibold\">{app.displayName}</h2>\n {app.deprecated &&\n (() => {\n const deprecationType = app.deprecated.type || 'deprecated'\n return (\n <Badge\n variant={\n deprecationType === 'discouraged'\n ? 'secondary'\n : 'destructive'\n }\n >\n {deprecationType === 'discouraged'\n ? 'Discouraged'\n : 'Deprecated'}\n </Badge>\n )\n })()}\n </div>\n {app.appUrl && (\n <a\n href={app.appUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n onClick={() => recordClick(app.slug)}\n className=\"mt-1 inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-primary\"\n >\n {app.appUrl.replaceAll(/https?:\\/\\//g, '')}\n <ExternalLink className=\"size-3\" />\n </a>\n )}\n </div>\n </div>\n\n {/* Deprecation/Discouraged Warning */}\n {app.deprecated &&\n (() => {\n const deprecationType = app.deprecated.type || 'deprecated'\n const isDiscouraged = deprecationType === 'discouraged'\n return (\n <div\n className={\n isDiscouraged\n ? 'mt-6 p-4 border border-yellow-500/50 rounded-lg bg-yellow-50 dark:bg-yellow-950/20'\n : 'mt-6 p-4 border border-destructive/50 rounded-lg bg-destructive/10'\n }\n >\n <h3\n className={\n isDiscouraged\n ? 'text-sm font-semibold text-yellow-700 dark:text-yellow-500 mb-2'\n : 'text-sm font-semibold text-destructive mb-2'\n }\n >\n {isDiscouraged\n ? 'Usage discouraged'\n : 'This application is deprecated'}\n </h3>\n <p className=\"text-sm text-muted-foreground mb-3\">\n {app.deprecated.comment}\n </p>\n {replacementApp && (\n <button\n onClick={() => onAppClick?.(replacementApp)}\n className=\"text-sm font-medium text-primary hover:underline inline-flex items-center gap-1\"\n >\n View replacement: {replacementApp.displayName}\n <ExternalLink className=\"size-3\" />\n </button>\n )}\n </div>\n )\n })()}\n\n {/* Description */}\n {app.description && (\n <div className=\"mt-6\">\n <h3 className=\"mb-2 text-sm font-medium\">Description</h3>\n <p className=\"text-sm text-muted-foreground\">{app.description}</p>\n </div>\n )}\n\n {/* Screenshots - Clickable preview */}\n {app.screenshotIds && app.screenshotIds.length > 0 && (\n <div className=\"mt-6\">\n <h3 className=\"mb-2 text-sm font-medium\">\n Screenshots ({app.screenshotIds.length})\n </h3>\n <div\n className=\"cursor-pointer hover:opacity-80 transition-opacity\"\n onClick={() => handleScreenshotClick(0)}\n >\n <AppScreenshot app={app} />\n {app.screenshotIds.length > 1 && (\n <p className=\"text-xs text-muted-foreground mt-2 text-center\">\n Click to view all {app.screenshotIds.length} screenshots\n </p>\n )}\n </div>\n </div>\n )}\n\n {/* Access Request Section */}\n <AccessRequestSection app={app} approvalMethods={approvalMethods} />\n\n {/* Tags */}\n {app.tags && app.tags.length > 0 && (\n <div className=\"mt-6\">\n <h3 className=\"mb-2 text-sm font-medium\">Tags</h3>\n <div className=\"flex flex-wrap gap-2\">\n {app.tags.map((tag) => (\n <Badge key={tag} variant=\"secondary\" className=\"text-xs\">\n {tag}\n </Badge>\n ))}\n </div>\n </div>\n )}\n\n {/* Teams */}\n {app.teams && app.teams.length > 0 && (\n <div className=\"mt-6\">\n <h3 className=\"mb-2 text-sm font-medium\">Teams</h3>\n <div className=\"flex flex-wrap gap-2\">\n {app.teams.map((team) => (\n <Badge key={team} variant=\"outline\" className=\"text-xs\">\n {team}\n </Badge>\n ))}\n </div>\n </div>\n )}\n </div>\n\n {/* Screenshot Gallery Dialog */}\n <ScreenshotGallery\n app={app}\n screenshotIds={app.screenshotIds || []}\n open={isGalleryOpen}\n onOpenChange={setIsGalleryOpen}\n initialIndex={galleryInitialIndex}\n title={`${app.displayName} - Screenshots`}\n />\n </>\n )\n}\n\ninterface GroupedApps {\n groupName: string\n apps: Array<AppForCatalog>\n}\n\nfunction groupApps(\n apps: Array<AppForCatalog>,\n groupingDef?: GroupingTagDefinition,\n hasSearch?: boolean,\n): Array<GroupedApps> {\n if (!groupingDef) {\n const sortedApps = [...apps].sort((a, b) =>\n a.displayName.localeCompare(b.displayName),\n )\n return [{ groupName: 'All Apps', apps: sortedApps }]\n }\n\n const grouped = new Map<string, Array<AppForCatalog>>()\n const ungrouped: Array<AppForCatalog> = []\n\n for (const app of apps) {\n const matchingTag = app.tags?.find((tag) =>\n tag.startsWith(`${groupingDef.prefix}:`),\n )\n\n if (matchingTag) {\n const value = matchingTag.split(':')[1]\n if (value) {\n const tagValue = groupingDef.values.find((v) => v.value === value)\n const displayName = tagValue?.displayName || value\n\n if (!grouped.has(displayName)) {\n grouped.set(displayName, [])\n }\n grouped.get(displayName)!.push(app)\n } else {\n ungrouped.push(app)\n }\n } else {\n ungrouped.push(app)\n }\n }\n\n const result: Array<GroupedApps> = []\n for (const [groupName, appsInGroup] of grouped) {\n // Sort apps alphabetically within each group\n const sortedGroupApps = appsInGroup.sort((a, b) =>\n a.displayName.localeCompare(b.displayName),\n )\n result.push({ groupName, apps: sortedGroupApps })\n }\n\n if (ungrouped.length > 0) {\n // Sort ungrouped apps alphabetically\n const sortedUngrouped = ungrouped.sort((a, b) =>\n a.displayName.localeCompare(b.displayName),\n )\n result.push({ groupName: 'Other', apps: sortedUngrouped })\n }\n\n // Sort groups: when no search, sort by app count descending\n // When search is active, keep the order based on app relevance\n if (!hasSearch) {\n result.sort((a, b) => b.apps.length - a.apps.length)\n }\n\n return result\n}\n\nexport function AppCatalogGrid({\n apps,\n selectedAppSlug,\n groupingDefinition,\n onAppClick,\n hasSearch = false,\n}: AppCatalogGridProps) {\n const selectedApp = selectedAppSlug\n ? apps.find((a) => a.slug === selectedAppSlug)\n : null\n\n const groupedApps = groupApps(apps, groupingDefinition, hasSearch)\n\n // Flatten grouped apps to get display order for keyboard navigation\n const appsInDisplayOrder = React.useMemo(\n () => groupedApps.flatMap((group) => group.apps),\n [groupedApps],\n )\n\n // Use keyboard navigation hook with apps in display order\n const { rowRefs } = useKeyboardNavigation({\n apps: appsInDisplayOrder,\n selectedAppSlug,\n onAppClick,\n })\n\n // Define columns\n const columns = React.useMemo<Array<ColumnDef<AppForCatalog>>>(\n () => [\n {\n id: 'application',\n header: 'Application',\n cell: ({ row }) => (\n <div className=\"flex items-center gap-3\">\n <AppIcon app={row.original} className=\"size-6\" />\n <div className=\"flex items-center gap-2\">\n <span className=\"font-medium\">\n {row.original.displayName || 'Unnamed App'}\n </span>\n {row.original.deprecated &&\n (() => {\n const deprecationType =\n row.original.deprecated.type || 'deprecated'\n return (\n <span className=\"text-[0.7rem] text-muted-foreground\">\n (\n {deprecationType === 'discouraged'\n ? 'Discouraged'\n : 'Deprecated'}\n )\n </span>\n )\n })()}\n </div>\n </div>\n ),\n meta: {\n className: 'w-[300px]',\n },\n },\n {\n id: 'description',\n header: 'Description',\n cell: ({ row }) => (\n <span className=\"text-sm text-muted-foreground line-clamp-2\">\n {row.original.description || '—'}\n </span>\n ),\n },\n ],\n [],\n )\n\n // Create a single table instance with all apps\n const table = useReactTable({\n data: apps,\n columns,\n getCoreRowModel: getCoreRowModel(),\n getRowId: (row) => row.id,\n })\n\n // Panel visibility state - default to closed\n const [isPanelOpen, setIsPanelOpen] = useState(false)\n\n // Open panel when app is selected\n useEffect(() => {\n if (selectedApp) {\n setIsPanelOpen(true)\n }\n }, [selectedApp])\n\n // Auto-scroll to selected app (only on initial load)\n const hasScrolledRef = React.useRef(false)\n React.useEffect(() => {\n // Only scroll once on initial load if there's a selection\n if (selectedAppSlug && !hasScrolledRef.current) {\n const rowElement = rowRefs.current.get(selectedAppSlug)\n if (rowElement) {\n rowElement.scrollIntoView({ behavior: 'smooth', block: 'center' })\n }\n hasScrolledRef.current = true\n }\n }, [selectedAppSlug, rowRefs])\n\n const handleAppClick = (app: AppForCatalog) => {\n onAppClick?.(app)\n }\n\n const handleClosePanel = () => {\n setIsPanelOpen(false)\n }\n\n return (\n <ResizablePanelGroup orientation=\"horizontal\" className=\"h-full w-full\">\n {/* Left Panel - Table */}\n <ResizablePanel\n defaultSize={isPanelOpen ? 60 : 100}\n minSize={30}\n className=\"overflow-hidden\"\n >\n <div className=\"h-full overflow-y-auto pr-2 pb-6 [scrollbar-gutter:stable]\">\n <Table>\n <TableHeader className=\"sticky top-0 border-b bg-background z-10\">\n {table.getHeaderGroups().map((headerGroup) => (\n <TableRow key={headerGroup.id}>\n {headerGroup.headers.map((header) => (\n <TableHead\n key={header.id}\n className={cn(\n 'px-4 py-3 text-left font-medium text-sm',\n header.column.columnDef.meta?.className,\n )}\n >\n {header.isPlaceholder\n ? null\n : flexRender(\n header.column.columnDef.header,\n header.getContext(),\n )}\n </TableHead>\n ))}\n </TableRow>\n ))}\n </TableHeader>\n\n <TableBody>\n {groupedApps.map((group) => (\n <React.Fragment key={group.groupName}>\n {/* Group Header Row */}\n <TableRow className=\"bg-muted/50 hover:bg-muted/50\">\n <TableCell\n colSpan={columns.length}\n className=\"px-4 py-6 sticky top-[49px] bg-muted/90 backdrop-blur z-10\"\n >\n <div className=\"flex items-center justify-center\">\n <span className=\"font-bold text-lg tracking-widest uppercase leading-loose text-muted-foreground\">\n {group.groupName}\n </span>\n </div>\n </TableCell>\n </TableRow>\n\n {/* Group Apps */}\n {group.apps.map((app) => {\n const row = table\n .getRowModel()\n .rows.find((r) => r.id === app.id)\n if (!row) return null\n\n return (\n <TableRow\n key={row.id}\n ref={(el) => {\n if (el && row.original.slug) {\n rowRefs.current.set(row.original.slug, el)\n } else if (row.original.slug) {\n rowRefs.current.delete(row.original.slug)\n }\n }}\n onClick={() => handleAppClick(row.original)}\n className={cn(\n 'border-b cursor-pointer transition-colors',\n selectedApp?.id === row.original.id\n ? 'bg-blue-100 dark:bg-blue-950 hover:bg-blue-200 dark:hover:bg-blue-900'\n : 'hover:bg-muted/30',\n )}\n >\n {row.getVisibleCells().map((cell) => (\n <TableCell\n key={cell.id}\n className={cn(\n 'px-4 py-4',\n cell.column.columnDef.meta?.className,\n )}\n >\n {flexRender(\n cell.column.columnDef.cell,\n cell.getContext(),\n )}\n </TableCell>\n ))}\n </TableRow>\n )\n })}\n </React.Fragment>\n ))}\n </TableBody>\n </Table>\n </div>\n </ResizablePanel>\n\n {/* Right Panel - Details (only render when panel is open) */}\n {isPanelOpen && (\n <>\n {/* Resizable Handle */}\n <ResizableHandle withHandle />\n\n <ResizablePanel\n defaultSize={40}\n minSize={25}\n className=\"overflow-hidden\"\n >\n <div className=\"h-full overflow-y-auto border-l bg-background pl-4\">\n {selectedApp ? (\n <div className=\"relative\">\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"absolute top-2 right-2 z-10\"\n onClick={handleClosePanel}\n aria-label=\"Close details panel\"\n >\n <X className=\"h-4 w-4\" />\n </Button>\n <AppDetails app={selectedApp} onAppClick={onAppClick} />\n </div>\n ) : null}\n </div>\n </ResizablePanel>\n </>\n )}\n </ResizablePanelGroup>\n )\n}\n"],"names":["React","_a"],"mappings":"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"}
|
|
@@ -62,26 +62,9 @@ function AppCatalogPage() {
|
|
|
62
62
|
topAppSlugs,
|
|
63
63
|
searchValue: deferredSearchValue
|
|
64
64
|
});
|
|
65
|
-
useEffect(() => {
|
|
66
|
-
if (filteredApps.length === 1 && filteredApps[0]) {
|
|
67
|
-
setSelectedAppSlug(filteredApps[0].slug);
|
|
68
|
-
}
|
|
69
|
-
}, [filteredApps, setSelectedAppSlug]);
|
|
70
65
|
const handleAppClick = (app) => {
|
|
71
66
|
setSelectedAppSlug(app.slug);
|
|
72
67
|
};
|
|
73
|
-
const handleClearFilters = () => {
|
|
74
|
-
setSearchValue("");
|
|
75
|
-
actions.clearAllFilters();
|
|
76
|
-
setSelectedAppSlug(void 0);
|
|
77
|
-
};
|
|
78
|
-
const totalAppsCount = useMemo(() => {
|
|
79
|
-
let count = apps.length;
|
|
80
|
-
if (!filterState.showDeprecated) {
|
|
81
|
-
count = apps.filter((app) => !app.deprecated).length;
|
|
82
|
-
}
|
|
83
|
-
return count;
|
|
84
|
-
}, [apps, filterState.showDeprecated]);
|
|
85
68
|
if (isLoadingApps) {
|
|
86
69
|
return /* @__PURE__ */ jsx("div", { className: "py-6 text-muted-foreground", children: "Loading…" });
|
|
87
70
|
}
|
|
@@ -128,9 +111,7 @@ function AppCatalogPage() {
|
|
|
128
111
|
selectedAppSlug,
|
|
129
112
|
groupingDefinition,
|
|
130
113
|
onAppClick: handleAppClick,
|
|
131
|
-
hasSearch: !!deferredSearchValue
|
|
132
|
-
totalAppsCount,
|
|
133
|
-
onClearFilters: handleClearFilters
|
|
114
|
+
hasSearch: !!deferredSearchValue
|
|
134
115
|
}
|
|
135
116
|
) })
|
|
136
117
|
] });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AppCatalogPage.js","sources":["../../../../../../src/modules/appCatalog/ui/pages/AppCatalogPage.tsx"],"sourcesContent":["import type { AppForCatalog } from '@igstack/app-catalog-backend-core'\nimport { X } from 'lucide-react'\nimport { useDeferredValue, useEffect, useMemo, useState } from 'react'\nimport { Button } from '~/ui/button'\nimport {\n Empty,\n EmptyContent,\n EmptyDescription,\n EmptyHeader,\n EmptyMedia,\n EmptyTitle,\n} from '~/ui/empty'\nimport { useAppCatalogContext } from '../../context/AppCatalogContext'\nimport { useAppClickHistory } from '../../hooks/useAppClickHistory'\nimport { useAppCounts } from '../../hooks/useAppCounts'\nimport { useUrlSyncedState } from '../../hooks/useUrlSyncedState'\nimport { searchApps } from '../../utils/searchApps'\nimport { OnboardingCard } from '../components/OnboardingCard'\nimport { useAppCatalogFilters } from '../context/AppCatalogFiltersContext'\nimport { FilterBar } from '../filters/FilterBar'\nimport { AppCatalogGrid } from '../grid/AppCatalogGrid'\n\nexport function AppCatalogPage() {\n const { apps, isLoadingApps, tagsDefinitions } = useAppCatalogContext()\n const { state: filterState, actions } = useAppCatalogFilters()\n const { getTopApps } = useAppClickHistory()\n\n // URL-synced state\n const [selectedAppSlug, setSelectedAppSlug] = useUrlSyncedState<\n string | undefined\n >({\n key: 'app',\n defaultValue: undefined,\n })\n\n // Search value from context (URL-synced in AppCatalogFiltersContext)\n const searchValue = filterState.searchValue\n const setSearchValue = actions.setSearchValue\n\n // Defer the search value used for filtering to avoid blocking the input\n const deferredSearchValue = useDeferredValue(searchValue)\n\n // State for top apps (loaded async)\n const [topAppSlugs, setTopAppSlugs] = useState<Array<string>>([])\n\n // Load top apps on mount to calculate recent count\n useEffect(() => {\n void getTopApps(10).then(setTopAppSlugs)\n }, [getTopApps])\n\n const filteredApps = useMemo(() => {\n let result = apps\n\n // Step 1: Filter deprecated apps (if not showing them)\n if (!filterState.showDeprecated) {\n result = result.filter((app) => !app.deprecated)\n }\n\n // Step 2: Apply recent mode or tag filters\n if (filterState.recentMode) {\n // Filter to top 10 most clicked apps\n result = result.filter((app) => topAppSlugs.includes(app.slug))\n } else if (Object.keys(filterState.tagFilters).length > 0) {\n // Apply tag filters (AND condition)\n result = result.filter((app) => {\n return Object.entries(filterState.tagFilters).every(\n ([prefix, value]) => {\n const fullTag = `${prefix}:${value}`\n return app.tags?.some(\n (tag) => tag.toLowerCase() === fullTag.toLowerCase(),\n )\n },\n )\n })\n }\n\n // Step 3: Apply search (using deferred value)\n result = searchApps(result, deferredSearchValue)\n\n return result\n }, [\n apps,\n deferredSearchValue,\n filterState.recentMode,\n filterState.tagFilters,\n filterState.showDeprecated,\n topAppSlugs,\n ])\n\n // Calculate counts for FilterBar\n const { allCount, recentCount, deprecatedCount } = useAppCounts({\n apps,\n topAppSlugs,\n searchValue: deferredSearchValue,\n })\n\n
|
|
1
|
+
{"version":3,"file":"AppCatalogPage.js","sources":["../../../../../../src/modules/appCatalog/ui/pages/AppCatalogPage.tsx"],"sourcesContent":["import type { AppForCatalog } from '@igstack/app-catalog-backend-core'\nimport { X } from 'lucide-react'\nimport { useDeferredValue, useEffect, useMemo, useState } from 'react'\nimport { Button } from '~/ui/button'\nimport {\n Empty,\n EmptyContent,\n EmptyDescription,\n EmptyHeader,\n EmptyMedia,\n EmptyTitle,\n} from '~/ui/empty'\nimport { useAppCatalogContext } from '../../context/AppCatalogContext'\nimport { useAppClickHistory } from '../../hooks/useAppClickHistory'\nimport { useAppCounts } from '../../hooks/useAppCounts'\nimport { useUrlSyncedState } from '../../hooks/useUrlSyncedState'\nimport { searchApps } from '../../utils/searchApps'\nimport { OnboardingCard } from '../components/OnboardingCard'\nimport { useAppCatalogFilters } from '../context/AppCatalogFiltersContext'\nimport { FilterBar } from '../filters/FilterBar'\nimport { AppCatalogGrid } from '../grid/AppCatalogGrid'\n\nexport function AppCatalogPage() {\n const { apps, isLoadingApps, tagsDefinitions } = useAppCatalogContext()\n const { state: filterState, actions } = useAppCatalogFilters()\n const { getTopApps } = useAppClickHistory()\n\n // URL-synced state\n const [selectedAppSlug, setSelectedAppSlug] = useUrlSyncedState<\n string | undefined\n >({\n key: 'app',\n defaultValue: undefined,\n })\n\n // Search value from context (URL-synced in AppCatalogFiltersContext)\n const searchValue = filterState.searchValue\n const setSearchValue = actions.setSearchValue\n\n // Defer the search value used for filtering to avoid blocking the input\n const deferredSearchValue = useDeferredValue(searchValue)\n\n // State for top apps (loaded async)\n const [topAppSlugs, setTopAppSlugs] = useState<Array<string>>([])\n\n // Load top apps on mount to calculate recent count\n useEffect(() => {\n void getTopApps(10).then(setTopAppSlugs)\n }, [getTopApps])\n\n const filteredApps = useMemo(() => {\n let result = apps\n\n // Step 1: Filter deprecated apps (if not showing them)\n if (!filterState.showDeprecated) {\n result = result.filter((app) => !app.deprecated)\n }\n\n // Step 2: Apply recent mode or tag filters\n if (filterState.recentMode) {\n // Filter to top 10 most clicked apps\n result = result.filter((app) => topAppSlugs.includes(app.slug))\n } else if (Object.keys(filterState.tagFilters).length > 0) {\n // Apply tag filters (AND condition)\n result = result.filter((app) => {\n return Object.entries(filterState.tagFilters).every(\n ([prefix, value]) => {\n const fullTag = `${prefix}:${value}`\n return app.tags?.some(\n (tag) => tag.toLowerCase() === fullTag.toLowerCase(),\n )\n },\n )\n })\n }\n\n // Step 3: Apply search (using deferred value)\n result = searchApps(result, deferredSearchValue)\n\n return result\n }, [\n apps,\n deferredSearchValue,\n filterState.recentMode,\n filterState.tagFilters,\n filterState.showDeprecated,\n topAppSlugs,\n ])\n\n // Calculate counts for FilterBar\n const { allCount, recentCount, deprecatedCount } = useAppCounts({\n apps,\n topAppSlugs,\n searchValue: deferredSearchValue,\n })\n\n const handleAppClick = (app: AppForCatalog) => {\n setSelectedAppSlug(app.slug)\n }\n\n if (isLoadingApps) {\n return <div className=\"py-6 text-muted-foreground\">Loading…</div>\n }\n\n // Use first tag definition for grouping\n const groupingDefinition = tagsDefinitions[0]\n\n return (\n <div className=\"flex flex-col flex-1 min-h-0\">\n <div className=\"shrink-0\">\n <OnboardingCard />\n </div>\n\n <div className=\"shrink-0\">\n <FilterBar\n totalCount={allCount}\n recentCount={recentCount}\n deprecatedCount={deprecatedCount}\n apps={apps}\n />\n </div>\n\n <div className=\"flex-1 min-h-0\">\n {filteredApps.length === 0 ? (\n <Empty>\n <EmptyHeader>\n <EmptyMedia variant=\"icon\">\n <X className=\"h-6 w-6\" />\n </EmptyMedia>\n <EmptyTitle>\n No apps found{searchValue && ` for \"${searchValue}\"`}\n </EmptyTitle>\n <EmptyDescription>\n Try adjusting your search or filters\n </EmptyDescription>\n </EmptyHeader>\n <EmptyContent>\n {searchValue && (\n <Button\n variant=\"outline\"\n onClick={() => {\n setSearchValue('')\n setSelectedAppSlug(undefined)\n }}\n className=\"gap-2\"\n >\n <X className=\"h-4 w-4\" />\n Clear search\n </Button>\n )}\n </EmptyContent>\n </Empty>\n ) : (\n <AppCatalogGrid\n apps={filteredApps}\n selectedAppSlug={selectedAppSlug}\n groupingDefinition={groupingDefinition}\n onAppClick={handleAppClick}\n hasSearch={!!deferredSearchValue}\n />\n )}\n </div>\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@igstack/app-catalog-frontend-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Frontend core library for App Catalog",
|
|
5
5
|
"homepage": "https://github.com/lislon/app-catalog",
|
|
6
6
|
"repository": {
|
|
@@ -133,8 +133,8 @@
|
|
|
133
133
|
"vite-plugin-static-copy": "^3.1.4",
|
|
134
134
|
"vite-plugin-svgr": "^4.2.0",
|
|
135
135
|
"vitest": "3.2.2",
|
|
136
|
-
"@igstack/app-catalog-
|
|
137
|
-
"@igstack/app-catalog-
|
|
136
|
+
"@igstack/app-catalog-shared-core": "0.2.0",
|
|
137
|
+
"@igstack/app-catalog-backend-core": "0.2.0"
|
|
138
138
|
},
|
|
139
139
|
"peerDependencies": {
|
|
140
140
|
"react": "19.1.2",
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import { jsx } from "react/jsx-runtime";
|
|
2
|
-
import { cva } from "class-variance-authority";
|
|
3
|
-
import { cn } from "../lib/utils.js";
|
|
4
|
-
import { Button } from "./button.js";
|
|
5
|
-
import { Input } from "./input.js";
|
|
6
|
-
function InputGroup({ className, ...props }) {
|
|
7
|
-
return /* @__PURE__ */ jsx(
|
|
8
|
-
"div",
|
|
9
|
-
{
|
|
10
|
-
"data-slot": "input-group",
|
|
11
|
-
role: "group",
|
|
12
|
-
className: cn(
|
|
13
|
-
"group/input-group border-input dark:bg-input/30 relative flex w-full items-center rounded-md border shadow-xs transition-[color,box-shadow] outline-none",
|
|
14
|
-
"h-9 min-w-0 has-[>textarea]:h-auto",
|
|
15
|
-
// Variants based on alignment.
|
|
16
|
-
"has-[>[data-align=inline-start]]:[&>input]:pl-2",
|
|
17
|
-
"has-[>[data-align=inline-end]]:[&>input]:pr-2",
|
|
18
|
-
"has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3",
|
|
19
|
-
"has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3",
|
|
20
|
-
// Focus state.
|
|
21
|
-
"has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-[3px]",
|
|
22
|
-
// Error state.
|
|
23
|
-
"has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40",
|
|
24
|
-
className
|
|
25
|
-
),
|
|
26
|
-
...props
|
|
27
|
-
}
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
|
-
const inputGroupAddonVariants = cva(
|
|
31
|
-
"text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 [&>kbd]:rounded-[calc(var(--radius)-5px)] group-data-[disabled=true]/input-group:opacity-50",
|
|
32
|
-
{
|
|
33
|
-
variants: {
|
|
34
|
-
align: {
|
|
35
|
-
"inline-start": "order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]",
|
|
36
|
-
"inline-end": "order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]",
|
|
37
|
-
"block-start": "order-first w-full justify-start px-3 pt-3 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5",
|
|
38
|
-
"block-end": "order-last w-full justify-start px-3 pb-3 [.border-t]:pt-3 group-has-[>input]/input-group:pb-2.5"
|
|
39
|
-
}
|
|
40
|
-
},
|
|
41
|
-
defaultVariants: {
|
|
42
|
-
align: "inline-start"
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
);
|
|
46
|
-
function InputGroupAddon({
|
|
47
|
-
className,
|
|
48
|
-
align = "inline-start",
|
|
49
|
-
...props
|
|
50
|
-
}) {
|
|
51
|
-
return /* @__PURE__ */ jsx(
|
|
52
|
-
"div",
|
|
53
|
-
{
|
|
54
|
-
role: "group",
|
|
55
|
-
"data-slot": "input-group-addon",
|
|
56
|
-
"data-align": align,
|
|
57
|
-
className: cn(inputGroupAddonVariants({ align }), className),
|
|
58
|
-
onClick: (e) => {
|
|
59
|
-
var _a, _b;
|
|
60
|
-
if (e.target.closest("button")) {
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
(_b = (_a = e.currentTarget.parentElement) == null ? void 0 : _a.querySelector("input")) == null ? void 0 : _b.focus();
|
|
64
|
-
},
|
|
65
|
-
...props
|
|
66
|
-
}
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
const inputGroupButtonVariants = cva(
|
|
70
|
-
"text-sm shadow-none flex gap-2 items-center",
|
|
71
|
-
{
|
|
72
|
-
variants: {
|
|
73
|
-
size: {
|
|
74
|
-
xs: "h-6 gap-1 px-2 rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2",
|
|
75
|
-
sm: "h-8 px-2.5 gap-1.5 rounded-md has-[>svg]:px-2.5",
|
|
76
|
-
"icon-xs": "size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0",
|
|
77
|
-
"icon-sm": "size-8 p-0 has-[>svg]:p-0"
|
|
78
|
-
}
|
|
79
|
-
},
|
|
80
|
-
defaultVariants: {
|
|
81
|
-
size: "xs"
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
);
|
|
85
|
-
function InputGroupButton({
|
|
86
|
-
className,
|
|
87
|
-
type = "button",
|
|
88
|
-
variant = "ghost",
|
|
89
|
-
size = "xs",
|
|
90
|
-
...props
|
|
91
|
-
}) {
|
|
92
|
-
return /* @__PURE__ */ jsx(
|
|
93
|
-
Button,
|
|
94
|
-
{
|
|
95
|
-
type,
|
|
96
|
-
"data-size": size,
|
|
97
|
-
variant,
|
|
98
|
-
className: cn(inputGroupButtonVariants({ size }), className),
|
|
99
|
-
...props
|
|
100
|
-
}
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
function InputGroupInput({
|
|
104
|
-
className,
|
|
105
|
-
...props
|
|
106
|
-
}) {
|
|
107
|
-
return /* @__PURE__ */ jsx(
|
|
108
|
-
Input,
|
|
109
|
-
{
|
|
110
|
-
"data-slot": "input-group-control",
|
|
111
|
-
className: cn(
|
|
112
|
-
"flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent",
|
|
113
|
-
className
|
|
114
|
-
),
|
|
115
|
-
...props
|
|
116
|
-
}
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
export {
|
|
120
|
-
InputGroup,
|
|
121
|
-
InputGroupAddon,
|
|
122
|
-
InputGroupButton,
|
|
123
|
-
InputGroupInput
|
|
124
|
-
};
|
|
125
|
-
//# sourceMappingURL=input-group.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"input-group.js","sources":["../../../src/ui/input-group.tsx"],"sourcesContent":["import type { VariantProps } from 'class-variance-authority'\nimport { cva } from 'class-variance-authority'\nimport * as React from 'react'\n\nimport { cn } from '~/lib/utils'\nimport { Button } from '~/ui/button'\nimport { Input } from '~/ui/input'\nimport { Textarea } from '~/ui/textarea'\n\nfunction InputGroup({ className, ...props }: React.ComponentProps<'div'>) {\n return (\n <div\n data-slot=\"input-group\"\n role=\"group\"\n className={cn(\n 'group/input-group border-input dark:bg-input/30 relative flex w-full items-center rounded-md border shadow-xs transition-[color,box-shadow] outline-none',\n 'h-9 min-w-0 has-[>textarea]:h-auto',\n\n // Variants based on alignment.\n 'has-[>[data-align=inline-start]]:[&>input]:pl-2',\n 'has-[>[data-align=inline-end]]:[&>input]:pr-2',\n 'has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3',\n 'has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3',\n\n // Focus state.\n 'has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-[3px]',\n\n // Error state.\n 'has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40',\n\n className,\n )}\n {...props}\n />\n )\n}\n\nconst inputGroupAddonVariants = cva(\n \"text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 [&>kbd]:rounded-[calc(var(--radius)-5px)] group-data-[disabled=true]/input-group:opacity-50\",\n {\n variants: {\n align: {\n 'inline-start':\n 'order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]',\n 'inline-end':\n 'order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]',\n 'block-start':\n 'order-first w-full justify-start px-3 pt-3 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5',\n 'block-end':\n 'order-last w-full justify-start px-3 pb-3 [.border-t]:pt-3 group-has-[>input]/input-group:pb-2.5',\n },\n },\n defaultVariants: {\n align: 'inline-start',\n },\n },\n)\n\nfunction InputGroupAddon({\n className,\n align = 'inline-start',\n ...props\n}: React.ComponentProps<'div'> & VariantProps<typeof inputGroupAddonVariants>) {\n return (\n <div\n role=\"group\"\n data-slot=\"input-group-addon\"\n data-align={align}\n className={cn(inputGroupAddonVariants({ align }), className)}\n onClick={(e) => {\n if ((e.target as HTMLElement).closest('button')) {\n return\n }\n e.currentTarget.parentElement?.querySelector('input')?.focus()\n }}\n {...props}\n />\n )\n}\n\nconst inputGroupButtonVariants = cva(\n 'text-sm shadow-none flex gap-2 items-center',\n {\n variants: {\n size: {\n xs: \"h-6 gap-1 px-2 rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2\",\n sm: 'h-8 px-2.5 gap-1.5 rounded-md has-[>svg]:px-2.5',\n 'icon-xs':\n 'size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0',\n 'icon-sm': 'size-8 p-0 has-[>svg]:p-0',\n },\n },\n defaultVariants: {\n size: 'xs',\n },\n },\n)\n\nfunction InputGroupButton({\n className,\n type = 'button',\n variant = 'ghost',\n size = 'xs',\n ...props\n}: Omit<React.ComponentProps<typeof Button>, 'size'> &\n VariantProps<typeof inputGroupButtonVariants>) {\n return (\n <Button\n type={type}\n data-size={size}\n variant={variant}\n className={cn(inputGroupButtonVariants({ size }), className)}\n {...props}\n />\n )\n}\n\nfunction InputGroupText({ className, ...props }: React.ComponentProps<'span'>) {\n return (\n <span\n className={cn(\n \"text-muted-foreground flex items-center gap-2 text-sm [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4\",\n className,\n )}\n {...props}\n />\n )\n}\n\nfunction InputGroupInput({\n className,\n ...props\n}: React.ComponentProps<'input'>) {\n return (\n <Input\n data-slot=\"input-group-control\"\n className={cn(\n 'flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent',\n className,\n )}\n {...props}\n />\n )\n}\n\nfunction InputGroupTextarea({\n className,\n ...props\n}: React.ComponentProps<'textarea'>) {\n return (\n <Textarea\n data-slot=\"input-group-control\"\n className={cn(\n 'flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent',\n className,\n )}\n {...props}\n />\n )\n}\n\nexport {\n InputGroup,\n InputGroupAddon,\n InputGroupButton, InputGroupInput, InputGroupText, InputGroupTextarea\n}\n\n"],"names":[],"mappings":";;;;;AASA,SAAS,WAAW,EAAE,WAAW,GAAG,SAAsC;AACxE,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,aAAU;AAAA,MACV,MAAK;AAAA,MACL,WAAW;AAAA,QACT;AAAA,QACA;AAAA;AAAA,QAGA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA;AAAA,QAGA;AAAA;AAAA,QAGA;AAAA,QAEA;AAAA,MAAA;AAAA,MAED,GAAG;AAAA,IAAA;AAAA,EAAA;AAGV;AAEA,MAAM,0BAA0B;AAAA,EAC9B;AAAA,EACA;AAAA,IACE,UAAU;AAAA,MACR,OAAO;AAAA,QACL,gBACE;AAAA,QACF,cACE;AAAA,QACF,eACE;AAAA,QACF,aACE;AAAA,MAAA;AAAA,IACJ;AAAA,IAEF,iBAAiB;AAAA,MACf,OAAO;AAAA,IAAA;AAAA,EACT;AAEJ;AAEA,SAAS,gBAAgB;AAAA,EACvB;AAAA,EACA,QAAQ;AAAA,EACR,GAAG;AACL,GAA+E;AAC7E,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,MAAK;AAAA,MACL,aAAU;AAAA,MACV,cAAY;AAAA,MACZ,WAAW,GAAG,wBAAwB,EAAE,MAAA,CAAO,GAAG,SAAS;AAAA,MAC3D,SAAS,CAAC,MAAM;;AACd,YAAK,EAAE,OAAuB,QAAQ,QAAQ,GAAG;AAC/C;AAAA,QACF;AACA,sBAAE,cAAc,kBAAhB,mBAA+B,cAAc,aAA7C,mBAAuD;AAAA,MACzD;AAAA,MACC,GAAG;AAAA,IAAA;AAAA,EAAA;AAGV;AAEA,MAAM,2BAA2B;AAAA,EAC/B;AAAA,EACA;AAAA,IACE,UAAU;AAAA,MACR,MAAM;AAAA,QACJ,IAAI;AAAA,QACJ,IAAI;AAAA,QACJ,WACE;AAAA,QACF,WAAW;AAAA,MAAA;AAAA,IACb;AAAA,IAEF,iBAAiB;AAAA,MACf,MAAM;AAAA,IAAA;AAAA,EACR;AAEJ;AAEA,SAAS,iBAAiB;AAAA,EACxB;AAAA,EACA,OAAO;AAAA,EACP,UAAU;AAAA,EACV,OAAO;AAAA,EACP,GAAG;AACL,GACiD;AAC/C,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC;AAAA,MACA,aAAW;AAAA,MACX;AAAA,MACA,WAAW,GAAG,yBAAyB,EAAE,KAAA,CAAM,GAAG,SAAS;AAAA,MAC1D,GAAG;AAAA,IAAA;AAAA,EAAA;AAGV;AAcA,SAAS,gBAAgB;AAAA,EACvB;AAAA,EACA,GAAG;AACL,GAAkC;AAChC,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MAAA;AAAA,MAED,GAAG;AAAA,IAAA;AAAA,EAAA;AAGV;"}
|