@igstack/app-catalog-frontend-core 0.2.0 → 0.3.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.
Files changed (192) hide show
  1. package/dist/esm/api/infra/trpc.d.ts +0 -1491
  2. package/dist/esm/modules/appCatalog/context/AppCatalogContext.js +1 -0
  3. package/dist/esm/modules/appCatalog/context/AppCatalogContext.js.map +1 -1
  4. package/dist/esm/modules/appCatalog/ui/filters/FilterBar.js +23 -11
  5. package/dist/esm/modules/appCatalog/ui/filters/FilterBar.js.map +1 -1
  6. package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.d.ts +5 -1
  7. package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js +146 -56
  8. package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js.map +1 -1
  9. package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js +20 -1
  10. package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js.map +1 -1
  11. package/dist/esm/modules/auth/AuthContext.js +1 -1
  12. package/dist/esm/modules/auth/AuthModalContext.js +1 -1
  13. package/dist/esm/modules/auth/authClient.d.ts +2 -2
  14. package/dist/esm/node_modules/.pnpm/zod@4.3.5/node_modules/zod/v4/classic/schemas.js +4 -37
  15. package/dist/esm/node_modules/.pnpm/zod@4.3.5/node_modules/zod/v4/classic/schemas.js.map +1 -1
  16. package/dist/esm/node_modules/.pnpm/zod@4.3.5/node_modules/zod/v4/core/api.js +2 -10
  17. package/dist/esm/node_modules/.pnpm/zod@4.3.5/node_modules/zod/v4/core/api.js.map +1 -1
  18. package/dist/esm/node_modules/.pnpm/zod@4.3.5/node_modules/zod/v4/core/checks.js +1 -1
  19. package/dist/esm/node_modules/.pnpm/zod@4.3.5/node_modules/zod/v4/core/json-schema-processors.js +0 -44
  20. package/dist/esm/node_modules/.pnpm/zod@4.3.5/node_modules/zod/v4/core/json-schema-processors.js.map +1 -1
  21. package/dist/esm/node_modules/.pnpm/zod@4.3.5/node_modules/zod/v4/core/parse.js +0 -4
  22. package/dist/esm/node_modules/.pnpm/zod@4.3.5/node_modules/zod/v4/core/parse.js.map +1 -1
  23. package/dist/esm/node_modules/.pnpm/zod@4.3.5/node_modules/zod/v4/core/regexes.js +0 -2
  24. package/dist/esm/node_modules/.pnpm/zod@4.3.5/node_modules/zod/v4/core/regexes.js.map +1 -1
  25. package/dist/esm/node_modules/.pnpm/zod@4.3.5/node_modules/zod/v4/core/schemas.js +4 -49
  26. package/dist/esm/node_modules/.pnpm/zod@4.3.5/node_modules/zod/v4/core/schemas.js.map +1 -1
  27. package/dist/esm/routeTree.gen.d.ts +3 -164
  28. package/dist/esm/routeTree.gen.js +8 -80
  29. package/dist/esm/routeTree.gen.js.map +1 -1
  30. package/dist/esm/ui/button.d.ts +1 -1
  31. package/dist/esm/ui/card.js +1 -48
  32. package/dist/esm/ui/card.js.map +1 -1
  33. package/dist/esm/ui/command.js +1 -15
  34. package/dist/esm/ui/command.js.map +1 -1
  35. package/dist/esm/ui/components/header/Header.js +2 -11
  36. package/dist/esm/ui/components/header/Header.js.map +1 -1
  37. package/dist/esm/ui/input-group.js +125 -0
  38. package/dist/esm/ui/input-group.js.map +1 -0
  39. package/package.json +3 -3
  40. package/src/modules/appCatalog/ui/components/AppDetailModal.tsx +2 -21
  41. package/src/routeTree.gen.ts +2 -220
  42. package/src/ui/components/header/Header.tsx +2 -12
  43. package/dist/esm/components/IconPickerDialog.d.ts +0 -8
  44. package/dist/esm/components/IconPickerDialog.js +0 -98
  45. package/dist/esm/components/IconPickerDialog.js.map +0 -1
  46. package/dist/esm/components/IconPickerField.d.ts +0 -9
  47. package/dist/esm/components/IconPickerField.js +0 -76
  48. package/dist/esm/components/IconPickerField.js.map +0 -1
  49. package/dist/esm/modules/admin-base/components/AdminChat.d.ts +0 -1
  50. package/dist/esm/modules/admin-base/components/AdminChat.js +0 -82
  51. package/dist/esm/modules/admin-base/components/AdminChat.js.map +0 -1
  52. package/dist/esm/modules/admin-base/components/AdminLayout.d.ts +0 -5
  53. package/dist/esm/modules/admin-base/components/AdminLayout.js +0 -83
  54. package/dist/esm/modules/admin-base/components/AdminLayout.js.map +0 -1
  55. package/dist/esm/modules/admin-base/components/AdminWelcome.d.ts +0 -1
  56. package/dist/esm/modules/admin-base/components/AdminWelcome.js +0 -37
  57. package/dist/esm/modules/admin-base/components/AdminWelcome.js.map +0 -1
  58. package/dist/esm/modules/admin-base/context/AdminConfigContext.d.ts +0 -8
  59. package/dist/esm/modules/admin-base/context/AdminConfigContext.js +0 -27
  60. package/dist/esm/modules/admin-base/context/AdminConfigContext.js.map +0 -1
  61. package/dist/esm/modules/admin-base/index.d.ts +0 -5
  62. package/dist/esm/modules/admin-base/types/adminTypes.d.ts +0 -10
  63. package/dist/esm/modules/appCatalog/AppCatalogAdminPage.d.ts +0 -1
  64. package/dist/esm/modules/appCatalog/AppCatalogAdminPage.js +0 -196
  65. package/dist/esm/modules/appCatalog/AppCatalogAdminPage.js.map +0 -1
  66. package/dist/esm/modules/appCatalog/ScreenshotItem.js +0 -57
  67. package/dist/esm/modules/appCatalog/ScreenshotItem.js.map +0 -1
  68. package/dist/esm/modules/appCatalog/ScreenshotManager.js +0 -155
  69. package/dist/esm/modules/appCatalog/ScreenshotManager.js.map +0 -1
  70. package/dist/esm/modules/approvalMethod/AccessRequestFormFields.d.ts +0 -7
  71. package/dist/esm/modules/approvalMethod/AccessRequestFormFields.js +0 -323
  72. package/dist/esm/modules/approvalMethod/AccessRequestFormFields.js.map +0 -1
  73. package/dist/esm/modules/approvalMethod/ApprovalMethodForm.d.ts +0 -14
  74. package/dist/esm/modules/approvalMethod/ApprovalMethodForm.js +0 -227
  75. package/dist/esm/modules/approvalMethod/ApprovalMethodForm.js.map +0 -1
  76. package/dist/esm/modules/approvalMethod/ApprovalMethodSelector.d.ts +0 -7
  77. package/dist/esm/modules/approvalMethod/ApprovalMethodSelector.js +0 -124
  78. package/dist/esm/modules/approvalMethod/ApprovalMethodSelector.js.map +0 -1
  79. package/dist/esm/modules/approvalMethod/api/ApiQueryMagazineApprovalMethod.d.ts +0 -381
  80. package/dist/esm/modules/approvalMethod/api/ApiQueryMagazineApprovalMethod.js +0 -26
  81. package/dist/esm/modules/approvalMethod/api/ApiQueryMagazineApprovalMethod.js.map +0 -1
  82. package/dist/esm/modules/auth/authUtils.js +0 -25
  83. package/dist/esm/modules/auth/authUtils.js.map +0 -1
  84. package/dist/esm/modules/icons/IconManagementPage.d.ts +0 -1
  85. package/dist/esm/modules/icons/IconManagementPage.js +0 -177
  86. package/dist/esm/modules/icons/IconManagementPage.js.map +0 -1
  87. package/dist/esm/node_modules/.pnpm/@dnd-kit_accessibility@3.1.1_react@19.1.2/node_modules/@dnd-kit/accessibility/dist/accessibility.esm.js +0 -60
  88. package/dist/esm/node_modules/.pnpm/@dnd-kit_accessibility@3.1.1_react@19.1.2/node_modules/@dnd-kit/accessibility/dist/accessibility.esm.js.map +0 -1
  89. package/dist/esm/node_modules/.pnpm/@dnd-kit_core@6.3.1_react-dom@19.1.2_react@19.1.2__react@19.1.2/node_modules/@dnd-kit/core/dist/core.esm.js +0 -3055
  90. package/dist/esm/node_modules/.pnpm/@dnd-kit_core@6.3.1_react-dom@19.1.2_react@19.1.2__react@19.1.2/node_modules/@dnd-kit/core/dist/core.esm.js.map +0 -1
  91. package/dist/esm/node_modules/.pnpm/@dnd-kit_sortable@10.0.0_@dnd-kit_core@6.3.1_react-dom@19.1.2_react@19.1.2__react@19.1.2__react@19.1.2/node_modules/@dnd-kit/sortable/dist/sortable.esm.js +0 -593
  92. package/dist/esm/node_modules/.pnpm/@dnd-kit_sortable@10.0.0_@dnd-kit_core@6.3.1_react-dom@19.1.2_react@19.1.2__react@19.1.2__react@19.1.2/node_modules/@dnd-kit/sortable/dist/sortable.esm.js.map +0 -1
  93. package/dist/esm/node_modules/.pnpm/@dnd-kit_utilities@3.2.2_react@19.1.2/node_modules/@dnd-kit/utilities/dist/utilities.esm.js +0 -302
  94. package/dist/esm/node_modules/.pnpm/@dnd-kit_utilities@3.2.2_react@19.1.2/node_modules/@dnd-kit/utilities/dist/utilities.esm.js.map +0 -1
  95. package/dist/esm/node_modules/.pnpm/@hookform_resolvers@5.2.2_react-hook-form@7.71.1_react@19.1.2_/node_modules/@hookform/resolvers/dist/resolvers.js +0 -34
  96. package/dist/esm/node_modules/.pnpm/@hookform_resolvers@5.2.2_react-hook-form@7.71.1_react@19.1.2_/node_modules/@hookform/resolvers/dist/resolvers.js.map +0 -1
  97. package/dist/esm/node_modules/.pnpm/@hookform_resolvers@5.2.2_react-hook-form@7.71.1_react@19.1.2_/node_modules/@hookform/resolvers/zod/dist/zod.js +0 -94
  98. package/dist/esm/node_modules/.pnpm/@hookform_resolvers@5.2.2_react-hook-form@7.71.1_react@19.1.2_/node_modules/@hookform/resolvers/zod/dist/zod.js.map +0 -1
  99. package/dist/esm/node_modules/.pnpm/react-hook-form@7.71.1_react@19.1.2/node_modules/react-hook-form/dist/index.esm.js +0 -1894
  100. package/dist/esm/node_modules/.pnpm/react-hook-form@7.71.1_react@19.1.2/node_modules/react-hook-form/dist/index.esm.js.map +0 -1
  101. package/dist/esm/routes/admin/app-for-catalog/$id.d.ts +0 -5
  102. package/dist/esm/routes/admin/app-for-catalog/_id.js +0 -67
  103. package/dist/esm/routes/admin/app-for-catalog/_id.js.map +0 -1
  104. package/dist/esm/routes/admin/app-for-catalog/_id2.js +0 -321
  105. package/dist/esm/routes/admin/app-for-catalog/_id2.js.map +0 -1
  106. package/dist/esm/routes/admin/app-for-catalog/index.d.ts +0 -1
  107. package/dist/esm/routes/admin/app-for-catalog/index.js +0 -9
  108. package/dist/esm/routes/admin/app-for-catalog/index.js.map +0 -1
  109. package/dist/esm/routes/admin/app-for-catalog/index2.js +0 -12
  110. package/dist/esm/routes/admin/app-for-catalog/index2.js.map +0 -1
  111. package/dist/esm/routes/admin/app-for-catalog.d.ts +0 -1
  112. package/dist/esm/routes/admin/app-for-catalog.js +0 -14
  113. package/dist/esm/routes/admin/app-for-catalog.js.map +0 -1
  114. package/dist/esm/routes/admin/app-for-catalog2.js +0 -9
  115. package/dist/esm/routes/admin/app-for-catalog2.js.map +0 -1
  116. package/dist/esm/routes/admin/approval-methods/index.d.ts +0 -32
  117. package/dist/esm/routes/admin/approval-methods/index.js +0 -24
  118. package/dist/esm/routes/admin/approval-methods/index.js.map +0 -1
  119. package/dist/esm/routes/admin/approval-methods/index2.js +0 -100
  120. package/dist/esm/routes/admin/approval-methods/index2.js.map +0 -1
  121. package/dist/esm/routes/admin/approval-methods.d.ts +0 -1
  122. package/dist/esm/routes/admin/approval-methods.js +0 -14
  123. package/dist/esm/routes/admin/approval-methods.js.map +0 -1
  124. package/dist/esm/routes/admin/approval-methods2.js +0 -7
  125. package/dist/esm/routes/admin/approval-methods2.js.map +0 -1
  126. package/dist/esm/routes/admin/chat.d.ts +0 -1
  127. package/dist/esm/routes/admin/chat.js +0 -14
  128. package/dist/esm/routes/admin/chat.js.map +0 -1
  129. package/dist/esm/routes/admin/chat2.js +0 -9
  130. package/dist/esm/routes/admin/chat2.js.map +0 -1
  131. package/dist/esm/routes/admin/icons.d.ts +0 -1
  132. package/dist/esm/routes/admin/icons.js +0 -14
  133. package/dist/esm/routes/admin/icons.js.map +0 -1
  134. package/dist/esm/routes/admin/icons2.js +0 -12
  135. package/dist/esm/routes/admin/icons2.js.map +0 -1
  136. package/dist/esm/routes/admin/index.d.ts +0 -1
  137. package/dist/esm/routes/admin/index.js +0 -9
  138. package/dist/esm/routes/admin/index.js.map +0 -1
  139. package/dist/esm/routes/admin/index2.js +0 -9
  140. package/dist/esm/routes/admin/index2.js.map +0 -1
  141. package/dist/esm/routes/admin.d.ts +0 -1
  142. package/dist/esm/routes/admin.js +0 -37
  143. package/dist/esm/routes/admin.js.map +0 -1
  144. package/dist/esm/routes/admin2.js +0 -18
  145. package/dist/esm/routes/admin2.js.map +0 -1
  146. package/dist/esm/ui/alert-dialog.js +0 -141
  147. package/dist/esm/ui/alert-dialog.js.map +0 -1
  148. package/dist/esm/ui/breadcrumb.js +0 -84
  149. package/dist/esm/ui/breadcrumb.js.map +0 -1
  150. package/dist/esm/ui/components/Breadcrumbs.js +0 -36
  151. package/dist/esm/ui/components/Breadcrumbs.js.map +0 -1
  152. package/dist/esm/ui/crud-list/CrudList.js +0 -189
  153. package/dist/esm/ui/crud-list/CrudList.js.map +0 -1
  154. package/dist/esm/ui/editable-list/EditableListField.js +0 -130
  155. package/dist/esm/ui/editable-list/EditableListField.js.map +0 -1
  156. package/dist/esm/ui/form.js +0 -134
  157. package/dist/esm/ui/form.js.map +0 -1
  158. package/dist/esm/ui/linkExternal.js +0 -26
  159. package/dist/esm/ui/linkExternal.js.map +0 -1
  160. package/dist/esm/ui/markdown-editor/MarkdownEditor.js +0 -116
  161. package/dist/esm/ui/markdown-editor/MarkdownEditor.js.map +0 -1
  162. package/dist/esm/ui/markdown-editor/MarkdownToolbar.js +0 -99
  163. package/dist/esm/ui/markdown-editor/MarkdownToolbar.js.map +0 -1
  164. package/dist/esm/ui/scroll-area.js +0 -62
  165. package/dist/esm/ui/scroll-area.js.map +0 -1
  166. package/dist/esm/ui/select.js +0 -138
  167. package/dist/esm/ui/select.js.map +0 -1
  168. package/dist/esm/ui/textarea.js +0 -19
  169. package/dist/esm/ui/textarea.js.map +0 -1
  170. package/src/components/IconPickerDialog.tsx +0 -136
  171. package/src/components/IconPickerField.tsx +0 -88
  172. package/src/modules/admin-base/components/AdminChat.tsx +0 -122
  173. package/src/modules/admin-base/components/AdminLayout.tsx +0 -111
  174. package/src/modules/admin-base/components/AdminWelcome.tsx +0 -52
  175. package/src/modules/admin-base/context/AdminConfigContext.tsx +0 -36
  176. package/src/modules/admin-base/index.ts +0 -16
  177. package/src/modules/admin-base/types/adminTypes.ts +0 -11
  178. package/src/modules/appCatalog/AppCatalogAdminPage.tsx +0 -274
  179. package/src/modules/approvalMethod/AccessRequestFormFields.tsx +0 -393
  180. package/src/modules/approvalMethod/ApprovalMethodForm.tsx +0 -323
  181. package/src/modules/approvalMethod/ApprovalMethodSelector.tsx +0 -150
  182. package/src/modules/approvalMethod/api/ApiQueryMagazineApprovalMethod.ts +0 -34
  183. package/src/modules/icons/IconManagementPage.tsx +0 -245
  184. package/src/routes/admin/app-for-catalog/$id.tsx +0 -571
  185. package/src/routes/admin/app-for-catalog/index.tsx +0 -19
  186. package/src/routes/admin/app-for-catalog.tsx +0 -12
  187. package/src/routes/admin/approval-methods/index.tsx +0 -161
  188. package/src/routes/admin/approval-methods.tsx +0 -10
  189. package/src/routes/admin/chat.tsx +0 -13
  190. package/src/routes/admin/icons.tsx +0 -22
  191. package/src/routes/admin/index.tsx +0 -9
  192. package/src/routes/admin.tsx +0 -60
