@igstack/app-catalog-frontend-core 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/dist/esm/__tests__/integration/harness/given.d.ts +1 -0
  2. package/dist/esm/__tests__/integration/skewWarning.integration.test.d.ts +0 -0
  3. package/dist/esm/__tests__/integration/tools/AppDetailTools.d.ts +5 -0
  4. package/dist/esm/__tests__/integration/tools/CatalogTools.d.ts +15 -1
  5. package/dist/esm/__tests__/modules/appCatalog/utils/resolveHelpers.test.d.ts +1 -0
  6. package/dist/esm/modules/appCatalog/hooks/useSessionSyncedState.d.ts +40 -0
  7. package/dist/esm/modules/appCatalog/hooks/useSessionSyncedState.js +40 -0
  8. package/dist/esm/modules/appCatalog/hooks/useSessionSyncedState.js.map +1 -0
  9. package/dist/esm/modules/appCatalog/ui/components/AccessPrerequisiteChain.d.ts +17 -0
  10. package/dist/esm/modules/appCatalog/ui/components/AccessPrerequisiteChain.js +61 -0
  11. package/dist/esm/modules/appCatalog/ui/components/AccessPrerequisiteChain.js.map +1 -0
  12. package/dist/esm/modules/appCatalog/ui/components/AccessRequestSection.js +9 -2
  13. package/dist/esm/modules/appCatalog/ui/components/AccessRequestSection.js.map +1 -1
  14. package/dist/esm/modules/appCatalog/ui/components/OnboardingCard.js +1 -1
  15. package/dist/esm/modules/appCatalog/ui/components/OnboardingCard.js.map +1 -1
  16. package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.d.ts +9 -1
  17. package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js +12 -3
  18. package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js.map +1 -1
  19. package/dist/esm/modules/appCatalog/ui/context/AppCatalogFiltersContext.js +3 -2
  20. package/dist/esm/modules/appCatalog/ui/context/AppCatalogFiltersContext.js.map +1 -1
  21. package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.d.ts +5 -0
  22. package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js +56 -10
  23. package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js.map +1 -1
  24. package/dist/esm/modules/appCatalog/ui/launcher/LauncherDetailPanel.d.ts +12 -0
  25. package/dist/esm/modules/appCatalog/ui/launcher/LauncherDetailPanel.js +48 -0
  26. package/dist/esm/modules/appCatalog/ui/launcher/LauncherDetailPanel.js.map +1 -0
  27. package/dist/esm/modules/appCatalog/ui/launcher/LauncherHome.d.ts +19 -0
  28. package/dist/esm/modules/appCatalog/ui/launcher/LauncherHome.js +355 -0
  29. package/dist/esm/modules/appCatalog/ui/launcher/LauncherHome.js.map +1 -0
  30. package/dist/esm/modules/appCatalog/ui/launcher/ResourceIcon.d.ts +13 -0
  31. package/dist/esm/modules/appCatalog/ui/launcher/ResourceIcon.js +45 -0
  32. package/dist/esm/modules/appCatalog/ui/launcher/ResourceIcon.js.map +1 -0
  33. package/dist/esm/modules/appCatalog/ui/launcher/monogram.d.ts +4 -0
  34. package/dist/esm/modules/appCatalog/ui/launcher/monogram.js +41 -0
  35. package/dist/esm/modules/appCatalog/ui/launcher/monogram.js.map +1 -0
  36. package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js +40 -6
  37. package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js.map +1 -1
  38. package/dist/esm/modules/appCatalog/utils/markdownToPlainText.d.ts +1 -1
  39. package/dist/esm/modules/appCatalog/utils/markdownToPlainText.js.map +1 -1
  40. package/dist/esm/modules/appCatalog/utils/resolveHelpers.d.ts +10 -0
  41. package/dist/esm/modules/appCatalog/utils/resolveHelpers.js +17 -0
  42. package/dist/esm/modules/appCatalog/utils/resolveHelpers.js.map +1 -1
  43. package/dist/esm/routeTree.gen.d.ts +1 -1
  44. package/dist/esm/routes/_layout/app.$slug.d.ts +1 -1
  45. package/dist/esm/routes/_layout/app._slug.js +4 -3
  46. package/dist/esm/routes/_layout/app._slug.js.map +1 -1
  47. package/dist/esm/routes/_layout/catalog.apps.index.js +2 -1
  48. package/dist/esm/routes/_layout/catalog.apps.index.js.map +1 -1
  49. package/dist/esm/routes/_layout/index.js +2 -1
  50. package/dist/esm/routes/_layout/index.js.map +1 -1
  51. package/dist/esm/routes/_layout/service-desks.js +2 -1
  52. package/dist/esm/routes/_layout/service-desks.js.map +1 -1
  53. package/dist/esm/ui/components/header/Header.js +1 -1
  54. package/dist/esm/ui/components/header/Header.js.map +1 -1
  55. package/dist/index.css +73 -66
  56. package/package.json +4 -4
  57. package/src/__tests__/integration/appCatalog.integration.test.ts +106 -0
  58. package/src/__tests__/integration/appDeepLink.integration.test.ts +50 -24
  59. package/src/__tests__/integration/crossRefLinkListView.integration.test.ts +8 -8
  60. package/src/__tests__/integration/harness/given.tsx +9 -1
  61. package/src/__tests__/integration/replacementLink.integration.test.ts +6 -2
  62. package/src/__tests__/integration/searchDeprecatedFallback.integration.test.ts +21 -16
  63. package/src/__tests__/integration/setup/testSetup.ts +1 -0
  64. package/src/__tests__/integration/skewWarning.integration.test.ts +56 -0
  65. package/src/__tests__/integration/tools/AppDetailTools.ts +13 -0
  66. package/src/__tests__/integration/tools/CatalogTools.ts +60 -11
  67. package/src/__tests__/modules/appCatalog/utils/markdownToPlainText.test.ts +5 -5
  68. package/src/__tests__/modules/appCatalog/utils/resolveHelpers.test.ts +79 -0
  69. package/src/index.css +73 -66
  70. package/src/modules/appCatalog/hooks/useSessionSyncedState.ts +83 -0
  71. package/src/modules/appCatalog/ui/components/AccessPrerequisiteChain.tsx +89 -0
  72. package/src/modules/appCatalog/ui/components/AccessRequestSection.tsx +31 -2
  73. package/src/modules/appCatalog/ui/components/OnboardingCard.tsx +1 -1
  74. package/src/modules/appCatalog/ui/components/SubResourcesSection.tsx +22 -1
  75. package/src/modules/appCatalog/ui/context/AppCatalogFiltersContext.tsx +11 -5
  76. package/src/modules/appCatalog/ui/grid/AppCatalogGrid.tsx +68 -10
  77. package/src/modules/appCatalog/ui/launcher/LauncherDetailPanel.tsx +55 -0
  78. package/src/modules/appCatalog/ui/launcher/LauncherHome.tsx +497 -0
  79. package/src/modules/appCatalog/ui/launcher/ResourceIcon.tsx +54 -0
  80. package/src/modules/appCatalog/ui/launcher/monogram.ts +41 -0
  81. package/src/modules/appCatalog/ui/pages/AppCatalogPage.tsx +75 -13
  82. package/src/modules/appCatalog/utils/markdownToPlainText.ts +1 -1
  83. package/src/modules/appCatalog/utils/resolveHelpers.ts +31 -0
  84. package/src/routes/_layout/app.$slug.tsx +7 -4
  85. package/src/routes/_layout/catalog.apps.index.tsx +4 -1
  86. package/src/routes/_layout/index.tsx +5 -1
  87. package/src/routes/_layout/service-desks.tsx +4 -1
  88. package/src/ui/components/header/Header.tsx +3 -1
