@igstack/app-catalog-frontend-core 0.3.1-alpha-20260405015231 → 0.3.1-alpha-20260724172703

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 (67) hide show
  1. package/dist/esm/__tests__/integration/harness/MockBackendVerifier.d.ts +3 -3
  2. package/dist/esm/__tests__/integration/mock-backend/MockBackendConfigurer.d.ts +4 -4
  3. package/dist/esm/__tests__/integration/mock-backend/MockDb.d.ts +11 -7
  4. package/dist/esm/api/infra/trpc.d.ts +3 -3
  5. package/dist/esm/modules/appCatalog/context/AppCatalogContext.d.ts +2 -3
  6. package/dist/esm/modules/appCatalog/context/AppCatalogContext.js +2 -4
  7. package/dist/esm/modules/appCatalog/context/AppCatalogContext.js.map +1 -1
  8. package/dist/esm/modules/appCatalog/hooks/useAppCounts.d.ts +2 -2
  9. package/dist/esm/modules/appCatalog/hooks/useAppCounts.js +3 -3
  10. package/dist/esm/modules/appCatalog/hooks/useAppCounts.js.map +1 -1
  11. package/dist/esm/modules/appCatalog/hooks/useUpdateApp.d.ts +9 -0
  12. package/dist/esm/modules/appCatalog/ui/components/AccessRequestSection.d.ts +2 -2
  13. package/dist/esm/modules/appCatalog/ui/components/AccessRequestSection.js +1 -13
  14. package/dist/esm/modules/appCatalog/ui/components/AccessRequestSection.js.map +1 -1
  15. package/dist/esm/modules/appCatalog/ui/components/AppDetailModal.d.ts +2 -2
  16. package/dist/esm/modules/appCatalog/ui/components/MarkdownText.d.ts +18 -0
  17. package/dist/esm/modules/appCatalog/ui/components/MarkdownText.js +19 -0
  18. package/dist/esm/modules/appCatalog/ui/components/MarkdownText.js.map +1 -0
  19. package/dist/esm/modules/appCatalog/ui/components/ScreenshotGallery.d.ts +2 -2
  20. package/dist/esm/modules/appCatalog/ui/components/ScreenshotGallery.js.map +1 -1
  21. package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.d.ts +2 -2
  22. package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js +12 -14
  23. package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js.map +1 -1
  24. package/dist/esm/modules/appCatalog/ui/components/TierVariantsSection.d.ts +2 -2
  25. package/dist/esm/modules/appCatalog/ui/components/TierVariantsSection.js.map +1 -1
  26. package/dist/esm/modules/appCatalog/ui/filters/FilterBar.d.ts +2 -2
  27. package/dist/esm/modules/appCatalog/ui/filters/FilterBar.js.map +1 -1
  28. package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.d.ts +3 -3
  29. package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js +19 -19
  30. package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js.map +1 -1
  31. package/dist/esm/modules/appCatalog/ui/grid/AppCatalogTable.d.ts +2 -2
  32. package/dist/esm/modules/appCatalog/ui/grid/appCatalogUtils.d.ts +2 -2
  33. package/dist/esm/modules/appCatalog/ui/hooks/useKeyboardNavigation.d.ts +3 -3
  34. package/dist/esm/modules/appCatalog/ui/hooks/useKeyboardNavigation.js.map +1 -1
  35. package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js +20 -12
  36. package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js.map +1 -1
  37. package/dist/esm/modules/appCatalog/utils/resolveHelpers.d.ts +5 -2
  38. package/dist/esm/modules/appCatalog/utils/resolveHelpers.js +4 -4
  39. package/dist/esm/modules/appCatalog/utils/resolveHelpers.js.map +1 -1
  40. package/dist/esm/modules/appCatalog/utils/searchApps.d.ts +11 -6
  41. package/dist/esm/modules/appCatalog/utils/searchApps.js +15 -14
  42. package/dist/esm/modules/appCatalog/utils/searchApps.js.map +1 -1
  43. package/package.json +3 -3
  44. package/src/__tests__/integration/appCatalog.integration.test.ts +3 -3
  45. package/src/__tests__/integration/harness/MockBackendVerifier.ts +5 -5
  46. package/src/__tests__/integration/mock-backend/MockBackendConfigurer.ts +16 -12
  47. package/src/__tests__/integration/mock-backend/MockDb.ts +30 -22
  48. package/src/__tests__/modules/appCatalog/ScreenshotPreview.test.tsx +4 -5
  49. package/src/__tests__/modules/appCatalog/utils/searchApps.test.ts +28 -30
  50. package/src/__tests__/modules/gallery/EscapeDismissal.integration.test.tsx +3 -4
  51. package/src/modules/appCatalog/context/AppCatalogContext.tsx +4 -8
  52. package/src/modules/appCatalog/hooks/useAppCounts.ts +5 -5
  53. package/src/modules/appCatalog/ui/components/AccessRequestSection.tsx +3 -20
  54. package/src/modules/appCatalog/ui/components/AppDetailModal.tsx +14 -11
  55. package/src/modules/appCatalog/ui/components/MarkdownText.tsx +42 -0
  56. package/src/modules/appCatalog/ui/components/ScreenshotGallery.tsx +2 -2
  57. package/src/modules/appCatalog/ui/components/SearchAndFilterHeader.tsx +6 -2
  58. package/src/modules/appCatalog/ui/components/SubResourcesSection.tsx +17 -17
  59. package/src/modules/appCatalog/ui/components/TierVariantsSection.tsx +2 -2
  60. package/src/modules/appCatalog/ui/filters/FilterBar.tsx +2 -2
  61. package/src/modules/appCatalog/ui/grid/AppCatalogGrid.tsx +34 -37
  62. package/src/modules/appCatalog/ui/grid/AppCatalogTable.tsx +3 -3
  63. package/src/modules/appCatalog/ui/grid/appCatalogUtils.ts +2 -2
  64. package/src/modules/appCatalog/ui/hooks/useKeyboardNavigation.ts +3 -3
  65. package/src/modules/appCatalog/ui/pages/AppCatalogPage.tsx +24 -14
  66. package/src/modules/appCatalog/utils/resolveHelpers.ts +13 -10
  67. package/src/modules/appCatalog/utils/searchApps.ts +36 -31