@@ -2,6 +2,7 @@ import { jsx } from "react/jsx-runtime";
2
2
  import { useQuery } from "@tanstack/react-query";
3
3
  import { use, createContext, useMemo, useEffect } from "react";
4
4
  import { ApiQueryMagazineAppCatalog } from "../api/ApiQueryMagazineAppCatalog.js";
5
+ import "../ui/context/AppCatalogFiltersContext.js";
5
6
  import "@tanstack/react-query-devtools";
6
7
  import "next-themes";
7
8
  import "@radix-ui/react-dialog";
@@ -1 +1 @@
1
- {"version":3,"file":"AppCatalogContext.js","sources":["../../../../../src/modules/appCatalog/context/AppCatalogContext.tsx"],"sourcesContent":["import type {\n AppApprovalMethod,\n AppForCatalog,\n GroupingTagDefinition,\n} from '@igstack/app-catalog-backend-core'\nimport { useQuery } from '@tanstack/react-query'\nimport type { ReactNode } from 'react'\nimport { createContext, use, useEffect, useMemo } from 'react'\nimport { ApiQueryMagazineAppCatalog } from '~/modules/appCatalog'\n\nexport interface AppCatalogContextIface {\n apps: Array<AppForCatalog>\n isLoadingApps: boolean\n tagsDefinitions: Array<GroupingTagDefinition>\n approvalMethods: Array<AppApprovalMethod>\n appVersion?: { displayName: string; url?: string }\n}\n\nexport const AppCatalogContext = createContext<\n AppCatalogContextIface | undefined\n>(undefined)\n\ninterface AppCatalogProviderProps {\n children: ReactNode\n}\n\nexport function AppCatalogProvider({ children }: AppCatalogProviderProps) {\n const { data, isLoading: isLoadingApps } = useQuery(\n ApiQueryMagazineAppCatalog.getAppCatalog(),\n )\n\n const contextValue = useMemo<AppCatalogContextIface>(\n () => ({\n apps: data?.apps ?? [],\n isLoadingApps,\n tagsDefinitions: data?.tagsDefinitions ?? [],\n approvalMethods: data?.approvalMethods ?? [],\n appVersion: data?.appVersion,\n }),\n [\n data?.approvalMethods,\n data?.apps,\n data?.tagsDefinitions,\n data?.appVersion,\n isLoadingApps,\n ],\n )\n\n // Update document title based on app version\n useEffect(() => {\n if (data?.appVersion?.displayName === 'local') {\n document.title = 'Local'\n } else {\n document.title = 'App Catalog'\n }\n }, [data?.appVersion?.displayName])\n\n return <AppCatalogContext value={contextValue}>{children}</AppCatalogContext>\n}\n\nexport function useAppCatalogContext(): AppCatalogContextIface {\n const context = use(AppCatalogContext)\n if (!context) {\n throw new Error(\n 'useAppCatalogContext must be used within AppCatalogProvider',\n )\n }\n return context\n}\n"],"names":["_a"],"mappings":";;;;;;;;;;;;;;;AAkBO,MAAM,oBAAoB,cAE/B,MAAS;AAMJ,SAAS,mBAAmB,EAAE,YAAqC;;AACxE,QAAM,EAAE,MAAM,WAAW,cAAA,IAAkB;AAAA,IACzC,2BAA2B,cAAA;AAAA,EAAc;AAG3C,QAAM,eAAe;AAAA,IACnB,OAAO;AAAA,MACL,OAAM,6BAAM,SAAQ,CAAA;AAAA,MACpB;AAAA,MACA,kBAAiB,6BAAM,oBAAmB,CAAA;AAAA,MAC1C,kBAAiB,6BAAM,oBAAmB,CAAA;AAAA,MAC1C,YAAY,6BAAM;AAAA,IAAA;AAAA,IAEpB;AAAA,MACE,6BAAM;AAAA,MACN,6BAAM;AAAA,MACN,6BAAM;AAAA,MACN,6BAAM;AAAA,MACN;AAAA,IAAA;AAAA,EACF;AAIF,YAAU,MAAM;;AACd,UAAIA,MAAA,6BAAM,eAAN,gBAAAA,IAAkB,iBAAgB,SAAS;AAC7C,eAAS,QAAQ;AAAA,IACnB,OAAO;AACL,eAAS,QAAQ;AAAA,IACnB;AAAA,EACF,GAAG,EAAC,kCAAM,eAAN,mBAAkB,WAAW,CAAC;AAElC,SAAO,oBAAC,mBAAA,EAAkB,OAAO,cAAe,SAAA,CAAS;AAC3D;AAEO,SAAS,uBAA+C;AAC7D,QAAM,UAAU,IAAI,iBAAiB;AACrC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAAA,EAEJ;AACA,SAAO;AACT;"}
1
+ {"version":3,"file":"AppCatalogContext.js","sources":["../../../../../src/modules/appCatalog/context/AppCatalogContext.tsx"],"sourcesContent":["import type {\n AppApprovalMethod,\n AppForCatalog,\n GroupingTagDefinition,\n} from '@igstack/app-catalog-backend-core'\nimport { useQuery } from '@tanstack/react-query'\nimport type { ReactNode } from 'react'\nimport { createContext, use, useEffect, useMemo } from 'react'\nimport { ApiQueryMagazineAppCatalog } from '~/modules/appCatalog'\n\nexport interface AppCatalogContextIface {\n apps: Array<AppForCatalog>\n isLoadingApps: boolean\n tagsDefinitions: Array<GroupingTagDefinition>\n approvalMethods: Array<AppApprovalMethod>\n appVersion?: { displayName: string; url?: string }\n}\n\nexport const AppCatalogContext = createContext<\n AppCatalogContextIface | undefined\n>(undefined)\n\ninterface AppCatalogProviderProps {\n children: ReactNode\n}\n\nexport function AppCatalogProvider({ children }: AppCatalogProviderProps) {\n const { data, isLoading: isLoadingApps } = useQuery(\n ApiQueryMagazineAppCatalog.getAppCatalog(),\n )\n\n const contextValue = useMemo<AppCatalogContextIface>(\n () => ({\n apps: data?.apps ?? [],\n isLoadingApps,\n tagsDefinitions: data?.tagsDefinitions ?? [],\n approvalMethods: data?.approvalMethods ?? [],\n appVersion: data?.appVersion,\n }),\n [\n data?.approvalMethods,\n data?.apps,\n data?.tagsDefinitions,\n data?.appVersion,\n isLoadingApps,\n ],\n )\n\n // Update document title based on app version\n useEffect(() => {\n if (data?.appVersion?.displayName === 'local') {\n document.title = 'Local'\n } else {\n document.title = 'App Catalog'\n }\n }, [data?.appVersion?.displayName])\n\n return <AppCatalogContext value={contextValue}>{children}</AppCatalogContext>\n}\n\nexport function useAppCatalogContext(): AppCatalogContextIface {\n const context = use(AppCatalogContext)\n if (!context) {\n throw new Error(\n 'useAppCatalogContext must be used within AppCatalogProvider',\n )\n }\n return context\n}\n"],"names":["_a"],"mappings":";;;;;;;;;;;;;;;;AAkBO,MAAM,oBAAoB,cAE/B,MAAS;AAMJ,SAAS,mBAAmB,EAAE,YAAqC;;AACxE,QAAM,EAAE,MAAM,WAAW,cAAA,IAAkB;AAAA,IACzC,2BAA2B,cAAA;AAAA,EAAc;AAG3C,QAAM,eAAe;AAAA,IACnB,OAAO;AAAA,MACL,OAAM,6BAAM,SAAQ,CAAA;AAAA,MACpB;AAAA,MACA,kBAAiB,6BAAM,oBAAmB,CAAA;AAAA,MAC1C,kBAAiB,6BAAM,oBAAmB,CAAA;AAAA,MAC1C,YAAY,6BAAM;AAAA,IAAA;AAAA,IAEpB;AAAA,MACE,6BAAM;AAAA,MACN,6BAAM;AAAA,MACN,6BAAM;AAAA,MACN,6BAAM;AAAA,MACN;AAAA,IAAA;AAAA,EACF;AAIF,YAAU,MAAM;;AACd,UAAIA,MAAA,6BAAM,eAAN,gBAAAA,IAAkB,iBAAgB,SAAS;AAC7C,eAAS,QAAQ;AAAA,IACnB,OAAO;AACL,eAAS,QAAQ;AAAA,IACnB;AAAA,EACF,GAAG,EAAC,kCAAM,eAAN,mBAAkB,WAAW,CAAC;AAElC,SAAO,oBAAC,mBAAA,EAAkB,OAAO,cAAe,SAAA,CAAS;AAC3D;AAEO,SAAS,uBAA+C;AAC7D,QAAM,UAAU,IAAI,iBAAiB;AACrC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAAA,EAEJ;AACA,SAAO;AACT;"}
@@ -1,8 +1,9 @@
1
1
  import { jsxs, jsx } from "react/jsx-runtime";