@@ -29,6 +29,9 @@ import {
29
29
  TableRow,
30
30
  } from '~/ui/table'
31
31
  import { AccessRequestSection } from '../components/AccessRequestSection'
32
+ import { AccessPrerequisiteChain } from '../components/AccessPrerequisiteChain'
33
+ import { useAppCatalogFilters } from '../context/AppCatalogFiltersContext'
34
+ import { PersonBadge } from '../components/PersonBadge'
32
35
  import { useUser } from '~/modules/auth'
33
36
  import { InlineEditableField } from '../components/InlineEditableField'
34
37
  import { MarkdownText } from '../components/MarkdownText'
@@ -173,6 +176,10 @@ function AppScreenshot({ app }: { app: Resource }) {
173
176
 
174
177
  function TiersAndSubResourcesPanel({ app }: { app: Resource }) {
175
178
  const { resources } = useAppCatalogContext()
179
+ // #38 item C: the active catalog search term. When the user reached this app
180
+ // by searching something that matched a child, seed the sub-resource filter
181
+ // with it so the matched child is revealed.
182
+ const { state: filterState } = useAppCatalogFilters()
176
183
  const appSubResources = React.useMemo(
177
184
  () => getChildResources(resources, app.slug),
178
185
  [resources, app.slug],
@@ -187,14 +194,17 @@ function TiersAndSubResourcesPanel({ app }: { app: Resource }) {
187
194
  )}
188
195
  {appSubResources.length > 0 && (
189
196
  <div className="mt-6">
190
- <SubResourcesSection subResources={appSubResources} />
197
+ <SubResourcesSection
198
+ subResources={appSubResources}
199
+ initialSearch={filterState.searchValue}
200
+ />
191
201
  </div>
192
202
  )}
193
203
  </>
194
204
  )
195
205
  }
