@igstack/app-catalog-frontend-core 6.3.1-alpha-20261003051755 → 6.5.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 (73) hide show
  1. package/dist/esm/__tests__/modules/appCatalog/ui/detail/AppDetails.subResourceTab.test.d.ts +0 -0
  2. package/dist/esm/__tests__/modules/appCatalog/ui/detail/FeedbackSection.emptyState.test.d.ts +0 -0
  3. package/dist/esm/__tests__/modules/appCatalog/ui/detail/FeedbackSection.reviewStatus.test.d.ts +0 -0
  4. package/dist/esm/__tests__/modules/appCatalog/utils/accessTabLabel.test.d.ts +1 -0
  5. package/dist/esm/api/infra/trpc.d.ts +42 -21
  6. package/dist/esm/modules/appCatalog/hooks/useUpdateApp.d.ts +1 -0
  7. package/dist/esm/modules/appCatalog/ui/catalog/AppCatalogGrid.js +19 -0
  8. package/dist/esm/modules/appCatalog/ui/catalog/AppCatalogGrid.js.map +1 -1
  9. package/dist/esm/modules/appCatalog/ui/catalog/AppDetailPanel.js +17 -2
  10. package/dist/esm/modules/appCatalog/ui/catalog/AppDetailPanel.js.map +1 -1
  11. package/dist/esm/modules/appCatalog/ui/catalog/RequestAppDialog.d.ts +14 -0
  12. package/dist/esm/modules/appCatalog/ui/catalog/RequestAppDialog.js +115 -0
  13. package/dist/esm/modules/appCatalog/ui/catalog/RequestAppDialog.js.map +1 -0
  14. package/dist/esm/modules/appCatalog/ui/components/AccessPrerequisiteChain.js +1 -0
  15. package/dist/esm/modules/appCatalog/ui/components/AccessPrerequisiteChain.js.map +1 -1
  16. package/dist/esm/modules/appCatalog/ui/components/AccessRequestSection.js +11 -4
  17. package/dist/esm/modules/appCatalog/ui/components/AccessRequestSection.js.map +1 -1
  18. package/dist/esm/modules/appCatalog/ui/components/QuickJumpBar.js +0 -4
  19. package/dist/esm/modules/appCatalog/ui/components/QuickJumpBar.js.map +1 -1
  20. package/dist/esm/modules/appCatalog/ui/components/SubResourceDetailPanel.js +4 -1
  21. package/dist/esm/modules/appCatalog/ui/components/SubResourceDetailPanel.js.map +1 -1
  22. package/dist/esm/modules/appCatalog/ui/detail/AppDetails.js +377 -385
  23. package/dist/esm/modules/appCatalog/ui/detail/AppDetails.js.map +1 -1
  24. package/dist/esm/modules/appCatalog/ui/detail/AppDoor.d.ts +20 -0
  25. package/dist/esm/modules/appCatalog/ui/detail/AppDoor.js +59 -0
  26. package/dist/esm/modules/appCatalog/ui/detail/AppDoor.js.map +1 -0
  27. package/dist/esm/modules/appCatalog/ui/detail/DetailTabs.d.ts +64 -0
  28. package/dist/esm/modules/appCatalog/ui/detail/DetailTabs.js +90 -0
  29. package/dist/esm/modules/appCatalog/ui/detail/DetailTabs.js.map +1 -0
  30. package/dist/esm/modules/appCatalog/ui/detail/FeedbackSection.d.ts +13 -0
  31. package/dist/esm/modules/appCatalog/ui/detail/FeedbackSection.js +433 -0
  32. package/dist/esm/modules/appCatalog/ui/detail/FeedbackSection.js.map +1 -0
  33. package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js +18 -1
  34. package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js.map +1 -1
  35. package/dist/esm/modules/appCatalog/utils/accessTabLabel.d.ts +30 -0
  36. package/dist/esm/modules/appCatalog/utils/accessTabLabel.js +34 -0
  37. package/dist/esm/modules/appCatalog/utils/accessTabLabel.js.map +1 -0
  38. package/dist/index.css +269 -5
  39. package/package.json +4 -4
  40. package/src/__tests__/modules/appCatalog/ui/catalog/AppDetailPanel.appSwitch.test.tsx +17 -3
  41. package/src/__tests__/modules/appCatalog/ui/catalog/RequestAppDialog.test.tsx +113 -0
  42. package/src/__tests__/modules/appCatalog/ui/components/AccessRequestSection.test.tsx +6 -4
  43. package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.background.test.tsx +25 -10
  44. package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.doorAndTabs.test.tsx +193 -0
  45. package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.escape.test.tsx +2 -2
  46. package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.sectionOrder.test.tsx +32 -9
  47. package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.subResourceTab.test.tsx +125 -0
  48. package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.tags.test.tsx +2 -2
  49. package/src/__tests__/modules/appCatalog/ui/detail/FeedbackSection.emptyState.test.tsx +181 -0
  50. package/src/__tests__/modules/appCatalog/ui/detail/FeedbackSection.reviewStatus.test.tsx +131 -0
  51. package/src/__tests__/modules/appCatalog/utils/accessTabLabel.test.ts +206 -0
  52. package/src/__tests__/ui/components/header/Header.loginHidden.test.tsx +1 -1
  53. package/src/index.css +269 -5
  54. package/src/modules/appCatalog/ui/catalog/AppCatalogGrid.tsx +24 -0
  55. package/src/modules/appCatalog/ui/catalog/AppDetailPanel.tsx +46 -2
  56. package/src/modules/appCatalog/ui/catalog/RequestAppDialog.tsx +169 -0
  57. package/src/modules/appCatalog/ui/components/AccessPrerequisiteChain.tsx +3 -0
  58. package/src/modules/appCatalog/ui/components/AccessRequestSection.tsx +9 -4
  59. package/src/modules/appCatalog/ui/components/SubResourceDetailPanel.tsx +4 -1
  60. package/src/modules/appCatalog/ui/detail/AppDetails.tsx +574 -554
  61. package/src/modules/appCatalog/ui/detail/AppDoor.tsx +97 -0
  62. package/src/modules/appCatalog/ui/detail/DetailTabs.tsx +195 -0
  63. package/src/modules/appCatalog/ui/detail/FeedbackSection.tsx +605 -0
  64. package/src/modules/appCatalog/ui/pages/AppCatalogPage.tsx +20 -1
  65. package/src/modules/appCatalog/utils/accessTabLabel.ts +95 -0
  66. package/dist/esm/modules/appCatalog/ui/detail/CommentsSection.d.ts +0 -3
  67. package/dist/esm/modules/appCatalog/ui/detail/CommentsSection.js +0 -228
  68. package/dist/esm/modules/appCatalog/ui/detail/CommentsSection.js.map +0 -1
  69. package/src/__tests__/modules/appCatalog/ui/detail/CommentsSection.emptyState.test.tsx +0 -64
  70. package/src/__tests__/modules/appCatalog/ui/detail/CommentsSection.reviewStatus.test.tsx +0 -93
  71. package/src/modules/appCatalog/ui/detail/CommentsSection.tsx +0 -296
  72. /package/dist/esm/__tests__/modules/appCatalog/ui/{detail/CommentsSection.emptyState.test.d.ts → catalog/RequestAppDialog.test.d.ts} +0 -0
  73. /package/dist/esm/__tests__/modules/appCatalog/ui/detail/{CommentsSection.reviewStatus.test.d.ts → AppDetails.doorAndTabs.test.d.ts} +0 -0