2
+ import { X } from "lucide-react";
2
3
  import { useMemo } from "react";
3
4
  import { Button } from "../../../../ui/button.js";
4
5
  import { Checkbox } from "../../../../ui/checkbox.js";
5
- import { Input } from "../../../../ui/input.js";
6
+ import { InputGroup, InputGroupInput, InputGroupAddon, InputGroupButton } from "../../../../ui/input-group.js";
6
7
  import { Label } from "../../../../ui/label.js";
7
8
  import { useAppCatalogFilters } from "../context/AppCatalogFiltersContext.js";
8
9
  import { FilterCombobox } from "./FilterCombobox.js";
@@ -40,16 +41,27 @@ function FilterBar({
40
41
  data.availableTagsByPrefix
41
42
  ]);
42
43
  return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 mb-4", children: [
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
- ),
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
+ ] }),
53
65
  /* @__PURE__ */ jsx("div", { className: "h-8 w-px bg-border" }),
54
66
  /* @__PURE__ */ jsxs("div", { className: "flex items-center rounded-md border", children: [
55
67
  /* @__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 { 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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"}
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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"}
@@ -6,5 +6,9 @@ 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;
9
13
  }
10
- export declare function AppCatalogGrid({ apps, selectedAppSlug, groupingDefinition, onAppClick, hasSearch, }: AppCatalogGridProps): import("react/jsx-runtime").JSX.Element;
14
+ export declare function AppCatalogGrid({ apps, selectedAppSlug, groupingDefinition, onAppClick, hasSearch, totalAppsCount, onClearFilters, }: AppCatalogGridProps): import("react/jsx-runtime").JSX.Element;
@@ -2,6 +2,7 @@ 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";
5
6
  import { cn } from "../../../../lib/utils.js";
6
7
  import { Badge } from "../../../../ui/badge.js";
7
8
  import { Button } from "../../../../ui/button.js";
@@ -70,13 +71,37 @@ function AppScreenshot({ app }) {
70
71
  }
71
72
  function AppDetails({
72
73
  app,
73
- onAppClick
74
+ onAppClick,
75
+ onClosePanel
74
76
  }) {
75
77
  var _a;
76
78
  const [isGalleryOpen, setIsGalleryOpen] = React__default.useState(false);
77
79
  const [galleryInitialIndex, setGalleryInitialIndex] = React__default.useState(0);
78
80
  const { approvalMethods, apps } = useAppCatalogContext();
79
81
  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
+ );
80
105
  const handleScreenshotClick = (index) => {
81
106
  setGalleryInitialIndex(index);
82
107
  setIsGalleryOpen(true);
@@ -87,10 +112,10 @@ function AppDetails({
87
112
  }) : null;
88
113
  return /* @__PURE__ */ jsxs(Fragment, { children: [
89
114
  /* @__PURE__ */ jsxs("div", { className: "flex h-full flex-col p-6", children: [
90
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-4 border-b pb-6", children: [
115
+ /* @__PURE__ */ jsx("div", { className: "border-b pb-6", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
91
116
  /* @__PURE__ */ jsx(AppIcon, { app, className: "size-16" }),
92
- /* @__PURE__ */ jsxs("div", { children: [
93
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
117
+ /* @__PURE__ */ jsxs("div", { className: "-mx-3", children: [
118
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-3", children: [
94
119
  /* @__PURE__ */ jsx("h2", { className: "text-2xl font-semibold", children: app.displayName }),
95
120
  app.deprecated && (() => {
96
121
  const deprecationType = app.deprecated.type || "deprecated";
@@ -110,15 +135,15 @@ function AppDetails({
110
135
  target: "_blank",
111
136
  rel: "noopener noreferrer",
112
137
  onClick: () => recordClick(app.slug),
113
- className: "mt-1 inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-primary",
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",
114
139
  children: [
115
140
  app.appUrl.replaceAll(/https?:\/\//g, ""),
116
- /* @__PURE__ */ jsx(ExternalLink, { className: "size-3" })
141
+ /* @__PURE__ */ jsx(ExternalLink, { className: "size-3.5 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity" })
117
142
  ]
118
143
  }
119
144
  )
120
145
  ] })
121
- ] }),
146
+ ] }) }),
122
147
  app.deprecated && (() => {
123
148
  const deprecationType = app.deprecated.type || "deprecated";
124
149
  const isDiscouraged = deprecationType === "discouraged";
@@ -178,6 +203,23 @@ function AppDetails({
178
203
  )
179
204
  ] }),
180
205
  /* @__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
+ ] }),
181
223
  app.tags && app.tags.length > 0 && /* @__PURE__ */ jsxs("div", { className: "mt-6", children: [
182
224
  /* @__PURE__ */ jsx("h3", { className: "mb-2 text-sm font-medium", children: "Tags" }),
183
225
  /* @__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)) })
@@ -185,6 +227,22 @@ function AppDetails({
185
227
  app.teams && app.teams.length > 0 && /* @__PURE__ */ jsxs("div", { className: "mt-6", children: [
186
228
  /* @__PURE__ */ jsx("h3", { className: "mb-2 text-sm font-medium", children: "Teams" }),
187
229
  /* @__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)) })
188
246
  ] })
189
247
  ] }),
190
248
  /* @__PURE__ */ jsx(
@@ -253,7 +311,9 @@ function AppCatalogGrid({
253
311
  selectedAppSlug,
254
312
  groupingDefinition,
255
313
  onAppClick,
256
- hasSearch = false
314
+ hasSearch = false,
315
+ totalAppsCount,
316
+ onClearFilters
257
317
  }) {
258
318
  const selectedApp = selectedAppSlug ? apps.find((a) => a.slug === selectedAppSlug) : null;
259
319
  const groupedApps = groupApps(apps, groupingDefinition, hasSearch);
@@ -350,55 +410,78 @@ function AppCatalogGrid({
350
410
  header.id
351
411
  );
352
412
  }) }, headerGroup.id)) }),
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(
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(
355
464
  TableCell,
356
465
  {
357
466
  colSpan: columns.length,
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 }) })
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
+ )
360
482
  }
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)) })
483
+ ) })
484
+ ] })
402
485
  ] }) })