196
206
 
197
- function AppDetails({
207
+ export function AppDetails({
198
208
  app,
199
209
  onAppClick,
200
210
  onClosePanel,
@@ -269,7 +279,7 @@ function AppDetails({
269
279
  <AppIcon app={app} className="size-16" />
270
280
  <div className="-mx-3 flex-1 min-w-0">
271
281
  <div className="flex items-center gap-2 px-3">
272
- <div className="text-2xl font-semibold min-w-0">
282
+ <div className="font-serif text-2xl font-semibold min-w-0">
273
283
  {app.abbreviation
274
284
  ? `${app.displayName} (${app.abbreviation})`
275
285
  : app.displayName}
@@ -333,10 +343,11 @@ function AppDetails({
333
343
  target="_blank"
334
344
  rel="noopener noreferrer"
335
345
  onClick={() => recordClick(app.slug)}
336
- className="inline-flex items-center gap-1 rounded-md py-1 text-sm text-blue-600 hover:bg-accent/30 hover:underline dark:text-blue-400 transition-all"
346
+ className="inline-flex items-center gap-1.5 rounded-md border border-border px-2.5 py-1 text-xs text-muted-foreground hover:border-primary hover:text-primary transition-all"
347
+ title="Open app in new tab (secondary — see access info below)"
337
348
  >
338
349
  {app.appUrl.replace(/https?:\/\//g, '')}
339
- <ExternalLink className="size-3.5 shrink-0 opacity-40 transition-opacity" />
350
+ <ExternalLink className="size-3 shrink-0 opacity-60" />
340
351
  </a>
341
352
  ) : (
342
353
  <span className="text-muted-foreground">—</span>
@@ -387,6 +398,13 @@ function AppDetails({
387
398
  )
388
399
  })()}
389
400
 
401
+ {/* Two-step access (#38 D): for a nested resource, show the parent-first
402
+ prerequisite chain before this resource's own access instructions. */}
403
+ <AccessPrerequisiteChain resource={app} onOpenParent={onAppClick} />
404
+
405
+ {/* Access Request Section — hero of the detail, shown before description */}
406
+ <AccessRequestSection app={app} approvalMethods={approvalMethods} />
407
+
390
408
  {/* Description */}
391
409
  <div className="mt-6">
392
410
  <h3 className="mb-2 text-sm font-medium">Description</h3>
@@ -429,8 +447,17 @@ function AppDetails({
429
447
  </div>
430
448
  )}
431
449
 
432
- {/* Access Request Section */}
433
- <AccessRequestSection app={app} approvalMethods={approvalMethods} />
450
+ {/* Owner who is responsible for this resource. Distinct from the
451
+ access approver (who decides access requests); see domain model. */}
452
+ {app.ownerPersonSlug && (
453
+ <div className="mt-6">
454
+ <h3 className="mb-1 text-sm font-medium">Owner</h3>
455
+ <p className="mb-2 text-xs text-muted-foreground">
456
+ Who is responsible for this resource
457
+ </p>
458
+ <PersonBadge slug={app.ownerPersonSlug} />
459
+ </div>
460
+ )}
434
461
 
435
462
  {/* Tier Variants and Sub-Resources */}
436
463
  <TiersAndSubResourcesPanel app={app} />
@@ -742,6 +769,9 @@ export function AppCatalogGrid({
742
769
  onAppClick,
743
770
  })
744
771
 
772
+ // Launch history for the secondary "open in new tab" action on each row.
773
+ const { recordClick: recordLaunch } = useAppClickHistory()
774
+
745
775
  // Build a map of parentSlug -> matched child resource displayName for search annotation
746
776
  const { resources: allResources2 } = useAppCatalogContext()
747
777
  const matchedSubResourceMap = React.useMemo(() => {
@@ -847,8 +877,36 @@ export function AppCatalogGrid({
847
877
  </div>
848
878
  ),
849
879
  },
880
+ {
881
+ // Secondary "launch" action (#31): the primary row click opens the
882
+ // detail panel (how to get access); this quiet ↗ button is the fast
883
+ // "I just want the URL" jump. stopPropagation so it doesn't also open
884
+ // the panel. Only shown when the resource has a URL.
885
+ id: 'launch',
886
+ header: '',
887
+ cell: ({ row }) =>
888
+ row.original.appUrl ? (
889
+ <a
890
+ href={row.original.appUrl}
891
+ target="_blank"
892
+ rel="noopener noreferrer"
893
+ aria-label={`Open ${row.original.displayName} in a new tab`}
894
+ title={`Open ${row.original.displayName}`}
895
+ onClick={(e) => {
896
+ e.stopPropagation()
897
+ recordLaunch(row.original.slug)
898
+ }}
899
+ className="inline-flex size-8 items-center justify-center rounded-full border text-muted-foreground opacity-0 transition-opacity hover:border-primary hover:text-primary group-hover:opacity-100 focus-visible:opacity-100"
900
+ >
901
+ <ExternalLink className="size-4" />
902
+ </a>
903
+ ) : null,
904
+ meta: {
905
+ className: 'w-[56px] text-right',
906
+ },
907
+ },
850
908
  ],
851
- [searchQuery, matchedSubResourceMap],
909
+ [searchQuery, matchedSubResourceMap, recordLaunch],
852
910
  )
853
911
 
854
912
  // Create a single table instance with all apps
@@ -937,7 +995,7 @@ export function AppCatalogGrid({
937
995
  className="px-4 py-6 sticky top-[49px] bg-muted/90 backdrop-blur z-10"
938
996
  >
939
997
  <div className="flex items-center justify-center">
940
- <span className="font-bold text-lg tracking-widest uppercase leading-loose text-muted-foreground">
998
+ <span className="font-serif font-semibold text-lg tracking-widest uppercase leading-loose text-muted-foreground">
941
999
  {group.groupName}
942
1000
  </span>
943
1001
  </div>
@@ -963,7 +1021,7 @@ export function AppCatalogGrid({
963
1021
  }}
964
1022
  onClick={() => handleAppClick(row.original)}
965
1023
  className={cn(
966
- 'border-b cursor-pointer transition-colors',
1024
+ 'group border-b cursor-pointer transition-colors',
967
1025
  selectedApp?.id === row.original.id
968
1026
  ? 'bg-blue-100 dark:bg-blue-950 hover:bg-blue-200 dark:hover:bg-blue-900'
969
1027
  : 'hover:bg-muted/30',
@@ -0,0 +1,55 @@
1
+ import type { Resource } from '@igstack/app-catalog-backend-core'
2
+ import { useEffect } from 'react'
3
+ import { AppDetails } from '../grid/AppCatalogGrid'
4
+
5
+ /**
6
+ * Slide-over detail panel for the launcher (#38, item B). Renders the rich
7
+ * AppDetails (access-hero, sub-resources, tiers — increments 3/4) as a right
8
+ * slide-over over the launcher backdrop, matching the option-a prototype,
9
+ * instead of dropping the user into the old grid + resizable split-pane.
10
+ */
11
+ export function LauncherDetailPanel({
12
+ app,
13
+ onClose,
14
+ onAppClick,
15
+ }: {
16
+ app: Resource
17
+ onClose: () => void
18
+ onAppClick?: (app: Resource) => void
19
+ }) {
20
+ // Close on Escape.
21
+ useEffect(() => {
22
+ const onKey = (e: KeyboardEvent) => {
23
+ if (e.key === 'Escape') onClose()
24
+ }
25
+ document.addEventListener('keydown', onKey)
26
+ return () => document.removeEventListener('keydown', onKey)
27
+ }, [onClose])
28
+
29
+ return (
30
+ <>
31
+ {/* scrim */}
32
+ <button
33
+ type="button"
34
+ aria-label="Close details panel"
35
+ onClick={onClose}
36
+ className="fixed inset-0 z-40 bg-black/30 backdrop-blur-[2px] animate-in fade-in"
37
+ />
38
+ {/* panel */}
39
+ <aside
40
+ role="dialog"
41
+ aria-modal="true"
42
+ aria-label={`${app.displayName} details`}
43
+ className="fixed right-0 top-0 z-50 h-full w-[min(560px,100%)] bg-background shadow-2xl border-l border-border flex flex-col animate-in slide-in-from-right duration-200"
44
+ >
45
+ <div className="overflow-y-auto flex-1 px-6 py-5">
46
+ <AppDetails
47
+ app={app}
48
+ onAppClick={onAppClick}
49
+ onClosePanel={onClose}
50
+ />
51
+ </div>
52
+ </aside>
53
+ </>
54
+ )
55
+ }