@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.
@@ -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 { InputGroup, InputGroupInput, InputGroupAddon, InputGroupButton } from "../../../../ui/input-group.js";
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__ */ jsxs(InputGroup, { className: "max-w-sm", children: [
45
- /* @__PURE__ */ jsx(
46
- InputGroupInput,
47
- {
48
- value: state.searchValue,
49
- onChange: (e) => actions.setSearchValue(e.target.value),
50
- onFocus: (e) => e.target.select(),
51
- placeholder: "Search apps by name, description, or tags…",
52
- "aria-label": "Search apps"
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 { X } from 'lucide-react'\nimport { useMemo } from 'react'\nimport { Button } from '~/ui/button'\nimport { Checkbox } from '~/ui/checkbox'\nimport {\n InputGroup,\n InputGroupAddon,\n InputGroupButton,\n InputGroupInput,\n} from '~/ui/input-group'\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 <InputGroup className=\"max-w-sm\">\n <InputGroupInput\n value={state.searchValue}\n onChange={(e) => actions.setSearchValue(e.target.value)}\n onFocus={(e) => e.target.select()}\n placeholder=\"Search apps by name, description, or tags…\"\n aria-label=\"Search apps\"\n />\n {state.searchValue && (\n <InputGroupAddon align=\"inline-end\">\n <InputGroupButton\n size=\"icon-xs\"\n onClick={() => actions.setSearchValue('')}\n aria-label=\"Clear search\"\n >\n <X />\n </InputGroupButton>\n </InputGroupAddon>\n )}\n </InputGroup>\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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"}
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, totalAppsCount, onClearFilters, }: AppCatalogGridProps): import("react/jsx-runtime").JSX.Element;
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__ */ jsx("div", { className: "border-b pb-6", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
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", { className: "-mx-3", children: [
118
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-3", children: [
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 rounded-md px-3 py-1 text-sm text-blue-600 hover:bg-accent/30 hover:underline dark:text-blue-400 transition-all group",
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.5 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity" })
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__ */ jsxs(TableBody, { children: [
414
- groupedApps.map((group) => /* @__PURE__ */ jsxs(React__default.Fragment, { children: [
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-8 text-center",
468
- children: /* @__PURE__ */ jsxs(
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-4 right-4 z-10 hover:bg-accent",
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-5 w-5" })
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 // Auto-open details when only 1 result\n useEffect(() => {\n if (filteredApps.length === 1 && filteredApps[0]) {\n setSelectedAppSlug(filteredApps[0].slug)\n }\n }, [filteredApps, setSelectedAppSlug])\n\n const handleAppClick = (app: AppForCatalog) => {\n setSelectedAppSlug(app.slug)\n }\n\n const handleClearFilters = () => {\n setSearchValue('')\n actions.clearAllFilters()\n setSelectedAppSlug(undefined)\n }\n\n // Calculate total apps count (respecting showDeprecated setting)\n const totalAppsCount = useMemo(() => {\n let count = apps.length\n if (!filterState.showDeprecated) {\n count = apps.filter((app) => !app.deprecated).length\n }\n return count\n }, [apps, filterState.showDeprecated])\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 totalAppsCount={totalAppsCount}\n onClearFilters={handleClearFilters}\n />\n )}\n </div>\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
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":";;;;;;;;;;;;;;AAsBO,SAAS,iBAAiB;AAC/B,QAAM,EAAE,MAAM,eAAe,gBAAA,IAAoB,qBAAA;AACjD,QAAM,EAAE,OAAO,aAAa,QAAA,IAAY,qBAAA;AACxC,QAAM,EAAE,WAAA,IAAe,mBAAA;AAGvB,QAAM,CAAC,iBAAiB,kBAAkB,IAAI,kBAE5C;AAAA,IACA,KAAK;AAAA,IACL,cAAc;AAAA,EAAA,CACf;AAGD,QAAM,cAAc,YAAY;AAChC,QAAM,iBAAiB,QAAQ;AAG/B,QAAM,sBAAsB,iBAAiB,WAAW;AAGxD,QAAM,CAAC,aAAa,cAAc,IAAI,SAAwB,CAAA,CAAE;AAGhE,YAAU,MAAM;AACd,SAAK,WAAW,EAAE,EAAE,KAAK,cAAc;AAAA,EACzC,GAAG,CAAC,UAAU,CAAC;AAEf,QAAM,eAAe,QAAQ,MAAM;AACjC,QAAI,SAAS;AAGb,QAAI,CAAC,YAAY,gBAAgB;AAC/B,eAAS,OAAO,OAAO,CAAC,QAAQ,CAAC,IAAI,UAAU;AAAA,IACjD;AAGA,QAAI,YAAY,YAAY;AAE1B,eAAS,OAAO,OAAO,CAAC,QAAQ,YAAY,SAAS,IAAI,IAAI,CAAC;AAAA,IAChE,WAAW,OAAO,KAAK,YAAY,UAAU,EAAE,SAAS,GAAG;AAEzD,eAAS,OAAO,OAAO,CAAC,QAAQ;AAC9B,eAAO,OAAO,QAAQ,YAAY,UAAU,EAAE;AAAA,UAC5C,CAAC,CAAC,QAAQ,KAAK,MAAM;;AACnB,kBAAM,UAAU,GAAG,MAAM,IAAI,KAAK;AAClC,oBAAO,SAAI,SAAJ,mBAAU;AAAA,cACf,CAAC,QAAQ,IAAI,YAAA,MAAkB,QAAQ,YAAA;AAAA;AAAA,UAE3C;AAAA,QAAA;AAAA,MAEJ,CAAC;AAAA,IACH;AAGA,aAAS,WAAW,QAAQ,mBAAmB;AAE/C,WAAO;AAAA,EACT,GAAG;AAAA,IACD;AAAA,IACA;AAAA,IACA,YAAY;AAAA,IACZ,YAAY;AAAA,IACZ,YAAY;AAAA,IACZ;AAAA,EAAA,CACD;AAGD,QAAM,EAAE,UAAU,aAAa,gBAAA,IAAoB,aAAa;AAAA,IAC9D;AAAA,IACA;AAAA,IACA,aAAa;AAAA,EAAA,CACd;AAED,QAAM,iBAAiB,CAAC,QAAuB;AAC7C,uBAAmB,IAAI,IAAI;AAAA,EAC7B;AAEA,MAAI,eAAe;AACjB,WAAO,oBAAC,OAAA,EAAI,WAAU,8BAA6B,UAAA,YAAQ;AAAA,EAC7D;AAGA,QAAM,qBAAqB,gBAAgB,CAAC;AAE5C,SACE,qBAAC,OAAA,EAAI,WAAU,gCACb,UAAA;AAAA,IAAA,oBAAC,OAAA,EAAI,WAAU,YACb,UAAA,oBAAC,kBAAe,GAClB;AAAA,IAEA,oBAAC,OAAA,EAAI,WAAU,YACb,UAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,YAAY;AAAA,QACZ;AAAA,QACA;AAAA,QACA;AAAA,MAAA;AAAA,IAAA,GAEJ;AAAA,IAEA,oBAAC,SAAI,WAAU,kBACZ,uBAAa,WAAW,yBACtB,OAAA,EACC,UAAA;AAAA,MAAA,qBAAC,aAAA,EACC,UAAA;AAAA,QAAA,oBAAC,cAAW,SAAQ,QAClB,8BAAC,GAAA,EAAE,WAAU,WAAU,EAAA,CACzB;AAAA,6BACC,YAAA,EAAW,UAAA;AAAA,UAAA;AAAA,UACI,eAAe,SAAS,WAAW;AAAA,QAAA,GACnD;AAAA,QACA,oBAAC,oBAAiB,UAAA,uCAAA,CAElB;AAAA,MAAA,GACF;AAAA,MACA,oBAAC,gBACE,UAAA,eACC;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,SAAQ;AAAA,UACR,SAAS,MAAM;AACb,2BAAe,EAAE;AACjB,+BAAmB,MAAS;AAAA,UAC9B;AAAA,UACA,WAAU;AAAA,UAEV,UAAA;AAAA,YAAA,oBAAC,GAAA,EAAE,WAAU,UAAA,CAAU;AAAA,YAAE;AAAA,UAAA;AAAA,QAAA;AAAA,MAAA,EAE3B,CAEJ;AAAA,IAAA,EAAA,CACF,IAEA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,MAAM;AAAA,QACN;AAAA,QACA;AAAA,QACA,YAAY;AAAA,QACZ,WAAW,CAAC,CAAC;AAAA,MAAA;AAAA,IAAA,EACf,CAEJ;AAAA,EAAA,GACF;AAEJ;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@igstack/app-catalog-frontend-core",
3
- "version": "0.1.1-alpha-20260304050203",
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-backend-core": "0.1.1-alpha-20260304050203",
137
- "@igstack/app-catalog-shared-core": "0.1.1-alpha-20260304050203"
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;"}