403
486
  }
404
487
  ),
@@ -416,13 +499,20 @@ function AppCatalogGrid({
416
499
  {
417
500
  variant: "ghost",
418
501
  size: "icon",
419
- className: "absolute top-2 right-2 z-10",
502
+ className: "absolute top-4 right-4 z-10 hover:bg-accent",
420
503
  onClick: handleClosePanel,
421
504
  "aria-label": "Close details panel",
422
- children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" })
505
+ children: /* @__PURE__ */ jsx(X, { className: "h-5 w-5" })
423
506
  }
424
507
  ),
425
- /* @__PURE__ */ jsx(AppDetails, { app: selectedApp, onAppClick })
508
+ /* @__PURE__ */ jsx(
509
+ AppDetails,
510
+ {
511
+ app: selectedApp,
512
+ onAppClick,
513
+ onClosePanel: handleClosePanel
514
+ }
515
+ )
426
516
  ] }) : null })
427
517
  }
428
518
  )
@@ -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'\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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"}
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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"}
@@ -62,9 +62,26 @@ 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]);
65
70
  const handleAppClick = (app) => {
66
71
  setSelectedAppSlug(app.slug);
67
72
  };
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]);
68
85
  if (isLoadingApps) {
69
86
  return /* @__PURE__ */ jsx("div", { className: "py-6 text-muted-foreground", children: "Loading…" });
70
87
  }
@@ -111,7 +128,9 @@ function AppCatalogPage() {
111
128
  selectedAppSlug,
112
129
  groupingDefinition,
113
130
  onAppClick: handleAppClick,
114
- hasSearch: !!deferredSearchValue
131
+ hasSearch: !!deferredSearchValue,
132
+ totalAppsCount,
133
+ onClearFilters: handleClearFilters
115
134
  }
116
135
  ) })
117
136
  ] });