@@ -1,6 +1,6 @@
1
1
  import type { Resource } from '@igstack/app-catalog-backend-core'
2
2
  import { X } from 'lucide-react'
3
- import { useEffect, useRef } from 'react'
3
+ import { useEffect, useLayoutEffect, useRef } from 'react'
4
4
  import { useAppCatalogContext } from '../../context/AppCatalogContext'
5
5
  import { SubResourceDetailPanel } from '../components/SubResourceDetailPanel'
6
6
  import { AppDetails } from '../detail/AppDetails'
@@ -38,6 +38,47 @@ export function AppDetailPanel({
38
38
  const { approvalMethods } = useAppCatalogContext()
39
39
  const dialogRef = useRef<HTMLDivElement>(null)
40
40
 
41
+ /**
42
+ * Centre the card ONCE when it opens, then hold that position.
43
+ *
44
+ * `my-auto` centred it on every render, so each panel height change pushed both
45
+ * edges and half the delta became upward movement of everything above it —
46
+ * including the tab strip, which moved 113px and took the control the user had
47
+ * just clicked with it (CLS 0.19, "needs improvement").
48
+ *
49
+ * Top-anchoring fixed the shift but looked wrong: a short card sat high on the
50
+ * page. So pin the centred offset instead. The card still opens centred, and
51
+ * anything that grows it afterwards extends downward only — which is what a
52
+ * reader expects when they open a tab with more in it.
53
+ *
54
+ * Only on open and on resize. Deliberately NOT on content change: reacting to
55
+ * content is precisely the bug.
56
+ */
57
+ useLayoutEffect(() => {
58
+ const card = dialogRef.current
59
+ const scroller = card?.parentElement
60
+ if (!card || !scroller) return
61
+
62
+ const pinCentre = () => {
63
+ card.style.marginTop = '0px'
64
+ const style = getComputedStyle(scroller)
65
+ const usable =
66
+ scroller.clientHeight -
67
+ parseFloat(style.paddingTop) -
68
+ parseFloat(style.paddingBottom)
69
+ const slack = usable - card.offsetHeight
70
+ // Negative slack means the card is already taller than the viewport; leave it
71
+ // at the top and let the overlay scroll, rather than pushing its head off.
72
+ card.style.marginTop = `${Math.max(0, Math.round(slack / 2))}px`
73
+ }
74
+
75
+ pinCentre()
76
+ window.addEventListener('resize', pinCentre)
77
+ return () => window.removeEventListener('resize', pinCentre)
78
+ // Keyed on the opened entry: a different card is a new open, and should
79
+ // re-centre for its own height.
80
+ }, [app.slug, subResource?.slug])
81
+
41
82
  // Focus the dialog on mount so Esc hotkeys fire immediately, even when
42
83
  // the card was opened by a mouse click (which leaves focus on the grid row).
43
84
  //
@@ -75,7 +116,10 @@ export function AppDetailPanel({
75
116
  aria-modal="true"
76
117
  aria-label={`${subResource?.displayName ?? app.displayName} details`}
77
118
  tabIndex={-1}
78
- className="relative w-full max-w-[min(1120px,94vw)] my-auto rounded-[var(--radius)] border border-border bg-background shadow-2xl animate-in fade-in zoom-in-95 duration-200 outline-none"
119
+ // Centred on open, then held — see the pin effect above. `my-auto` is
120
+ // deliberately absent: it re-centres on EVERY height change, which is the
121
+ // layout-shift bug.
122
+ className="relative w-full max-w-[min(1120px,94vw)] rounded-[var(--radius)] border border-border bg-background shadow-2xl animate-in fade-in zoom-in-95 duration-200 outline-none"
79
123
  >
80
124
  <button
81
125
  type="button"
@@ -0,0 +1,169 @@
1
+ import { useMutation, useQueryClient } from '@tanstack/react-query'
2
+ import { useEffect, useState } from 'react'
3
+ import { useTRPC } from '~/api/infra/trpc'
4
+ import { Button } from '~/ui/button'
5
+ import { Dialog, DialogContent, DialogHeader, DialogTitle } from '~/ui/dialog'
6
+ import { cn } from '~/lib/utils'
7
+
8
+ /**
9
+ * Asking for something that is not in the catalog yet.
10
+ *
11
+ * The search box is where people discover the gap — they type a tool's name, get
12
+ * nothing, and until now had nowhere to go. A zero-result search is the single most
13
+ * informative moment in the catalog: someone has told us exactly what they expected to
14
+ * find. Throwing that away and showing only "No results" wastes it.
15
+ *
16
+ * This is a catalog-level ask rather than feedback on an entry: `feedback.add` with a
17
+ * `subject` and NO `resourceSlug`. The server already distinguishes the two that way,
18
+ * and the tracker lists both.
19
+ */
20
+
21
+ /** Same shape the input primitive uses, minus the fixed height. */
22
+ const FIELD_CLASSES =
23
+ 'placeholder:text-muted-foreground dark:bg-input/30 border-input w-full rounded-md border bg-transparent px-3 py-2 text-sm shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50'
24
+
25
+ const MAX_SUBJECT = 120
26
+
27
+ interface RequestAppDialogProps {
28
+ open: boolean
29
+ onOpenChange: (open: boolean) => void
30
+ /** What they searched for. Pre-fills the name, because they just typed it. */
31
+ initialSubject?: string
32
+ }
33
+
34
+ /**
35
+ * Mounts nothing until it is opened, which is what keeps `useTRPC()` out of the
36
+ * closed path. The catalog grid is rendered bare in several tests with no
37
+ * TRPCProvider, and a hook cannot be called conditionally — so the gate has to be a
38
+ * component boundary rather than an `if` inside one.
39
+ */
40
+ export function RequestAppDialog(props: RequestAppDialogProps) {
41
+ if (!props.open) return null
42
+ return <RequestAppDialogBody {...props} />
43
+ }
44
+
45
+ function RequestAppDialogBody({
46
+ open,
47
+ onOpenChange,
48
+ initialSubject = '',
49
+ }: RequestAppDialogProps) {
50
+ const trpc = useTRPC()
51
+ const queryClient = useQueryClient()
52
+ const [subject, setSubject] = useState(initialSubject)
53
+ const [body, setBody] = useState('')
54
+ const [sent, setSent] = useState(false)
55
+
56
+ // Re-seed when opened for a different search. Keyed on the dialog opening rather
57
+ // than on every keystroke, so it never overwrites something half-typed.
58
+ useEffect(() => {
59
+ if (open) {
60
+ setSubject(initialSubject)
61
+ setBody('')
62
+ setSent(false)
63
+ }
64
+ }, [open, initialSubject])
65
+
66
+ const add = useMutation(
67
+ trpc.feedback.add.mutationOptions({
68
+ onSuccess: () => {
69
+ setSent(true)
70
+ // The tracker counts this, so its badge has to notice.
71
+ void queryClient.invalidateQueries({
72
+ queryKey: trpc.feedback.mine.queryKey(),
73
+ })
74
+ },
75
+ }),
76
+ )
77
+
78
+ const trimmed = subject.trim()
79
+ const send = () => {
80
+ if (!trimmed) return
81
+ add.mutate({
82
+ subject: trimmed.slice(0, MAX_SUBJECT),
83
+ ...(body.trim() ? { body: body.trim() } : {}),
84
+ })
85
+ }
86
+
87
+ return (
88
+ <Dialog open={open} onOpenChange={onOpenChange}>
89
+ <DialogContent className="sm:max-w-[480px]">
90
+ <DialogHeader>
91
+ <DialogTitle>
92
+ {sent ? 'Thanks — that is logged' : 'Ask for an app to be added'}
93
+ </DialogTitle>
94
+ </DialogHeader>
95
+
96
+ {sent ? (
97
+ <div className="space-y-3">
98
+ <p className="text-muted-foreground text-sm">
99
+ A maintainer will look at it. You can see what happened to it
100
+ under your own requests.
101
+ </p>
102
+ <Button type="button" size="sm" onClick={() => onOpenChange(false)}>
103
+ Close
104
+ </Button>
105
+ </div>
106
+ ) : (
107
+ <div className="space-y-3">
108
+ <div className="space-y-1.5">
109
+ <label htmlFor="request-app-name" className="text-sm font-medium">
110
+ What are you looking for?
111
+ </label>
112
+ <input
113
+ id="request-app-name"
114
+ autoFocus
115
+ className={cn(FIELD_CLASSES)}
116
+ placeholder="Name of the tool, or what it does"
117
+ maxLength={MAX_SUBJECT}
118
+ value={subject}
119
+ onChange={(event) => setSubject(event.target.value)}
120
+ />
121
+ </div>
122
+
123
+ <div className="space-y-1.5">
124
+ <label
125
+ htmlFor="request-app-detail"
126
+ className="text-sm font-medium"
127
+ >
128
+ Anything that would help us find it{' '}
129
+ <span className="text-muted-foreground font-normal">
130
+ (optional)
131
+ </span>
132
+ </label>
133
+ <textarea
134
+ id="request-app-detail"
135
+ rows={3}
136
+ className={cn(FIELD_CLASSES)}
137
+ placeholder="A link, the team that owns it, what you needed it for"
138
+ value={body}
139
+ onChange={(event) => setBody(event.target.value)}
140
+ />
141
+ </div>
142
+
143
+ <div className="flex flex-wrap items-center gap-3">
144
+ <Button
145
+ type="button"
146
+ size="sm"
147
+ disabled={!trimmed || add.isPending}
148
+ onClick={send}
149
+ >
150
+ {add.isPending ? 'Sending…' : 'Send request'}
151
+ </Button>
152
+ <span className="text-muted-foreground text-xs">
153
+ Sent under a nickname. No login needed.
154
+ </span>
155
+ </div>
156
+
157
+ {add.error && (
158
+ <p className="text-destructive text-xs">
159
+ {add.error instanceof Error
160
+ ? add.error.message
161
+ : 'Something went wrong'}
162
+ </p>
163
+ )}
164
+ </div>
165
+ )}
166
+ </DialogContent>
167
+ </Dialog>
168
+ )
169
+ }
@@ -31,6 +31,9 @@ export function AccessPrerequisiteChain({
31
31
 
32
32
  return (
33
33
  <div
34
+ // Part of the access story, so it carries the same anchor the instructions
35
+ // do — a reader (and a test) wants the whole chain, not half of it.
36
+ data-access-section=""
34
37
  className="mt-6 rounded-lg border-[1.5px] border-primary/40 bg-primary/[0.03] p-4"
35
38
  role="note"
36
39
  aria-label="Access requires prerequisite steps"
@@ -235,10 +235,15 @@ const UNDOCUMENTED_WITHOUT_CONTACT =
235
235
  */
236
236
  function AccessBox({ children }: { children: ReactNode }) {
237
237
  return (
238
- <div className="mt-6 rounded-lg border-[1.5px] border-primary/40 bg-primary/[0.03] p-4 space-y-4">
239
- <h3 className="text-sm font-semibold text-foreground">
240
- How to get access
241
- </h3>
238
+ // No frame and no heading. Both existed to mark this out as THE thing on a long
239
+ // scrolling card; now it is its own tab, so the tab's label already says what
240
+ // this is and the panel edge already bounds it. A box inside a box, under a
241
+ // heading that repeats the tab you just clicked, is two statements of the same
242
+ // fact and ink the reader has to look past.
243
+ // A stable anchor for tests, because neither of the old handles survives: the
244
+ // heading is gone, and a sub-resource page has no tab panel to read either.
245
+ // Text and structure both move; this does not.
246
+ <div data-access-section="" className="space-y-4">
242
247
  {children}
243
248
  </div>
244
249
  )
@@ -104,7 +104,10 @@ export function SubResourceDetailPanel({
104
104
 
105
105
  {/* Two-step access section */}
106
106
  {hasAnyAccess && (
107
- <div className="mt-6 space-y-6">
107
+ // The whole two-step region carries the anchor: the step labels naming
108
+ // the parent live out here, not inside the nested AccessRequestSections,
109
+ // so anchoring only those dropped the parent from the chain.
110
+ <div data-access-section="" className="mt-6 space-y-6">
108
111
  <h3 className="text-sm font-medium">How to get access</h3>
109
112
 
110
113
  {hasTwoStepAccess ? (