@@ -1,6 +1,6 @@
1
1
  import type {
2
- AppForCatalog,
3
2
  GroupingTagDefinition,
3
+ Resource,
4
4
  } from '@igstack/app-catalog-backend-core'
5
5
  import type { ColumnDef } from '@tanstack/react-table'
6
6
  import {
@@ -39,13 +39,13 @@ import { useKeyboardNavigation } from '../hooks/useKeyboardNavigation'
39
39
  import { highlightText } from '../../utils/searchApps'
40
40
  import { TierVariantsSection } from '../components/TierVariantsSection'
41
41
  import { SubResourcesSection } from '../components/SubResourcesSection'
42
- import { getSubResourcesForApp } from '../../utils/resolveHelpers'
42
+ import { getChildResources } from '../../utils/resolveHelpers'
43
43
 
44
44
  export interface AppCatalogGridProps {
45
- apps: AppForCatalog[]
45
+ apps: Resource[]
46
46
  selectedAppSlug?: string
47
47
  groupingDefinition?: GroupingTagDefinition
48
- onAppClick?: (app: AppForCatalog) => void
48
+ onAppClick?: (app: Resource) => void
49
49
  /** Whether search is active (affects group sorting) */
50
50
  hasSearch?: boolean
51
51
  /** Search query for highlighting matches */
@@ -91,13 +91,7 @@ function HighlightedText({
91
91
  )
92
92
  }
93
93
 
94
- function AppIcon({
95
- app,
96
- className,
97
- }: {
98
- app: AppForCatalog
99
- className?: string
100
- }) {
94
+ function AppIcon({ app, className }: { app: Resource; className?: string }) {
101
95
  const [imageError, setImageError] = React.useState(false)
102
96
 
103
97
  // Use iconName from backend if available
@@ -127,7 +121,7 @@ function AppIcon({
127
121
  )
128
122
  }
129
123
 
130
- function AppScreenshot({ app }: { app: AppForCatalog }) {
124
+ function AppScreenshot({ app }: { app: Resource }) {
131
125
  const [imageError, setImageError] = React.useState(false)
132
126
  const [isLoadingImage, setIsLoadingImage] = React.useState(true)
133
127
 
@@ -172,11 +166,11 @@ function AppScreenshot({ app }: { app: AppForCatalog }) {
172
166
  )
173
167
  }
174
168
 
175
- function TiersAndSubResourcesPanel({ app }: { app: AppForCatalog }) {
176
- const { subResources } = useAppCatalogContext()
169
+ function TiersAndSubResourcesPanel({ app }: { app: Resource }) {
170
+ const { resources } = useAppCatalogContext()
177
171
  const appSubResources = React.useMemo(
178
- () => getSubResourcesForApp(subResources ?? [], app.slug),
179
- [subResources, app.slug],
172
+ () => getChildResources(resources, app.slug),
173
+ [resources, app.slug],
180
174
  )
181
175
 
182
176
  return (
@@ -200,13 +194,13 @@ function AppDetails({
200
194
  onAppClick,
201
195
  onClosePanel,
202
196
  }: {
203
- app: AppForCatalog
204
- onAppClick?: (app: AppForCatalog) => void
197
+ app: Resource
198
+ onAppClick?: (app: Resource) => void
205
199
  onClosePanel: () => void
206
200
  }) {
207
201
  const [isGalleryOpen, setIsGalleryOpen] = React.useState(false)
208
202
  const [galleryInitialIndex, setGalleryInitialIndex] = React.useState(0)
209
- const { approvalMethods, apps } = useAppCatalogContext()
203
+ const { approvalMethods, resources: allResources } = useAppCatalogContext()
210
204
  const { recordClick } = useAppClickHistory()
211
205
  const updateApp = useUpdateApp()
212
206
  const [draftSource, setDraftSource] = React.useState<string | null>(null)
@@ -258,7 +252,7 @@ function AppDetails({
258
252
 
259
253
  // Find replacement app if deprecated
260
254
  const replacementApp = app.deprecated?.replacementSlug
261
- ? apps.find((a) => a.slug === app.deprecated?.replacementSlug)
255
+ ? allResources.find((a) => a.slug === app.deprecated?.replacementSlug)
262
256
  : null
263
257
 
264
258
  return (
@@ -621,11 +615,11 @@ function AppDetails({
621
615
 
622
616
  interface GroupedApps {
623
617
  groupName: string
624
- apps: AppForCatalog[]
618
+ apps: Resource[]
625
619
  }
626
620
 
627
621
  function groupApps(
628
- apps: AppForCatalog[],
622
+ apps: Resource[],
629
623
  groupingDef?: GroupingTagDefinition,
630
624
  hasSearch?: boolean,
631
625
  ): GroupedApps[] {
@@ -642,8 +636,8 @@ function groupApps(
642
636
  return [{ groupName: 'All Apps', apps: sortedApps }]
643
637
  }
644
638
 
645
- const grouped = new Map<string, AppForCatalog[]>()
646
- const ungrouped: AppForCatalog[] = []
639
+ const grouped = new Map<string, Resource[]>()
640
+ const ungrouped: Resource[] = []
647
641
 
648
642
  for (const app of apps) {
649
643
  const matchingTag = app.tags?.find((tag) =>
@@ -720,11 +714,11 @@ export function AppCatalogGrid({
720
714
  onAppClick,
721
715
  })
722
716
 
723
- // Build a map of appSlug -> matched sub-resource displayName for search annotation
724
- const { subResources: allSubResources } = useAppCatalogContext()
717
+ // Build a map of parentSlug -> matched child resource displayName for search annotation
718
+ const { resources: allResources2 } = useAppCatalogContext()
725
719
  const matchedSubResourceMap = React.useMemo(() => {
726
720
  const map = new Map<string, string>()
727
- if (!searchQuery?.trim() || !allSubResources?.length) return map
721
+ if (!searchQuery?.trim() || allResources2.length === 0) return map
728
722
  const queryTerms = searchQuery
729
723
  .trim()
730
724
  .toLowerCase()
@@ -733,22 +727,25 @@ export function AppCatalogGrid({
733
727
  const allTermsMatch = (text: string): boolean =>
734
728
  queryTerms.every((term) => text.includes(term))
735
729
 
736
- for (const sr of allSubResources) {
737
- if (map.has(sr.appSlug)) continue
738
- const nameMatch = allTermsMatch(sr.displayName.toLowerCase())
739
- const aliasMatch = sr.aliases.some((a) => allTermsMatch(a.toLowerCase()))
740
- const descMatch = sr.description
741
- ? allTermsMatch(sr.description.toLowerCase())
730
+ for (const r of allResources2) {
731
+ if (!r.parentSlug) continue
732
+ if (map.has(r.parentSlug)) continue
733
+ const nameMatch = allTermsMatch(r.displayName.toLowerCase())
734
+ const aliasMatch = (r.aliases ?? []).some((a) =>
735
+ allTermsMatch(a.toLowerCase()),
736
+ )
737
+ const descMatch = r.description
738
+ ? allTermsMatch(r.description.toLowerCase())
742
739
  : false
743
740
  if (nameMatch || aliasMatch || descMatch) {
744
- map.set(sr.appSlug, sr.displayName)
741
+ map.set(r.parentSlug, r.displayName)
745
742
  }
746
743
  }
747
744
  return map
748
- }, [searchQuery, allSubResources])
745
+ }, [searchQuery, allResources2])
749
746
 
750
747
  // Define columns
751
- const columns = React.useMemo<ColumnDef<AppForCatalog>[]>(
748
+ const columns = React.useMemo<ColumnDef<Resource>[]>(
752
749
  () => [
753
750
  {
754
751
  id: 'application',
@@ -856,7 +853,7 @@ export function AppCatalogGrid({
856
853
  }
857
854
  }, [selectedAppSlug, rowRefs])
858
855
 
859
- const handleAppClick = (app: AppForCatalog) => {
856
+ const handleAppClick = (app: Resource) => {
860
857
  onAppClick?.(app)
861
858
  }
862
859
 
@@ -1,4 +1,4 @@
1
- import type { AppForCatalog } from '@igstack/app-catalog-backend-core'
1
+ import type { Resource } from '@igstack/app-catalog-backend-core'
2
2
  import type { ColumnDef } from '@tanstack/react-table'
3
3
  import {
4
4
  flexRender,
@@ -17,11 +17,11 @@ import {
17
17
  import { getAppUrl } from './appCatalogUtils'
18
18
 
19
19
  export interface AppCatalogTableProps {
20
- apps: AppForCatalog[]
20
+ apps: Resource[]
21
21
  }
22
22
 
23
23
  export function AppCatalogTable({ apps }: AppCatalogTableProps) {
24
- const columns = useMemo<ColumnDef<AppForCatalog>[]>(
24
+ const columns = useMemo<ColumnDef<Resource>[]>(
25
25
  () => [
26
26
  {
27
27
  slug: 'name',
@@ -1,5 +1,5 @@
1
- import type { AppForCatalog } from '@igstack/app-catalog-backend-core'
1
+ import type { Resource } from '@igstack/app-catalog-backend-core'
2
2
 
3
- export function getAppUrl(app: AppForCatalog): string {
3
+ export function getAppUrl(app: Resource): string {
4
4
  return app.appUrl || '#'
5
5
  }
@@ -1,10 +1,10 @@
1
- import type { AppForCatalog } from '@igstack/app-catalog-backend-core'
1
+ import type { Resource } from '@igstack/app-catalog-backend-core'
2
2
  import React from 'react'
3
3
 
4
4
  export interface UseKeyboardNavigationProps {
5
- apps: AppForCatalog[]
5
+ apps: Resource[]
6
6
  selectedAppSlug?: string
7
- onAppClick?: (app: AppForCatalog) => void
7
+ onAppClick?: (app: Resource) => void
8
8
  }
9
9
 
10
10
  export function useKeyboardNavigation({
@@ -1,4 +1,4 @@
1
- import type { AppForCatalog } from '@igstack/app-catalog-backend-core'
1
+ import type { Resource } from '@igstack/app-catalog-backend-core'
2
2
  import { X } from 'lucide-react'
3
3
  import { useDeferredValue, useEffect, useMemo, useState } from 'react'
4
4
  import { Button } from '~/ui/button'
@@ -14,15 +14,14 @@ import { useAppCatalogContext } from '../../context/AppCatalogContext'
14
14
  import { useAppClickHistory } from '../../hooks/useAppClickHistory'
15
15
  import { useAppCounts } from '../../hooks/useAppCounts'
16
16
  import { useUrlSyncedState } from '../../hooks/useUrlSyncedState'
17
- import { searchApps } from '../../utils/searchApps'
17
+ import { searchResources } from '../../utils/searchApps'
18
18
  import { OnboardingCard } from '../components/OnboardingCard'
19
19
  import { useAppCatalogFilters } from '../context/AppCatalogFiltersContext'
20
20
  import { FilterBar } from '../filters/FilterBar'
21
21
  import { AppCatalogGrid } from '../grid/AppCatalogGrid'
22
22
 
23
23
  export function AppCatalogPage() {
24
- const { apps, isLoadingApps, tagsDefinitions, subResources } =
25
- useAppCatalogContext()
24
+ const { resources, isLoadingApps, tagsDefinitions } = useAppCatalogContext()
26
25
  const { state: filterState, actions } = useAppCatalogFilters()
27
26
  const { getTopApps } = useAppClickHistory()
28
27
 
@@ -49,8 +48,14 @@ export function AppCatalogPage() {
49
48
  void getTopApps(10).then(setTopAppSlugs)
50
49
  }, [getTopApps])
51
50
 
51
+ // Get root resources for filtering (children handled internally by searchResources)
52
+ const rootResources = useMemo(
53
+ () => resources.filter((r) => !r.parentSlug),
54
+ [resources],
55
+ )
56
+
52
57
  const filteredApps = useMemo(() => {
53
- let result = apps
58
+ let result = rootResources
54
59
 
55
60
  // Step 1: Filter deprecated apps (if not showing them)
56
61
  if (!filterState.showDeprecated) {
@@ -76,22 +81,27 @@ export function AppCatalogPage() {
76
81
  }
77
82
 
78
83
  // Step 3: Apply search (using deferred value)
79
- result = searchApps(result, deferredSearchValue, subResources)
84
+ // Pass all resources so children contribute to parent scoring
85
+ const childResources = resources.filter((r) => r.parentSlug)
86
+ result = searchResources(
87
+ [...result, ...childResources],
88
+ deferredSearchValue,
89
+ )
80
90
 
81
91
  return result
82
92
  }, [
83
- apps,
93
+ rootResources,
94
+ resources,
84
95
  deferredSearchValue,
85
96
  filterState.recentMode,
86
97
  filterState.tagFilters,
87
98
  filterState.showDeprecated,
88
99
  topAppSlugs,
89
- subResources,
90
100
  ])
91
101
 
92
102
  // Calculate counts for FilterBar
93
103
  const { allCount, recentCount, deprecatedCount } = useAppCounts({
94
- apps,
104
+ apps: rootResources,
95
105
  topAppSlugs,
96
106
  searchValue: deferredSearchValue,
97
107
  })
@@ -103,7 +113,7 @@ export function AppCatalogPage() {
103
113
  }
104
114
  }, [filteredApps, setSelectedAppSlug])
105
115
 
106
- const handleAppClick = (app: AppForCatalog) => {
116
+ const handleAppClick = (app: Resource) => {
107
117
  setSelectedAppSlug(app.slug)
108
118
  }
109
119
 
@@ -115,12 +125,12 @@ export function AppCatalogPage() {
115
125
 
116
126
  // Calculate total apps count (respecting showDeprecated setting)
117
127
  const totalAppsCount = useMemo(() => {
118
- let count = apps.length
128
+ let count = rootResources.length
119
129
  if (!filterState.showDeprecated) {
120
- count = apps.filter((app) => !app.deprecated).length
130
+ count = rootResources.filter((app) => !app.deprecated).length
121
131
  }
122
132
  return count
123
- }, [apps, filterState.showDeprecated])
133
+ }, [rootResources, filterState.showDeprecated])
124
134
 
125
135
  if (isLoadingApps) {
126
136
  return <div className="py-6 text-muted-foreground">Loading…</div>
@@ -140,7 +150,7 @@ export function AppCatalogPage() {
140
150
  totalCount={allCount}
141
151
  recentCount={recentCount}
142
152
  deprecatedCount={deprecatedCount}
143
- apps={apps}
153
+ apps={rootResources}
144
154
  />
145
155
  </div>
146
156
 
@@ -1,8 +1,4 @@
1
- import type {
2
- Group,
3
- Person,
4
- SubResource,
5
- } from '@igstack/app-catalog-backend-core'
1
+ import type { Group, Person, Resource } from '@igstack/app-catalog-backend-core'
6
2
 
7
3
  export function getPersonBySlug(
8
4
  persons: Person[],
@@ -18,9 +14,16 @@ export function getGroupBySlug(
18
14
  return groups.find((g) => g.slug === slug)
19
15
  }
20
16
 
21
- export function getSubResourcesForApp(
22
- subResources: SubResource[],
23
- appSlug: string,
24
- ): SubResource[] {
25
- return subResources.filter((sr) => sr.appSlug === appSlug)
17
+ export function getChildResources(
18
+ resources: Resource[],
19
+ parentSlug: string,
20
+ ): Resource[] {
21
+ return resources.filter((r) => r.parentSlug === parentSlug)
26
22
  }
23
+
24
+ export function getRootResources(resources: Resource[]): Resource[] {
25
+ return resources.filter((r) => !r.parentSlug)
26
+ }
27
+
28
+ /** @deprecated Use getChildResources instead */
29
+ export const getSubResourcesForApp = getChildResources
@@ -1,7 +1,4 @@
1
- import type {
2
- AppForCatalog,
3
- SubResource,
4
- } from '@igstack/app-catalog-backend-core'
1
+ import type { Resource } from '@igstack/app-catalog-backend-core'
5
2
 
6
3
  export interface SearchMatch {
7
4
  /** Field where the match occurred */
@@ -19,12 +16,15 @@ export interface SearchMatch {
19
16
  }
20
17
 
21
18
  export interface SearchResult {
22
- app: AppForCatalog
19
+ app: Resource
23
20
  match: SearchMatch
24
21
  }
25
22
 
26
23
  /**
27
- * Search and sort apps by relevance with highlighting support.
24
+ * Search and sort resources by relevance with highlighting support.
25
+ * Only root resources (no parentSlug) are scored and returned.
26
+ * Child resources (with parentSlug) contribute to their parent's score.
27
+ *
28
28
  * Priority order:
29
29
  * 0. Exact match in abbreviation
30
30
  * 1. Exact match in displayName
@@ -41,19 +41,21 @@ export interface SearchResult {
41
41
  * 12. Teams
42
42
  * 13. Description
43
43
  *
44
- * @param apps - Array of apps to search
44
+ * @param resources - Array of all resources (root + children)
45
45
  * @param searchQuery - Search query string
46
- * @returns Filtered and sorted array of apps with search results
46
+ * @returns Filtered and sorted array of root resources
47
47
  */
48
- export function searchApps(
49
- apps: AppForCatalog[],
48
+ export function searchResources(
49
+ resources: Resource[],
50
50
  searchQuery: string,
51
- subResources?: SubResource[],
52
- ): AppForCatalog[] {
51
+ ): Resource[] {
53
52
  const normalizedQuery = searchQuery.trim().toLowerCase()
54
53
 
54
+ // Separate root resources from children
55
+ const rootResources = resources.filter((r) => !r.parentSlug)
56
+
55
57
  if (normalizedQuery === '') {
56
- return apps
58
+ return rootResources
57
59
  }
58
60
 
59
61
  // Split query into terms for multi-word matching (AND logic)
@@ -63,18 +65,18 @@ export function searchApps(
63
65
  const allTermsMatch = (text: string): boolean =>
64
66
  queryTerms.every((term) => text.includes(term))
65
67
 
66
- // Build sub-resource lookup: appSlug -> SubResource[]
67
- const subResourcesByApp = new Map<string, SubResource[]>()
68
- if (subResources) {
69
- for (const sr of subResources) {
70
- const list = subResourcesByApp.get(sr.appSlug) ?? []
71
- list.push(sr)
72
- subResourcesByApp.set(sr.appSlug, list)
68
+ // Build children lookup: parentSlug -> Resource[]
69
+ const childrenByParent = new Map<string, Resource[]>()
70
+ for (const r of resources) {
71
+ if (r.parentSlug) {
72
+ const list = childrenByParent.get(r.parentSlug) ?? []
73
+ list.push(r)
74
+ childrenByParent.set(r.parentSlug, list)
73
75
  }
74
76
  }
75
77
 
76
- // Filter and score apps
77
- const scoredApps = apps
78
+ // Filter and score root resources
79
+ const scoredApps = rootResources
78
80
  .map((app): SearchResult | null => {
79
81
  const name = app.displayName.toLowerCase()
80
82
  const abbreviation = app.abbreviation?.toLowerCase() || ''
@@ -143,15 +145,15 @@ export function searchApps(
143
145
  return { app, match: { field: 'description', type: 'contains' } }
144
146
  }
145
147
 
146
- // Check sub-resources (name, aliases, description) — supports multi-word queries
147
- const appSubResources = subResourcesByApp.get(app.slug)
148
- if (appSubResources) {
149
- const subMatch = appSubResources.some(
150
- (sr) =>
151
- allTermsMatch(sr.displayName.toLowerCase()) ||
152
- sr.aliases.some((a) => allTermsMatch(a.toLowerCase())) ||
153
- (sr.description
154
- ? allTermsMatch(sr.description.toLowerCase())
148
+ // Check child resources (name, aliases, description) — supports multi-word queries
149
+ const children = childrenByParent.get(app.slug)
150
+ if (children) {
151
+ const subMatch = children.some(
152
+ (r) =>
153
+ allTermsMatch(r.displayName.toLowerCase()) ||
154
+ (r.aliases ?? []).some((a) => allTermsMatch(a.toLowerCase())) ||
155
+ (r.description
156
+ ? allTermsMatch(r.description.toLowerCase())
155
157
  : false),
156
158
  )
157
159
  if (subMatch) {
@@ -259,3 +261,6 @@ export function highlightText(
259
261
 
260
262
  return segments
261
263
  }
264
+
265
+ /** @deprecated Use searchResources instead */
266
+ export const searchApps = searchResources